diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 9b70d91a08..4b15c8b510 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -180,11 +180,11 @@ "resources": [], "templateHtml": "\n", "templateCss": "#container tb-markdown-widget {\n height: 100%;\n display: block;\n}\n\n#container tb-markdown-widget .tb-markdown-view {\n height: 100%;\n overflow: auto;\n}\n", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.markdownWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.markdownWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true,\n hasDataPageLink: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Markdown/HTML card\",\n \"properties\": {\n \"markdownTextPattern\": {\n \"title\": \"Markdown/HTML pattern (markdown or HTML with variables, for ex. '${entityName} or ${keyName} - some text.')\",\n \"type\": \"string\",\n \"default\": \"# Markdown/HTML card \\n - **Current entity**: **${entityName}**. \\n - **Current value**: **${Random}**.\"\n },\n \"markdownCss\": {\n \"title\": \"Markdown/HTML CSS\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useMarkdownTextFunction\": {\n \"title\": \"Use markdown/HTML value function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"markdownTextFunction\": {\n \"title\": \"Markdown/HTML value function: f(data)\",\n \"type\": \"string\",\n \"default\": \"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useMarkdownTextFunction\",\n {\n \"key\": \"markdownTextPattern\",\n \"type\": \"markdown\",\n \"condition\": \"model.useMarkdownTextFunction !== true\"\n },\n {\n \"key\": \"markdownTextFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/markdown/markdown_text_fn\",\n \"condition\": \"model.useMarkdownTextFunction === true\"\n },\n {\n \"key\": \"markdownCss\",\n \"type\": \"css\"\n }\n ]\n}\n", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"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\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"markdownTextPattern\":\"### Markdown/HTML card\\n - **Current entity**: ${entityName}.\\n - **Current value**: ${Random}.\",\"markdownTextFunction\":\"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\",\"useMarkdownTextFunction\":false},\"title\":\"Markdown/HTML Card\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" } } ] -} \ No newline at end of file +} 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 1b085be999..3d81c0e5fe 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 @@ -38,7 +38,7 @@ "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\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"saveButtonLabel\": {\n \"title\": \"'SAVE' button label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"resetButtonLabel\": {\n \"title\": \"'UNDO' button label\",\n \"type\": \"string\",\n \"default\": \"\"\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 {\n \"key\": \"updateAllValues\",\n \"condition\": \"model.showActionButtons === true\"\n },\n {\n \"key\": \"saveButtonLabel\",\n \"condition\": \"model.showActionButtons === true\"\n },\n {\n \"key\": \"resetButtonLabel\",\n \"condition\": \"model.showActionButtons === true\"\n },\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 {\n \"key\": \"fieldsInRow\",\n \"condition\": \"model.fieldsAlignment === 'row'\"\n }\n ]\n}", - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"dataKeyType\": {\n \"title\": \"Datakey type\",\n \"type\": \"string\",\n \"default\": \"server\"\n },\n \"dataKeyValueType\": {\n \"title\": \"Datakey value type\",\n \"type\": \"string\",\n \"default\": \"string\"\n },\n \"selectOptions\": {\n \"title\": \"Select options\",\n \"type\": \"array\",\n \"default\": [],\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"value\": {\n \"title\": \"Value (write 'null' for create empty option)\",\n \"type\": \"string\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n }\n },\n \"required\": [\"value\"]\n }\n },\n \"step\": {\n \"title\": \"Step interval between values\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\"\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\"\n },\n \"required\": {\n \"title\": \"Value is required\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"minValueErrorMessage\": {\n \"title\": \"'Min Value' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValueErrorMessage\": {\n \"title\": \"'Max Value' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"invalidDateErrorMessage\": {\n \"title\": \"'Invalid Date' error message\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"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 \"value\": \"select\",\n \"label\": \"Select\"\n }\n ]\n },\n {\n \"key\": \"selectOptions\",\n \"condition\": \"model.dataKeyValueType === 'select'\",\n \"items\": [\"selectOptions[].value\", \"selectOptions[].label\"]\n },\n {\n \"key\": \"step\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"minValue\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"maxValue\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n \"required\",\n {\n \"key\": \"requiredErrorMessage\",\n \"condition\": \"model.required === true\"\n },\n {\n \"key\": \"invalidDateErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'dateTime' || model.dataKeyValueType === 'date' || model.dataKeyValueType === 'time'\"\n },\n {\n \"key\": \"minValueErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"maxValueErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\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\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t}\n ]\n}\n", + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"dataKeyType\": {\n \"title\": \"Datakey type\",\n \"type\": \"string\",\n \"default\": \"server\"\n },\n \"dataKeyValueType\": {\n \"title\": \"Datakey value type\",\n \"type\": \"string\",\n \"default\": \"string\"\n },\n \"slideToggleLabelPosition\": {\n \"title\": \"Label appears after or before the slide-toggle\",\n \"type\": \"string\",\n \"default\": \"after\"\n },\n \"selectOptions\": {\n \"title\": \"Select options\",\n \"type\": \"array\",\n \"default\": [],\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"value\": {\n \"title\": \"Value (write 'null' for create empty option)\",\n \"type\": \"string\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n }\n },\n \"required\": [\"value\"]\n }\n },\n \"step\": {\n \"title\": \"Step interval between values\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\"\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\"\n },\n \"required\": {\n \"title\": \"Value is required\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"minValueErrorMessage\": {\n \"title\": \"'Min Value' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValueErrorMessage\": {\n \"title\": \"'Max Value' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"invalidDateErrorMessage\": {\n \"title\": \"'Invalid Date' error message\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"useCustomIcon\": {\n \"title\": \"Use custom icon\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"customIcon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useGetValueFunction\": {\n \"title\": \"use getValue function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"getValueFunctionBody\": {\n \"title\": \"getValue function: f(value, ctx)\",\n \"type\": \"string\"\n },\n \"useSetValueFunction\": {\n \"title\": \"use setValue function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"setValueFunctionBody\": {\n \"title\": \"setValue function: f(value, originValue, ctx)\",\n \"type\": \"string\"\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 \"value\": \"select\",\n \"label\": \"Select\"\n }\n ]\n },\n {\n \"key\": \"slideToggleLabelPosition\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"condition\": \"model.dataKeyValueType === 'booleanSwitch'\",\n \"items\": [\n {\n \"value\": \"after\",\n \"label\": \"After\"\n },\n {\n \"value\": \"before\",\n \"label\": \"Before\"\n }\n ]\n },\n {\n \"key\": \"selectOptions\",\n \"condition\": \"model.dataKeyValueType === 'select'\",\n \"items\": [\"selectOptions[].value\", \"selectOptions[].label\"]\n },\n {\n \"key\": \"step\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"minValue\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"maxValue\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n \"required\",\n {\n \"key\": \"requiredErrorMessage\",\n \"condition\": \"model.required === true\"\n },\n {\n \"key\": \"invalidDateErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'dateTime' || model.dataKeyValueType === 'date' || model.dataKeyValueType === 'time'\"\n },\n {\n \"key\": \"minValueErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"maxValueErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\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 \"useCustomIcon\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\",\n\t\t\t\"condition\": \"model.useCustomIcon !== true\"\n\t\t},\n\t\t{\n \t\t\"key\": \"customIcon\",\n\t\t\t\"type\": \"image\",\n\t\t\t\"condition\": \"model.useCustomIcon === true\"\n\t\t},\n\t\t\"useGetValueFunction\",\n\t\t{\n\t\t \"key\": \"getValueFunctionBody\",\n\t\t \"type\": \"javascript\",\n\t\t \"condition\": \"model.useGetValueFunction === true\"\n\t\t},\n\t\t\"useSetValueFunction\",\n\t\t{\n\t\t \"key\": \"setValueFunctionBody\",\n\t\t \"type\": \"javascript\",\n\t\t \"condition\": \"model.useSetValueFunction === true\"\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/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index 7a6a1620ae..70a6b029ed 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -573,10 +573,9 @@ public class TelemetryController extends BaseController { for (String key : keys) { deleteTsKvQueries.add(new BaseDeleteTsKvQuery(key, deleteFromTs, deleteToTs, rewriteLatestIfDeleted)); } - ListenableFuture> future = tsService.remove(user.getTenantId(), entityId, deleteTsKvQueries); - Futures.addCallback(future, new FutureCallback<>() { + tsSubService.deleteTimeseriesAndNotify(tenantId, entityId, keys, deleteTsKvQueries, new FutureCallback<>() { @Override - public void onSuccess(@Nullable List tmp) { + public void onSuccess(@Nullable Void tmp) { logTimeseriesDeleted(user, entityId, keys, deleteFromTs, deleteToTs, null); result.setResult(new ResponseEntity<>(HttpStatus.OK)); } @@ -586,7 +585,7 @@ public class TelemetryController extends BaseController { logTimeseriesDeleted(user, entityId, keys, deleteFromTs, deleteToTs, t); result.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); } - }, executor); + }); }); } diff --git a/application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java b/application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java index 0c372fdfd6..883745e643 100644 --- a/application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java @@ -17,11 +17,11 @@ package org.thingsboard.server.service.ota; import com.google.common.util.concurrent.FutureCallback; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Required; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg; +import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -51,8 +51,6 @@ import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory; -import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.cluster.TbClusterService; import javax.annotation.Nullable; import java.util.ArrayList; @@ -122,17 +120,17 @@ public class DefaultOtaPackageStateService implements OtaPackageStateService { newFirmwareId = newDeviceProfile.getFirmwareId(); } if (oldDevice != null) { + OtaPackageId oldFirmwareId = oldDevice.getFirmwareId(); + if (oldFirmwareId == null) { + DeviceProfile oldDeviceProfile = deviceProfileService.findDeviceProfileById(oldDevice.getTenantId(), oldDevice.getDeviceProfileId()); + oldFirmwareId = oldDeviceProfile.getFirmwareId(); + } if (newFirmwareId != null) { - OtaPackageId oldFirmwareId = oldDevice.getFirmwareId(); - if (oldFirmwareId == null) { - DeviceProfile oldDeviceProfile = deviceProfileService.findDeviceProfileById(oldDevice.getTenantId(), oldDevice.getDeviceProfileId()); - oldFirmwareId = oldDeviceProfile.getFirmwareId(); - } if (!newFirmwareId.equals(oldFirmwareId)) { // Device was updated and new firmware is different from previous firmware. send(device.getTenantId(), device.getId(), newFirmwareId, System.currentTimeMillis(), FIRMWARE); } - } else { + } else if (oldFirmwareId != null){ // Device was updated and new firmware is not set. remove(device, FIRMWARE); } @@ -149,17 +147,17 @@ public class DefaultOtaPackageStateService implements OtaPackageStateService { newSoftwareId = newDeviceProfile.getSoftwareId(); } if (oldDevice != null) { + OtaPackageId oldSoftwareId = oldDevice.getSoftwareId(); + if (oldSoftwareId == null) { + DeviceProfile oldDeviceProfile = deviceProfileService.findDeviceProfileById(oldDevice.getTenantId(), oldDevice.getDeviceProfileId()); + oldSoftwareId = oldDeviceProfile.getSoftwareId(); + } if (newSoftwareId != null) { - OtaPackageId oldSoftwareId = oldDevice.getSoftwareId(); - if (oldSoftwareId == null) { - DeviceProfile oldDeviceProfile = deviceProfileService.findDeviceProfileById(oldDevice.getTenantId(), oldDevice.getDeviceProfileId()); - oldSoftwareId = oldDeviceProfile.getSoftwareId(); - } if (!newSoftwareId.equals(oldSoftwareId)) { // Device was updated and new firmware is different from previous firmware. send(device.getTenantId(), device.getId(), newSoftwareId, System.currentTimeMillis(), SOFTWARE); } - } else { + } else if (oldSoftwareId != null){ // Device was updated and new firmware is not set. remove(device, SOFTWARE); } @@ -302,14 +300,16 @@ public class DefaultOtaPackageStateService implements OtaPackageStateService { private void updateAttributes(Device device, OtaPackageInfo otaPackage, long ts, TenantId tenantId, DeviceId deviceId, OtaPackageType otaPackageType) { List attributes = new ArrayList<>(); + List attrToRemove = new ArrayList<>(); attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(otaPackageType, TITLE), otaPackage.getTitle()))); attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(otaPackageType, VERSION), otaPackage.getVersion()))); if (StringUtils.isNotEmpty(otaPackage.getTag())) { attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(otaPackageType, TAG), otaPackage.getTag()))); + } else { + attrToRemove.add(getAttributeKey(otaPackageType, TAG)); } if (otaPackage.hasUrl()) { attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(otaPackageType, URL), otaPackage.getUrl()))); - List attrToRemove = new ArrayList<>(); if (otaPackage.getDataSize() == null) { attrToRemove.add(getAttributeKey(otaPackageType, SIZE)); @@ -328,15 +328,15 @@ public class DefaultOtaPackageStateService implements OtaPackageStateService { } else { attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(otaPackageType, CHECKSUM), otaPackage.getChecksum()))); } - - remove(device, otaPackageType, attrToRemove); } else { attributes.add(new BaseAttributeKvEntry(ts, new LongDataEntry(getAttributeKey(otaPackageType, SIZE), otaPackage.getDataSize()))); attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(otaPackageType, CHECKSUM_ALGORITHM), otaPackage.getChecksumAlgorithm().name()))); attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(otaPackageType, CHECKSUM), otaPackage.getChecksum()))); - remove(device, otaPackageType, Collections.singletonList(getAttributeKey(otaPackageType, URL))); + attrToRemove.add(getAttributeKey(otaPackageType, URL)); } + remove(device, otaPackageType, attrToRemove); + telemetryService.saveAndNotify(tenantId, deviceId, DataConstants.SHARED_SCOPE, attributes, new FutureCallback<>() { @Override public void onSuccess(@Nullable Void tmp) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index ad7a0340cf..3602aa804e 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -26,14 +26,15 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; @@ -47,6 +48,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.TbAlarmUpdateProto; import org.thingsboard.server.gen.transport.TransportProtos.TbAttributeDeleteProto; import org.thingsboard.server.gen.transport.TransportProtos.TbAttributeUpdateProto; import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionCloseProto; +import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesDeleteProto; import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesUpdateProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; @@ -64,7 +66,6 @@ import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.queue.processing.AbstractConsumerService; import org.thingsboard.server.service.queue.processing.IdMsgPair; -import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; import org.thingsboard.server.service.rpc.ToDeviceRpcRequestActorMsg; import org.thingsboard.server.service.state.DeviceStateService; @@ -467,6 +468,12 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService keys, TbCallback callback) { + onLocalTelemetrySubUpdate(entityId, + s -> { + if (TbSubscriptionType.TIMESERIES.equals(s.getType())) { + return (TbTimeseriesSubscription) s; + } else { + return null; + } + }, s -> true, s -> { + List subscriptionUpdate = null; + for (String key : keys) { + if (s.isAllKeys() || s.getKeyStates().containsKey(key)) { + if (subscriptionUpdate == null) { + subscriptionUpdate = new ArrayList<>(); + } + subscriptionUpdate.add(new BasicTsKvEntry(0, new StringDataEntry(key, null))); + } + } + return subscriptionUpdate; + }, false); + callback.onSuccess(); + } + private void onLocalTelemetrySubUpdate(EntityId entityId, Function castFunction, Predicate filterFunction, diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java index 37850b701f..67f2e10614 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java @@ -40,6 +40,8 @@ public interface SubscriptionManagerService extends ApplicationListener keys, TbCallback empty); + void onTimeSeriesDelete(TenantId tenantId, EntityId entityId, List keys, TbCallback callback); + void onAlarmUpdate(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback); void onAlarmDeleted(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java index 25b67d3a06..82e0b17f67 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.subscription; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -30,25 +31,25 @@ import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.KeyValueProto; import org.thingsboard.server.gen.transport.TransportProtos.KeyValueType; import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionMgrMsgProto; +import org.thingsboard.server.gen.transport.TransportProtos.TbAlarmDeleteProto; +import org.thingsboard.server.gen.transport.TransportProtos.TbAlarmUpdateProto; +import org.thingsboard.server.gen.transport.TransportProtos.TbAttributeDeleteProto; import org.thingsboard.server.gen.transport.TransportProtos.TbAttributeSubscriptionProto; import org.thingsboard.server.gen.transport.TransportProtos.TbAttributeUpdateProto; -import org.thingsboard.server.gen.transport.TransportProtos.TbAttributeDeleteProto; import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionCloseProto; import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionKetStateProto; import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionProto; import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionUpdateProto; import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionUpdateTsValue; +import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesDeleteProto; import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesSubscriptionProto; import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesUpdateProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; -import org.thingsboard.server.gen.transport.TransportProtos.TbAlarmUpdateProto; -import org.thingsboard.server.gen.transport.TransportProtos.TbAlarmDeleteProto; import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate; import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; @@ -207,6 +208,19 @@ public class TbSubscriptionUtils { return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); } + public static ToCoreMsg toTimeseriesDeleteProto(TenantId tenantId, EntityId entityId, List keys) { + TbTimeSeriesDeleteProto.Builder builder = TbTimeSeriesDeleteProto.newBuilder(); + builder.setEntityType(entityId.getEntityType().name()); + builder.setEntityIdMSB(entityId.getId().getMostSignificantBits()); + builder.setEntityIdLSB(entityId.getId().getLeastSignificantBits()); + builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); + builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); + builder.addAllKeys(keys); + SubscriptionMgrMsgProto.Builder msgBuilder = SubscriptionMgrMsgProto.newBuilder(); + msgBuilder.setTsDelete(builder); + return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); + } + public static ToCoreMsg toAttributesUpdateProto(TenantId tenantId, EntityId entityId, String scope, List attributes) { TbAttributeUpdateProto.Builder builder = TbAttributeUpdateProto.newBuilder(); builder.setEntityType(entityId.getEntityType().name()); diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java index bbd3811075..3512c37134 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java @@ -90,7 +90,7 @@ public abstract class AbstractSubscriptionService extends TbApplicationEventList Futures.addCallback(saveFuture, new FutureCallback() { @Override public void onSuccess(@Nullable T result) { - callback.accept(null); + callback.accept(result); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index 232f8bfd00..e9a6f45d00 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -33,10 +33,12 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -59,6 +61,7 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -279,7 +282,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer @Override public void deleteLatestInternal(TenantId tenantId, EntityId entityId, List keys, FutureCallback callback) { - ListenableFuture> deleteFuture = tsService.removeLatest(tenantId, entityId, keys); + ListenableFuture> deleteFuture = tsService.removeLatest(tenantId, entityId, keys); addVoidCallback(deleteFuture, callback); } @@ -299,6 +302,13 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer }, tsCallBackExecutor); } + @Override + public void deleteTimeseriesAndNotify(TenantId tenantId, EntityId entityId, List keys, List deleteTsKvQueries, FutureCallback callback) { + ListenableFuture> deleteFuture = tsService.remove(tenantId, entityId, deleteTsKvQueries); + addVoidCallback(deleteFuture, callback); + addWsCallback(deleteFuture, list -> onTimeSeriesDelete(tenantId, entityId, keys, list)); + } + @Override public void saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, long value, FutureCallback callback) { saveAndNotify(tenantId, entityId, scope, Collections.singletonList(new BaseAttributeKvEntry(new LongDataEntry(key, value) @@ -365,6 +375,34 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer } } + private void onTimeSeriesDelete(TenantId tenantId, EntityId entityId, List keys, List ts) { + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); + if (currentPartitions.contains(tpi)) { + if (subscriptionManagerService.isPresent()) { + List updated = new ArrayList<>(); + List deleted = new ArrayList<>(); + + ts.stream().filter(Objects::nonNull).forEach(res -> { + if (res.isRemoved()) { + if (res.getData() != null) { + updated.add(res.getData()); + } else { + deleted.add(res.getKey()); + } + } + }); + + subscriptionManagerService.get().onTimeSeriesUpdate(tenantId, entityId, updated, TbCallback.EMPTY); + subscriptionManagerService.get().onTimeSeriesDelete(tenantId, entityId, deleted, TbCallback.EMPTY); + } else { + log.warn("Possible misconfiguration because subscriptionManagerService is null!"); + } + } else { + TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toTimeseriesDeleteProto(tenantId, entityId, keys); + clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null); + } + } + private void addVoidCallback(ListenableFuture saveFuture, final FutureCallback callback) { Futures.addCallback(saveFuture, new FutureCallback() { @Override diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 3932ca28a9..c3ce2e3805 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -265,17 +265,17 @@ sql: batch_size: "${SQL_ATTRIBUTES_BATCH_SIZE:10000}" batch_max_delay: "${SQL_ATTRIBUTES_BATCH_MAX_DELAY_MS:100}" stats_print_interval_ms: "${SQL_ATTRIBUTES_BATCH_STATS_PRINT_MS:10000}" - batch_threads: "${SQL_ATTRIBUTES_BATCH_THREADS:4}" + batch_threads: "${SQL_ATTRIBUTES_BATCH_THREADS:3}" # batch thread count have to be a prime number like 3 or 5 to gain perfect hash distribution ts: batch_size: "${SQL_TS_BATCH_SIZE:10000}" batch_max_delay: "${SQL_TS_BATCH_MAX_DELAY_MS:100}" stats_print_interval_ms: "${SQL_TS_BATCH_STATS_PRINT_MS:10000}" - batch_threads: "${SQL_TS_BATCH_THREADS:4}" + batch_threads: "${SQL_TS_BATCH_THREADS:3}" # batch thread count have to be a prime number like 3 or 5 to gain perfect hash distribution ts_latest: batch_size: "${SQL_TS_LATEST_BATCH_SIZE:10000}" batch_max_delay: "${SQL_TS_LATEST_BATCH_MAX_DELAY_MS:100}" stats_print_interval_ms: "${SQL_TS_LATEST_BATCH_STATS_PRINT_MS:10000}" - batch_threads: "${SQL_TS_LATEST_BATCH_THREADS:4}" + batch_threads: "${SQL_TS_LATEST_BATCH_THREADS:3}" # batch thread count have to be a prime number like 3 or 5 to gain perfect hash distribution update_by_latest_ts: "${SQL_TS_UPDATE_BY_LATEST_TIMESTAMP:true}" # Specify whether to sort entities before batch update. Should be enabled for cluster mode to avoid deadlocks batch_sort: "${SQL_BATCH_SORT:false}" @@ -290,7 +290,7 @@ sql: timescale: # Specify Interval size for new data chunks storage. chunk_time_interval: "${SQL_TIMESCALE_CHUNK_TIME_INTERVAL:604800000}" - batch_threads: "${SQL_TIMESCALE_BATCH_THREADS:4}" + batch_threads: "${SQL_TIMESCALE_BATCH_THREADS:3}" # batch thread count have to be a prime number like 3 or 5 to gain perfect hash distribution ttl: ts: enabled: "${SQL_TTL_TS_ENABLED:true}" diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java index 9259f3253d..326d1465c1 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java @@ -48,15 +48,17 @@ import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Random; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.nullValue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; public abstract class BaseEdgeControllerTest extends AbstractControllerTest { + public static final String EDGE_HOST = "localhost"; + public static final int EDGE_PORT = 7070; + private IdComparator idComparator = new IdComparator<>(); private Tenant savedTenant; @@ -68,7 +70,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { loginSysAdmin(); Tenant tenant = new Tenant(); - tenant.setTitle("My tenant"); + tenant.setTitle("My tenant for Edge"); savedTenant = doPost("/api/tenant", tenant, Tenant.class); tenantId = savedTenant.getId(); Assert.assertNotNull(savedTenant); @@ -667,45 +669,45 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { @Test public void testSyncEdge() throws Exception { - Edge edge = doPost("/api/edge", constructEdge("Test Edge", "test"), Edge.class); + Edge edge = doPost("/api/edge", constructEdge("Test Sync Edge", "test"), Edge.class); Device device = new Device(); - device.setName("Edge Device 1"); + device.setName("Test Sync Edge Device 1"); device.setType("default"); Device savedDevice = doPost("/api/device", device, Device.class); doPost("/api/edge/" + edge.getId().getId().toString() + "/device/" + savedDevice.getId().getId().toString(), Device.class); Asset asset = new Asset(); - asset.setName("Edge Asset 1"); + asset.setName("Test Sync Edge Asset 1"); asset.setType("test"); Asset savedAsset = doPost("/api/asset", asset, Asset.class); doPost("/api/edge/" + edge.getId().getId().toString() + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); - EdgeImitator edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); + EdgeImitator edgeImitator = new EdgeImitator(EDGE_HOST, EDGE_PORT, edge.getRoutingKey(), edge.getSecret()); edgeImitator.ignoreType(UserCredentialsUpdateMsg.class); edgeImitator.expectMessageAmount(11); edgeImitator.connect(); - Assert.assertTrue(edgeImitator.waitForMessages()); + assertThat(edgeImitator.waitForMessages()).as("await for messages on first connect").isTrue(); - Assert.assertEquals(2, edgeImitator.findAllMessagesByType(RuleChainUpdateMsg.class).size()); // one msg during sync process, another from edge creation - Assert.assertEquals(1, edgeImitator.findAllMessagesByType(DeviceProfileUpdateMsg.class).size()); // one msg during sync process for 'default' device profile - Assert.assertEquals(1, edgeImitator.findAllMessagesByType(DeviceUpdateMsg.class).size()); // one msg once device assigned to edge - Assert.assertEquals(2, edgeImitator.findAllMessagesByType(AssetUpdateMsg.class).size()); // two msgs - one during sync process, and one more once asset assigned to edge - Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); // one msg during sync process for tenant admin user - Assert.assertEquals(4, edgeImitator.findAllMessagesByType(AdminSettingsUpdateMsg.class).size()); + assertThat(edgeImitator.findAllMessagesByType(RuleChainUpdateMsg.class)).as("one msg during sync process, another from edge creation").hasSize(2); + assertThat(edgeImitator.findAllMessagesByType(DeviceProfileUpdateMsg.class)).as("one msg during sync process for 'default' device profile").hasSize(1); + assertThat(edgeImitator.findAllMessagesByType(DeviceUpdateMsg.class)).as("one msg once device assigned to edge").hasSize(1); + assertThat(edgeImitator.findAllMessagesByType(AssetUpdateMsg.class)).as("two msgs - one during sync process, and one more once asset assigned to edge").hasSize(2); + assertThat(edgeImitator.findAllMessagesByType(UserUpdateMsg.class)).as("one msg during sync process for tenant admin user").hasSize(1); + assertThat(edgeImitator.findAllMessagesByType(AdminSettingsUpdateMsg.class)).as("admin setting update").hasSize(4); edgeImitator.expectMessageAmount(8); doPost("/api/edge/sync/" + edge.getId()); - Assert.assertTrue(edgeImitator.waitForMessages()); + assertThat(edgeImitator.waitForMessages()).as("await for messages after edge sync rest api call").isTrue(); - Assert.assertEquals(1, edgeImitator.findAllMessagesByType(RuleChainUpdateMsg.class).size()); - Assert.assertEquals(1, edgeImitator.findAllMessagesByType(DeviceProfileUpdateMsg.class).size()); - Assert.assertEquals(1, edgeImitator.findAllMessagesByType(AssetUpdateMsg.class).size()); - Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); - Assert.assertEquals(4, edgeImitator.findAllMessagesByType(AdminSettingsUpdateMsg.class).size()); + assertThat(edgeImitator.findAllMessagesByType(RuleChainUpdateMsg.class)).as("rule chain msg").hasSize(1); + assertThat(edgeImitator.findAllMessagesByType(DeviceProfileUpdateMsg.class)).as("device profile msg").hasSize(1); + assertThat(edgeImitator.findAllMessagesByType(AssetUpdateMsg.class)).as("asset update msg").hasSize(1); + assertThat(edgeImitator.findAllMessagesByType(UserUpdateMsg.class)).as("user update msg").hasSize(1); + assertThat(edgeImitator.findAllMessagesByType(AdminSettingsUpdateMsg.class)).as("admin setting update msg").hasSize(4); edgeImitator.allowIgnoredTypes(); try { @@ -720,4 +722,4 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } -} \ No newline at end of file +} diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java index 2dea0d2811..c412e86ec6 100644 --- a/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java @@ -767,7 +767,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { edgeImitator.expectMessageAmount(1); doDelete("/api/alarm/" + savedAlarm.getId().getId().toString()) .andExpect(status().isOk()); - Assert.assertTrue(edgeImitator.waitForMessages(1)); + Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof AlarmUpdateMsg); alarmUpdateMsg = (AlarmUpdateMsg) latestMessage; diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java index e257f5c5d2..46ee981be1 100644 --- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java +++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java @@ -65,6 +65,8 @@ import java.util.stream.Collectors; @Slf4j public class EdgeImitator { + public static final int TIMEOUT_IN_SECONDS = 30; + private String routingKey; private String routingSecret; @@ -293,7 +295,7 @@ public class EdgeImitator { } public boolean waitForMessages() throws InterruptedException { - return waitForMessages(5); + return waitForMessages(TIMEOUT_IN_SECONDS); } public boolean waitForMessages(int timeoutInSeconds) throws InterruptedException { @@ -308,7 +310,7 @@ public class EdgeImitator { } public boolean waitForResponses() throws InterruptedException { - return responsesLatch.await(5, TimeUnit.SECONDS); + return responsesLatch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS); } public void expectResponsesAmount(int messageAmount) { diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index dca92c1c7a..d576f38340 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -561,6 +561,15 @@ message TbAttributeDeleteProto { repeated string keys = 7; } +message TbTimeSeriesDeleteProto { + string entityType = 1; + int64 entityIdMSB = 2; + int64 entityIdLSB = 3; + int64 tenantIdMSB = 4; + int64 tenantIdLSB = 5; + repeated string keys = 6; +} + message TbTimeSeriesUpdateProto { string entityType = 1; int64 entityIdMSB = 2; @@ -614,6 +623,7 @@ message SubscriptionMgrMsgProto { TbAlarmSubscriptionProto alarmSub = 7; TbAlarmUpdateProto alarmUpdate = 8; TbAlarmDeleteProto alarmDelete = 9; + TbTimeSeriesDeleteProto tsDelete = 10; } message LocalSubscriptionServiceMsgProto { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java index 4418a826f3..d1ad11ceff 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import java.util.Collection; @@ -45,9 +46,9 @@ public interface TimeseriesService { ListenableFuture> saveLatest(TenantId tenantId, EntityId entityId, List tsKvEntry); - ListenableFuture> remove(TenantId tenantId, EntityId entityId, List queries); + ListenableFuture> remove(TenantId tenantId, EntityId entityId, List queries); - ListenableFuture> removeLatest(TenantId tenantId, EntityId entityId, Collection keys); + ListenableFuture> removeLatest(TenantId tenantId, EntityId entityId, Collection keys); ListenableFuture> removeAllLatest(TenantId tenantId, EntityId entityId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseDeleteTsKvQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseDeleteTsKvQuery.java index 6c9dfbd198..161883313d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseDeleteTsKvQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseDeleteTsKvQuery.java @@ -31,5 +31,4 @@ public class BaseDeleteTsKvQuery extends BaseTsKvQuery implements DeleteTsKvQuer this(key, startTs, endTs, false); } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvLatestRemovingResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvLatestRemovingResult.java new file mode 100644 index 0000000000..4c67dea2aa --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvLatestRemovingResult.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2021 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.common.data.kv; + +import lombok.Data; + +@Data +public class TsKvLatestRemovingResult { + private String key; + private TsKvEntry data; + private boolean removed; + + public TsKvLatestRemovingResult(TsKvEntry data) { + this.key = data.getKey(); + this.data = data; + this.removed = true; + } + + public TsKvLatestRemovingResult(String key, boolean removed) { + this.key = key; + this.removed = removed; + } +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java index 026f879c8d..f5a592e1c9 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java @@ -102,7 +102,7 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng @Override public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { - return new TbAwsSqsProducerTemplate<>(ruleEngineAdmin, sqsSettings, ruleEngineSettings.getTopic()); + return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, ruleEngineSettings.getTopic()); } @Override @@ -112,7 +112,7 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, coreSettings.getTopic()); + return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, coreSettings.getTopic()); } @Override @@ -182,13 +182,13 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng @Override public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { - return new TbAwsSqsConsumerTemplate<>(transportApiAdmin, sqsSettings, coreSettings.getUsageStatsTopic(), + return new TbAwsSqsConsumerTemplate<>(coreAdmin, sqsSettings, coreSettings.getUsageStatsTopic(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override public TbQueueConsumer> createToOtaPackageStateServiceMsgConsumer() { - return new TbAwsSqsConsumerTemplate<>(transportApiAdmin, sqsSettings, coreSettings.getOtaPackageTopic(), + return new TbAwsSqsConsumerTemplate<>(coreAdmin, sqsSettings, coreSettings.getOtaPackageTopic(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToOtaPackageStateServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java index eaa73bc108..87ebe42024 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java @@ -43,6 +43,7 @@ import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; +import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; import org.thingsboard.server.queue.sqs.TbAwsSqsAdmin; import org.thingsboard.server.queue.sqs.TbAwsSqsConsumerTemplate; import org.thingsboard.server.queue.sqs.TbAwsSqsProducerTemplate; @@ -63,6 +64,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { private final PartitionService partitionService; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; + private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -77,7 +79,8 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { PartitionService partitionService, TbServiceInfoProvider serviceInfoProvider, TbQueueRemoteJsInvokeSettings jsInvokeSettings, - TbAwsSqsQueueAttributes sqsQueueAttributes) { + TbAwsSqsQueueAttributes sqsQueueAttributes, + TbQueueTransportNotificationSettings transportNotificationSettings) { this.sqsSettings = sqsSettings; this.coreSettings = coreSettings; this.transportApiSettings = transportApiSettings; @@ -85,6 +88,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { this.partitionService = partitionService; this.serviceInfoProvider = serviceInfoProvider; this.jsInvokeSettings = jsInvokeSettings; + this.transportNotificationSettings = transportNotificationSettings; this.coreAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getCoreAttributes()); this.ruleEngineAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getRuleEngineAttributes()); @@ -95,7 +99,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTransportNotificationsMsgProducer() { - return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, coreSettings.getTopic()); + return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, transportNotificationSettings.getNotificationsTopic()); } @Override @@ -105,7 +109,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { - return new TbAwsSqsProducerTemplate<>(ruleEngineAdmin, sqsSettings, ruleEngineSettings.getTopic()); + return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, ruleEngineSettings.getTopic()); } @Override @@ -115,7 +119,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, coreSettings.getTopic()); + return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, coreSettings.getTopic()); } @Override @@ -139,7 +143,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTransportApiResponseProducer() { - return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, coreSettings.getTopic()); + return new TbAwsSqsProducerTemplate<>(transportApiAdmin, sqsSettings, transportApiSettings.getResponsesTopic()); } @Override @@ -172,13 +176,13 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { - return new TbAwsSqsConsumerTemplate<>(transportApiAdmin, sqsSettings, coreSettings.getUsageStatsTopic(), + return new TbAwsSqsConsumerTemplate<>(coreAdmin, sqsSettings, coreSettings.getUsageStatsTopic(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override public TbQueueConsumer> createToOtaPackageStateServiceMsgConsumer() { - return new TbAwsSqsConsumerTemplate<>(transportApiAdmin, sqsSettings, coreSettings.getOtaPackageTopic(), + return new TbAwsSqsConsumerTemplate<>(coreAdmin, sqsSettings, coreSettings.getOtaPackageTopic(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToOtaPackageStateServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java index 0e9f1812dd..05274625d3 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java @@ -37,6 +37,7 @@ import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; +import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration; import org.thingsboard.server.queue.sqs.TbAwsSqsAdmin; import org.thingsboard.server.queue.sqs.TbAwsSqsConsumerTemplate; @@ -57,6 +58,7 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory private final TbQueueRuleEngineSettings ruleEngineSettings; private final TbAwsSqsSettings sqsSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; + private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -68,13 +70,15 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory TbServiceInfoProvider serviceInfoProvider, TbAwsSqsSettings sqsSettings, TbAwsSqsQueueAttributes sqsQueueAttributes, - TbQueueRemoteJsInvokeSettings jsInvokeSettings) { + TbQueueRemoteJsInvokeSettings jsInvokeSettings, + TbQueueTransportNotificationSettings transportNotificationSettings) { this.partitionService = partitionService; this.coreSettings = coreSettings; this.serviceInfoProvider = serviceInfoProvider; this.ruleEngineSettings = ruleEngineSettings; this.sqsSettings = sqsSettings; this.jsInvokeSettings = jsInvokeSettings; + this.transportNotificationSettings = transportNotificationSettings; this.coreAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getCoreAttributes()); this.ruleEngineAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getRuleEngineAttributes()); @@ -84,17 +88,17 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory @Override public TbQueueProducer> createTransportNotificationsMsgProducer() { - return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, coreSettings.getTopic()); + return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, transportNotificationSettings.getNotificationsTopic()); } @Override public TbQueueProducer> createRuleEngineMsgProducer() { - return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, coreSettings.getTopic()); + return new TbAwsSqsProducerTemplate<>(ruleEngineAdmin, sqsSettings, ruleEngineSettings.getTopic()); } @Override public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { - return new TbAwsSqsProducerTemplate<>(ruleEngineAdmin, sqsSettings, ruleEngineSettings.getTopic()); + return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, ruleEngineSettings.getTopic()); } @Override @@ -104,7 +108,7 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, coreSettings.getTopic()); + return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, coreSettings.getTopic()); } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java index 689f2c041e..80f9e27d1c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java @@ -32,6 +32,7 @@ import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; import org.thingsboard.server.queue.sqs.TbAwsSqsAdmin; @@ -51,34 +52,39 @@ public class AwsSqsTransportQueueFactory implements TbTransportQueueFactory { private final TbAwsSqsSettings sqsSettings; private final TbQueueCoreSettings coreSettings; private final TbServiceInfoProvider serviceInfoProvider; + private final TbQueueRuleEngineSettings ruleEngineSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin transportApiAdmin; private final TbQueueAdmin notificationAdmin; + private final TbQueueAdmin ruleEngineAdmin; public AwsSqsTransportQueueFactory(TbQueueTransportApiSettings transportApiSettings, TbQueueTransportNotificationSettings transportNotificationSettings, TbAwsSqsSettings sqsSettings, TbServiceInfoProvider serviceInfoProvider, TbQueueCoreSettings coreSettings, - TbAwsSqsQueueAttributes sqsQueueAttributes) { + TbAwsSqsQueueAttributes sqsQueueAttributes, + TbQueueRuleEngineSettings ruleEngineSettings) { this.transportApiSettings = transportApiSettings; this.transportNotificationSettings = transportNotificationSettings; this.sqsSettings = sqsSettings; this.serviceInfoProvider = serviceInfoProvider; this.coreSettings = coreSettings; + this.ruleEngineSettings = ruleEngineSettings; this.coreAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getCoreAttributes()); this.transportApiAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getTransportApiAttributes()); this.notificationAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getNotificationsAttributes()); + this.ruleEngineAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getRuleEngineAttributes()); } @Override public TbQueueRequestTemplate, TbProtoQueueMsg> createTransportApiRequestTemplate() { - TbAwsSqsProducerTemplate> producerTemplate = + TbQueueProducer> producerTemplate = new TbAwsSqsProducerTemplate<>(transportApiAdmin, sqsSettings, transportApiSettings.getRequestsTopic()); - TbAwsSqsConsumerTemplate> consumerTemplate = + TbQueueConsumer> consumerTemplate = new TbAwsSqsConsumerTemplate<>(transportApiAdmin, sqsSettings, transportApiSettings.getResponsesTopic() + "_" + serviceInfoProvider.getServiceId(), msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportApiResponseMsg.parseFrom(msg.getData()), msg.getHeaders())); @@ -96,7 +102,7 @@ public class AwsSqsTransportQueueFactory implements TbTransportQueueFactory { @Override public TbQueueProducer> createRuleEngineMsgProducer() { - return new TbAwsSqsProducerTemplate<>(transportApiAdmin, sqsSettings, transportApiSettings.getRequestsTopic()); + return new TbAwsSqsProducerTemplate<>(ruleEngineAdmin, sqsSettings, ruleEngineSettings.getTopic()); } @Override @@ -126,5 +132,8 @@ public class AwsSqsTransportQueueFactory implements TbTransportQueueFactory { if (notificationAdmin != null) { notificationAdmin.destroy(); } + if (ruleEngineAdmin != null) { + ruleEngineAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index e08be485ea..b4dd38217d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -131,7 +131,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi requestBuilder.settings(kafkaSettings); requestBuilder.clientId("monolith-rule-engine-notifications-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(ruleEngineSettings.getTopic()); - requestBuilder.admin(ruleEngineAdmin); + requestBuilder.admin(notificationAdmin); return requestBuilder.build(); } @@ -151,7 +151,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi requestBuilder.settings(kafkaSettings); requestBuilder.clientId("monolith-core-notifications-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(coreSettings.getTopic()); - requestBuilder.admin(coreAdmin); + requestBuilder.admin(notificationAdmin); return requestBuilder.build(); } @@ -328,5 +328,8 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi if (notificationAdmin != null) { notificationAdmin.destroy(); } + if (fwUpdatesAdmin != null) { + fwUpdatesAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index bec01c7201..f560fcc5f4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -49,6 +49,7 @@ import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; +import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; import javax.annotation.PreDestroy; import java.nio.charset.StandardCharsets; @@ -65,6 +66,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueTransportApiSettings transportApiSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbKafkaConsumerStatsService consumerStatsService; + private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -80,6 +82,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { TbQueueTransportApiSettings transportApiSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbKafkaConsumerStatsService consumerStatsService, + TbQueueTransportNotificationSettings transportNotificationSettings, TbKafkaTopicConfigs kafkaTopicConfigs) { this.partitionService = partitionService; this.kafkaSettings = kafkaSettings; @@ -89,6 +92,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { this.transportApiSettings = transportApiSettings; this.jsInvokeSettings = jsInvokeSettings; this.consumerStatsService = consumerStatsService; + this.transportNotificationSettings = transportNotificationSettings; this.coreAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getRuleEngineConfigs()); @@ -103,8 +107,8 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("tb-core-transport-notifications-" + serviceInfoProvider.getServiceId()); - requestBuilder.defaultTopic(coreSettings.getTopic()); - requestBuilder.admin(coreAdmin); + requestBuilder.defaultTopic(transportNotificationSettings.getNotificationsTopic()); + requestBuilder.admin(notificationAdmin); return requestBuilder.build(); } @@ -124,7 +128,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { requestBuilder.settings(kafkaSettings); requestBuilder.clientId("tb-core-rule-engine-notifications-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(ruleEngineSettings.getTopic()); - requestBuilder.admin(ruleEngineAdmin); + requestBuilder.admin(notificationAdmin); return requestBuilder.build(); } @@ -144,7 +148,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { requestBuilder.settings(kafkaSettings); requestBuilder.clientId("tb-core-to-core-notifications-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(coreSettings.getTopic()); - requestBuilder.admin(coreAdmin); + requestBuilder.admin(notificationAdmin); return requestBuilder.build(); } @@ -192,8 +196,8 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("tb-core-transport-api-producer-" + serviceInfoProvider.getServiceId()); - requestBuilder.defaultTopic(coreSettings.getTopic()); - requestBuilder.admin(coreAdmin); + requestBuilder.defaultTopic(transportApiSettings.getResponsesTopic()); + requestBuilder.admin(transportApiAdmin); return requestBuilder.build(); } @@ -294,5 +298,8 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { if (notificationAdmin != null) { notificationAdmin.destroy(); } + if (fwUpdatesAdmin != null) { + fwUpdatesAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index 58bc38d3a9..dc3db14ec2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -46,6 +46,7 @@ import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; +import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration; import javax.annotation.PreDestroy; @@ -63,6 +64,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { private final TbQueueRuleEngineSettings ruleEngineSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbKafkaConsumerStatsService consumerStatsService; + private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -77,6 +79,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { TbQueueRuleEngineSettings ruleEngineSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbKafkaConsumerStatsService consumerStatsService, + TbQueueTransportNotificationSettings transportNotificationSettings, TbKafkaTopicConfigs kafkaTopicConfigs) { this.partitionService = partitionService; this.kafkaSettings = kafkaSettings; @@ -85,6 +88,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { this.ruleEngineSettings = ruleEngineSettings; this.jsInvokeSettings = jsInvokeSettings; this.consumerStatsService = consumerStatsService; + this.transportNotificationSettings = transportNotificationSettings; this.coreAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getRuleEngineConfigs()); @@ -98,8 +102,8 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("tb-rule-engine-transport-notifications-" + serviceInfoProvider.getServiceId()); - requestBuilder.defaultTopic(coreSettings.getTopic()); - requestBuilder.admin(coreAdmin); + requestBuilder.defaultTopic(transportNotificationSettings.getNotificationsTopic()); + requestBuilder.admin(notificationAdmin); return requestBuilder.build(); } @@ -108,8 +112,8 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("tb-rule-engine-to-rule-engine-" + serviceInfoProvider.getServiceId()); - requestBuilder.defaultTopic(coreSettings.getTopic()); - requestBuilder.admin(coreAdmin); + requestBuilder.defaultTopic(ruleEngineSettings.getTopic()); + requestBuilder.admin(ruleEngineAdmin); return requestBuilder.build(); } @@ -119,11 +123,10 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { requestBuilder.settings(kafkaSettings); requestBuilder.clientId("tb-rule-engine-to-rule-engine-notifications-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(ruleEngineSettings.getTopic()); - requestBuilder.admin(ruleEngineAdmin); + requestBuilder.admin(notificationAdmin); return requestBuilder.build(); } - @Override public TbQueueProducer> createTbCoreMsgProducer() { TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); @@ -150,7 +153,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { requestBuilder.settings(kafkaSettings); requestBuilder.clientId("tb-rule-engine-to-core-notifications-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(coreSettings.getTopic()); - requestBuilder.admin(coreAdmin); + requestBuilder.admin(notificationAdmin); return requestBuilder.build(); } @@ -239,5 +242,8 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { if (notificationAdmin != null) { notificationAdmin.destroy(); } + if (fwUpdatesAdmin != null) { + fwUpdatesAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java index 1d2f36c5c2..d22839b9be 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java @@ -112,7 +112,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng @Override public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { - return new TbPubSubProducerTemplate<>(ruleEngineAdmin, pubSubSettings, ruleEngineSettings.getTopic()); + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, ruleEngineSettings.getTopic()); } @Override @@ -122,7 +122,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getTopic()); + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, coreSettings.getTopic()); } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java index 65ea183d66..086c4d9420 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java @@ -46,7 +46,9 @@ import org.thingsboard.server.queue.pubsub.TbPubSubSettings; import org.thingsboard.server.queue.pubsub.TbPubSubSubscriptionSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; +import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; +import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; import javax.annotation.PreDestroy; import java.nio.charset.StandardCharsets; @@ -61,11 +63,14 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { private final PartitionService partitionService; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; + private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbQueueRuleEngineSettings ruleEngineSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin jsExecutorAdmin; private final TbQueueAdmin transportApiAdmin; private final TbQueueAdmin notificationAdmin; + private final TbQueueAdmin ruleEngineAdmin; public PubSubTbCoreQueueFactory(TbPubSubSettings pubSubSettings, TbQueueCoreSettings coreSettings, @@ -73,6 +78,8 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { PartitionService partitionService, TbServiceInfoProvider serviceInfoProvider, TbQueueRemoteJsInvokeSettings jsInvokeSettings, + TbQueueTransportNotificationSettings transportNotificationSettings, + TbQueueRuleEngineSettings ruleEngineSettings, TbPubSubSubscriptionSettings pubSubSubscriptionSettings) { this.pubSubSettings = pubSubSettings; this.coreSettings = coreSettings; @@ -80,16 +87,19 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { this.partitionService = partitionService; this.serviceInfoProvider = serviceInfoProvider; this.jsInvokeSettings = jsInvokeSettings; + this.transportNotificationSettings = transportNotificationSettings; + this.ruleEngineSettings = ruleEngineSettings; this.coreAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getCoreSettings()); this.jsExecutorAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getJsExecutorSettings()); this.transportApiAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getTransportApiSettings()); this.notificationAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getNotificationsSettings()); + this.ruleEngineAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getRuleEngineSettings()); } @Override public TbQueueProducer> createTransportNotificationsMsgProducer() { - return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getTopic()); + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, transportNotificationSettings.getNotificationsTopic()); } @Override @@ -99,7 +109,7 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { - return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getTopic()); + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, ruleEngineSettings.getTopic()); } @Override @@ -109,7 +119,7 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getTopic()); + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, coreSettings.getTopic()); } @Override @@ -133,7 +143,7 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTransportApiResponseProducer() { - return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getTopic()); + return new TbPubSubProducerTemplate<>(transportApiAdmin, pubSubSettings, transportApiSettings.getResponsesTopic()); } @Override @@ -195,5 +205,8 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { if (notificationAdmin != null) { notificationAdmin.destroy(); } + if (ruleEngineAdmin != null) { + ruleEngineAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java index b02322496f..f8f190853d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java @@ -45,6 +45,7 @@ import org.thingsboard.server.queue.pubsub.TbPubSubSubscriptionSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; +import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration; import javax.annotation.PreDestroy; @@ -60,6 +61,7 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory private final PartitionService partitionService; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; + private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -72,6 +74,7 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory PartitionService partitionService, TbServiceInfoProvider serviceInfoProvider, TbQueueRemoteJsInvokeSettings jsInvokeSettings, + TbQueueTransportNotificationSettings transportNotificationSettings, TbPubSubSubscriptionSettings pubSubSubscriptionSettings) { this.pubSubSettings = pubSubSettings; this.coreSettings = coreSettings; @@ -79,6 +82,7 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory this.partitionService = partitionService; this.serviceInfoProvider = serviceInfoProvider; this.jsInvokeSettings = jsInvokeSettings; + this.transportNotificationSettings = transportNotificationSettings; this.coreAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getCoreSettings()); this.ruleEngineAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getRuleEngineSettings()); @@ -88,28 +92,27 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory @Override public TbQueueProducer> createTransportNotificationsMsgProducer() { - return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getTopic()); + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, transportNotificationSettings.getNotificationsTopic()); } @Override public TbQueueProducer> createRuleEngineMsgProducer() { - return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getTopic()); + return new TbPubSubProducerTemplate<>(ruleEngineAdmin, pubSubSettings, ruleEngineSettings.getTopic()); } @Override public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { - return new TbPubSubProducerTemplate<>(ruleEngineAdmin, pubSubSettings, ruleEngineSettings.getTopic()); + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, ruleEngineSettings.getTopic()); } @Override public TbQueueProducer> createTbCoreMsgProducer() { return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getTopic()); - } @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getTopic()); + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, coreSettings.getTopic()); } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java index 7d48803258..e8ec6e4cff 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java @@ -66,13 +66,13 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE private final TbQueueTransportApiSettings transportApiSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbRabbitMqSettings rabbitMqSettings; + private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; private final TbQueueAdmin jsExecutorAdmin; private final TbQueueAdmin transportApiAdmin; private final TbQueueAdmin notificationAdmin; - private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; public RabbitMqMonolithQueueFactory(PartitionService partitionService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, @@ -89,13 +89,13 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE this.transportApiSettings = transportApiSettings; this.transportNotificationSettings = transportNotificationSettings; this.rabbitMqSettings = rabbitMqSettings; + this.jsInvokeSettings = jsInvokeSettings; this.coreAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getCoreArgs()); this.ruleEngineAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getRuleEngineArgs()); this.jsExecutorAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getJsExecutorArgs()); this.transportApiAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getTransportApiArgs()); this.notificationAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getNotificationsArgs()); - this.jsInvokeSettings = jsInvokeSettings; } @Override @@ -110,7 +110,7 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE @Override public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { - return new TbRabbitMqProducerTemplate<>(ruleEngineAdmin, rabbitMqSettings, ruleEngineSettings.getTopic()); + return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, ruleEngineSettings.getTopic()); } @Override @@ -120,7 +120,7 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, coreSettings.getTopic()); + return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, coreSettings.getTopic()); } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java index c0abeab162..303180bad6 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java @@ -48,6 +48,7 @@ import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; +import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; import javax.annotation.PreDestroy; import java.nio.charset.StandardCharsets; @@ -63,6 +64,7 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { private final PartitionService partitionService; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; + private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -77,6 +79,7 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { PartitionService partitionService, TbServiceInfoProvider serviceInfoProvider, TbQueueRemoteJsInvokeSettings jsInvokeSettings, + TbQueueTransportNotificationSettings transportNotificationSettings, TbRabbitMqQueueArguments queueArguments) { this.rabbitMqSettings = rabbitMqSettings; this.coreSettings = coreSettings; @@ -85,6 +88,7 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { this.partitionService = partitionService; this.serviceInfoProvider = serviceInfoProvider; this.jsInvokeSettings = jsInvokeSettings; + this.transportNotificationSettings = transportNotificationSettings; this.coreAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getCoreArgs()); this.ruleEngineAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getRuleEngineArgs()); @@ -95,7 +99,7 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTransportNotificationsMsgProducer() { - return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, coreSettings.getTopic()); + return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, transportNotificationSettings.getNotificationsTopic()); } @Override @@ -105,7 +109,7 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { - return new TbRabbitMqProducerTemplate<>(ruleEngineAdmin, rabbitMqSettings, ruleEngineSettings.getTopic()); + return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, ruleEngineSettings.getTopic()); } @Override @@ -115,7 +119,7 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, coreSettings.getTopic()); + return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, coreSettings.getTopic()); } @Override @@ -139,7 +143,7 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTransportApiResponseProducer() { - return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, coreSettings.getTopic()); + return new TbRabbitMqProducerTemplate<>(transportApiAdmin, rabbitMqSettings, transportApiSettings.getResponsesTopic()); } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java index 2f8a3ed655..470e4a8785 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java @@ -45,6 +45,7 @@ import org.thingsboard.server.queue.rabbitmq.TbRabbitMqSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; +import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration; import javax.annotation.PreDestroy; @@ -60,6 +61,7 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor private final TbQueueRuleEngineSettings ruleEngineSettings; private final TbRabbitMqSettings rabbitMqSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; + private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -71,6 +73,7 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor TbServiceInfoProvider serviceInfoProvider, TbRabbitMqSettings rabbitMqSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, + TbQueueTransportNotificationSettings transportNotificationSettings, TbRabbitMqQueueArguments queueArguments) { this.partitionService = partitionService; this.coreSettings = coreSettings; @@ -78,6 +81,7 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor this.ruleEngineSettings = ruleEngineSettings; this.rabbitMqSettings = rabbitMqSettings; this.jsInvokeSettings = jsInvokeSettings; + this.transportNotificationSettings = transportNotificationSettings; this.coreAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getCoreArgs()); this.ruleEngineAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getRuleEngineArgs()); @@ -87,17 +91,17 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor @Override public TbQueueProducer> createTransportNotificationsMsgProducer() { - return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, coreSettings.getTopic()); + return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, transportNotificationSettings.getNotificationsTopic()); } @Override public TbQueueProducer> createRuleEngineMsgProducer() { - return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, coreSettings.getTopic()); + return new TbRabbitMqProducerTemplate<>(ruleEngineAdmin, rabbitMqSettings, ruleEngineSettings.getTopic()); } @Override public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { - return new TbRabbitMqProducerTemplate<>(ruleEngineAdmin, rabbitMqSettings, ruleEngineSettings.getTopic()); + return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, ruleEngineSettings.getTopic()); } @Override @@ -107,7 +111,7 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, coreSettings.getTopic()); + return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, coreSettings.getTopic()); } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java index ec27a3ed04..e40816b242 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java @@ -37,6 +37,7 @@ import org.thingsboard.server.queue.rabbitmq.TbRabbitMqProducerTemplate; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqQueueArguments; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; @@ -51,6 +52,7 @@ public class RabbitMqTransportQueueFactory implements TbTransportQueueFactory { private final TbRabbitMqSettings rabbitMqSettings; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueCoreSettings coreSettings; + private final TbQueueRuleEngineSettings ruleEngineSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -62,12 +64,14 @@ public class RabbitMqTransportQueueFactory implements TbTransportQueueFactory { TbRabbitMqSettings rabbitMqSettings, TbServiceInfoProvider serviceInfoProvider, TbQueueCoreSettings coreSettings, + TbQueueRuleEngineSettings ruleEngineSettings, TbRabbitMqQueueArguments queueArguments) { this.transportApiSettings = transportApiSettings; this.transportNotificationSettings = transportNotificationSettings; this.rabbitMqSettings = rabbitMqSettings; this.serviceInfoProvider = serviceInfoProvider; this.coreSettings = coreSettings; + this.ruleEngineSettings = ruleEngineSettings; this.coreAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getCoreArgs()); this.ruleEngineAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getRuleEngineArgs()); @@ -77,10 +81,10 @@ public class RabbitMqTransportQueueFactory implements TbTransportQueueFactory { @Override public TbQueueRequestTemplate, TbProtoQueueMsg> createTransportApiRequestTemplate() { - TbRabbitMqProducerTemplate> producerTemplate = + TbQueueProducer> producerTemplate = new TbRabbitMqProducerTemplate<>(transportApiAdmin, rabbitMqSettings, transportApiSettings.getRequestsTopic()); - TbRabbitMqConsumerTemplate> consumerTemplate = + TbQueueConsumer> consumerTemplate = new TbRabbitMqConsumerTemplate<>(transportApiAdmin, rabbitMqSettings, transportApiSettings.getResponsesTopic() + "." + serviceInfoProvider.getServiceId(), msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportApiResponseMsg.parseFrom(msg.getData()), msg.getHeaders())); @@ -98,7 +102,7 @@ public class RabbitMqTransportQueueFactory implements TbTransportQueueFactory { @Override public TbQueueProducer> createRuleEngineMsgProducer() { - return new TbRabbitMqProducerTemplate<>(transportApiAdmin, rabbitMqSettings, transportApiSettings.getRequestsTopic()); + return new TbRabbitMqProducerTemplate<>(ruleEngineAdmin, rabbitMqSettings, ruleEngineSettings.getTopic()); } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java index 3731a56aee..39f41e8faf 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java @@ -109,7 +109,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul @Override public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { - return new TbServiceBusProducerTemplate<>(ruleEngineAdmin, serviceBusSettings, ruleEngineSettings.getTopic()); + return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, ruleEngineSettings.getTopic()); } @Override @@ -119,7 +119,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, coreSettings.getTopic()); + return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, coreSettings.getTopic()); } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java index 7a9da236ed..0280012008 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java @@ -48,6 +48,7 @@ import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; +import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; import javax.annotation.PreDestroy; import java.nio.charset.StandardCharsets; @@ -63,6 +64,7 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { private final PartitionService partitionService; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; + private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -77,6 +79,7 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { PartitionService partitionService, TbServiceInfoProvider serviceInfoProvider, TbQueueRemoteJsInvokeSettings jsInvokeSettings, + TbQueueTransportNotificationSettings transportNotificationSettings, TbServiceBusQueueConfigs serviceBusQueueConfigs) { this.serviceBusSettings = serviceBusSettings; this.coreSettings = coreSettings; @@ -85,6 +88,7 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { this.partitionService = partitionService; this.serviceInfoProvider = serviceInfoProvider; this.jsInvokeSettings = jsInvokeSettings; + this.transportNotificationSettings = transportNotificationSettings; this.coreAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getRuleEngineConfigs()); @@ -95,7 +99,7 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTransportNotificationsMsgProducer() { - return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, coreSettings.getTopic()); + return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, transportNotificationSettings.getNotificationsTopic()); } @Override @@ -105,7 +109,7 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { - return new TbServiceBusProducerTemplate<>(ruleEngineAdmin, serviceBusSettings, ruleEngineSettings.getTopic()); + return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, ruleEngineSettings.getTopic()); } @Override @@ -115,7 +119,7 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, coreSettings.getTopic()); + return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, coreSettings.getTopic()); } @Override @@ -139,7 +143,7 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTransportApiResponseProducer() { - return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, coreSettings.getTopic()); + return new TbServiceBusProducerTemplate<>(transportApiAdmin, serviceBusSettings, transportApiSettings.getResponsesTopic()); } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java index be11736ad2..d653c8aa17 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java @@ -45,6 +45,7 @@ import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; +import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration; import javax.annotation.PreDestroy; @@ -60,6 +61,7 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact private final TbQueueRuleEngineSettings ruleEngineSettings; private final TbServiceBusSettings serviceBusSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; + private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -71,6 +73,7 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact TbServiceInfoProvider serviceInfoProvider, TbServiceBusSettings serviceBusSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, + TbQueueTransportNotificationSettings transportNotificationSettings, TbServiceBusQueueConfigs serviceBusQueueConfigs) { this.partitionService = partitionService; this.coreSettings = coreSettings; @@ -78,6 +81,7 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact this.ruleEngineSettings = ruleEngineSettings; this.serviceBusSettings = serviceBusSettings; this.jsInvokeSettings = jsInvokeSettings; + this.transportNotificationSettings = transportNotificationSettings; this.coreAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getRuleEngineConfigs()); @@ -87,17 +91,17 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact @Override public TbQueueProducer> createTransportNotificationsMsgProducer() { - return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, coreSettings.getTopic()); + return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, transportNotificationSettings.getNotificationsTopic()); } @Override public TbQueueProducer> createRuleEngineMsgProducer() { - return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, coreSettings.getTopic()); + return new TbServiceBusProducerTemplate<>(ruleEngineAdmin, serviceBusSettings, ruleEngineSettings.getTopic()); } @Override public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { - return new TbServiceBusProducerTemplate<>(ruleEngineAdmin, serviceBusSettings, ruleEngineSettings.getTopic()); + return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, ruleEngineSettings.getTopic()); } @Override @@ -107,7 +111,7 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, coreSettings.getTopic()); + return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, coreSettings.getTopic()); } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java index 3befd52729..85ecdd2643 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java @@ -37,6 +37,7 @@ import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; @@ -51,14 +52,17 @@ public class ServiceBusTransportQueueFactory implements TbTransportQueueFactory private final TbServiceBusSettings serviceBusSettings; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueCoreSettings coreSettings; + private final TbQueueRuleEngineSettings ruleEngineSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin transportApiAdmin; private final TbQueueAdmin notificationAdmin; + private final TbQueueAdmin ruleEngineAdmin; public ServiceBusTransportQueueFactory(TbQueueTransportApiSettings transportApiSettings, TbQueueTransportNotificationSettings transportNotificationSettings, TbServiceBusSettings serviceBusSettings, + TbQueueRuleEngineSettings ruleEngineSettings, TbServiceInfoProvider serviceInfoProvider, TbQueueCoreSettings coreSettings, TbServiceBusQueueConfigs serviceBusQueueConfigs) { @@ -67,10 +71,12 @@ public class ServiceBusTransportQueueFactory implements TbTransportQueueFactory this.serviceBusSettings = serviceBusSettings; this.serviceInfoProvider = serviceInfoProvider; this.coreSettings = coreSettings; + this.ruleEngineSettings = ruleEngineSettings; this.coreAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getCoreConfigs()); this.transportApiAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getTransportApiConfigs()); this.notificationAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getNotificationsConfigs()); + this.ruleEngineAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getRuleEngineConfigs()); } @Override @@ -96,7 +102,7 @@ public class ServiceBusTransportQueueFactory implements TbTransportQueueFactory @Override public TbQueueProducer> createRuleEngineMsgProducer() { - return new TbServiceBusProducerTemplate<>(transportApiAdmin, serviceBusSettings, transportApiSettings.getRequestsTopic()); + return new TbServiceBusProducerTemplate<>(ruleEngineAdmin, serviceBusSettings, ruleEngineSettings.getTopic()); } @Override @@ -127,5 +133,8 @@ public class ServiceBusTransportQueueFactory implements TbTransportQueueFactory if (notificationAdmin != null) { notificationAdmin.destroy(); } + if (ruleEngineAdmin != null) { + ruleEngineAdmin.destroy(); + } } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java index e2b05847f7..246444ddaa 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -41,6 +41,7 @@ import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MSecurityInfo; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContext; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil; import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider; +import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfigService; import org.thingsboard.server.transport.lwm2m.server.ota.LwM2MOtaUpdateService; import org.thingsboard.server.transport.lwm2m.server.session.LwM2MSessionManager; import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MClientStore; @@ -76,6 +77,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { private final LwM2MSessionManager sessionManager; private final TransportDeviceProfileCache deviceProfileCache; private final LwM2mVersionedModelProvider modelProvider; + private final LwM2MModelConfigService modelConfigService; @Autowired @Lazy @@ -259,6 +261,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { // TODO: change tests to use new certificate. // this.securityStore.remove(client.getEndpoint(), registration.getId()); clientStore.remove(client.getEndpoint()); + modelConfigService.removeUpdates(client.getEndpoint()); UUID profileId = client.getProfileId(); if (profileId != null) { Optional otherClients = lwM2mClientsByRegistrationId.values().stream().filter(e -> e.getProfileId().equals(profileId)).findFirst(); @@ -333,6 +336,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { if (LwM2MClientState.REGISTERED.equals(lwM2MClient.getState())) { PowerMode powerMode = getPowerMode(lwM2MClient); if (PowerMode.PSM.equals(powerMode) || PowerMode.E_DRX.equals(powerMode)) { + modelConfigService.sendUpdates(lwM2MClient); defaultLwM2MUplinkMsgHandler.initAttributes(lwM2MClient); TransportProtos.TransportToDeviceActorMsg persistentRpcRequestMsg = TransportProtos.TransportToDeviceActorMsg .newBuilder() diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DownlinkRequestCallback.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DownlinkRequestCallback.java index 8912c51960..40cf896f55 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DownlinkRequestCallback.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DownlinkRequestCallback.java @@ -17,9 +17,9 @@ package org.thingsboard.server.transport.lwm2m.server.downlink; public interface DownlinkRequestCallback { - default boolean onSent(R request){ + default boolean onSent(R request) { return true; - }; + } void onSuccess(R request, T response); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfig.java new file mode 100644 index 0000000000..5c0acc150c --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfig.java @@ -0,0 +1,92 @@ +/** + * Copyright © 2016-2021 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.transport.lwm2m.server.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.device.data.lwm2m.ObjectAttributes; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import static org.thingsboard.common.util.CollectionsUtil.diffSets; + +@Data +@NoArgsConstructor +@ToString(exclude = "toCancelRead") +@Slf4j +public class LwM2MModelConfig { + private String endpoint; + private Map attributesToAdd; + private Set attributesToRemove; + private Set toObserve; + private Set toCancelObserve; + private Set toRead; + @JsonIgnore + private Set toCancelRead; + + public LwM2MModelConfig(String endpoint) { + this.endpoint = endpoint; + this.attributesToAdd = new ConcurrentHashMap<>(); + this.attributesToRemove = ConcurrentHashMap.newKeySet(); + this.toObserve = ConcurrentHashMap.newKeySet(); + this.toCancelObserve = ConcurrentHashMap.newKeySet(); + this.toRead = ConcurrentHashMap.newKeySet(); + this.toCancelRead = new HashSet<>(); + } + + public void merge(LwM2MModelConfig modelConfig) { + if (modelConfig.isEmpty() && modelConfig.getToCancelRead().isEmpty()) { + return; + } + + modelConfig.getAttributesToAdd().forEach((k, v) -> { + if (this.attributesToRemove.contains(k)) { + this.attributesToRemove.remove(k); + } else { + this.attributesToAdd.put(k, v); + } + }); + + modelConfig.getAttributesToRemove().forEach(k -> { + if (this.attributesToAdd.containsKey(k)) { + this.attributesToRemove.remove(k); + } else { + this.attributesToRemove.add(k); + } + }); + + this.toObserve.addAll(diffSets(this.toCancelObserve, modelConfig.getToObserve())); + this.toCancelObserve.addAll(diffSets(this.toObserve, modelConfig.getToCancelObserve())); + + this.toObserve.removeAll(modelConfig.getToCancelObserve()); + this.toCancelObserve.removeAll(modelConfig.getToObserve()); + + this.toRead.removeAll(modelConfig.getToObserve()); + this.toRead.removeAll(modelConfig.getToCancelRead()); + this.toRead.addAll(modelConfig.getToRead()); + } + + @JsonIgnore + public boolean isEmpty() { + return attributesToAdd.isEmpty() && toObserve.isEmpty() && toCancelObserve.isEmpty() && toRead.isEmpty(); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigService.java new file mode 100644 index 0000000000..9670b7791b --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigService.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2021 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.transport.lwm2m.server.model; + +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; + +public interface LwM2MModelConfigService { + + void sendUpdates(LwM2mClient lwM2mClient); + + void sendUpdates(LwM2mClient lwM2mClient, LwM2MModelConfig modelConfig); + + void persistUpdates(String endpoint); + + void removeUpdates(String endpoint); +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImpl.java new file mode 100644 index 0000000000..9f0a84cbce --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImpl.java @@ -0,0 +1,235 @@ +/** + * Copyright © 2016-2021 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.transport.lwm2m.server.model; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.device.data.lwm2m.ObjectAttributes; +import org.thingsboard.server.queue.util.AfterStartUp; +import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; +import org.thingsboard.server.transport.lwm2m.server.downlink.DownlinkRequestCallback; +import org.thingsboard.server.transport.lwm2m.server.downlink.LwM2mDownlinkMsgHandler; +import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MCancelObserveCallback; +import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MCancelObserveRequest; +import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MObserveCallback; +import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MObserveRequest; +import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MReadCallback; +import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MReadRequest; +import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttributesCallback; +import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttributesRequest; +import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService; +import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MModelConfigStore; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; + +import javax.annotation.PreDestroy; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +@Slf4j +@Service +@TbLwM2mTransportComponent +public class LwM2MModelConfigServiceImpl implements LwM2MModelConfigService { + + @Autowired + private TbLwM2MModelConfigStore modelStore; + + @Autowired + @Lazy + private LwM2mDownlinkMsgHandler downlinkMsgHandler; + @Autowired + @Lazy + private LwM2mUplinkMsgHandler uplinkMsgHandler; + @Autowired + @Lazy + private LwM2mClientContext clientContext; + + @Autowired + private LwM2MTelemetryLogService logService; + + private ConcurrentMap currentModelConfigs; + + @AfterStartUp(order = Integer.MAX_VALUE - 1) + private void init() { + List models = modelStore.getAll(); + log.debug("Fetched model configs: {}", models); + currentModelConfigs = models.stream() + .collect(Collectors.toConcurrentMap(LwM2MModelConfig::getEndpoint, m -> m)); + } + + @Override + public void sendUpdates(LwM2mClient lwM2mClient) { + LwM2MModelConfig modelConfig = currentModelConfigs.get(lwM2mClient.getEndpoint()); + if (modelConfig == null || modelConfig.isEmpty()) { + return; + } + + doSend(lwM2mClient, modelConfig); + } + + public void sendUpdates(LwM2mClient lwM2mClient, LwM2MModelConfig newModelConfig) { + String endpoint = lwM2mClient.getEndpoint(); + LwM2MModelConfig modelConfig = currentModelConfigs.get(endpoint); + if (modelConfig == null || modelConfig.isEmpty()) { + modelConfig = newModelConfig; + currentModelConfigs.put(endpoint, modelConfig); + } else { + modelConfig.merge(newModelConfig); + } + + if (lwM2mClient.isAsleep()) { + modelStore.put(modelConfig); + } else { + doSend(lwM2mClient, modelConfig); + } + } + + private void doSend(LwM2mClient lwM2mClient, LwM2MModelConfig modelConfig) { + log.trace("Send LwM2M Model updates: [{}]", modelConfig); + + String endpoint = lwM2mClient.getEndpoint(); + + Map attrToAdd = modelConfig.getAttributesToAdd(); + attrToAdd.forEach((id, attributes) -> { + TbLwM2MWriteAttributesRequest request = TbLwM2MWriteAttributesRequest.builder().versionedId(id) + .attributes(attributes) + .timeout(clientContext.getRequestTimeout(lwM2mClient)).build(); + downlinkMsgHandler.sendWriteAttributesRequest(lwM2mClient, request, + createDownlinkProxyCallback(() -> { + attrToAdd.remove(id); + if (modelConfig.isEmpty()) { + modelStore.remove(endpoint); + } + }, new TbLwM2MWriteAttributesCallback(logService, lwM2mClient, id)) + ); + }); + + Set attrToRemove = modelConfig.getAttributesToRemove(); + attrToRemove.forEach((id) -> { + TbLwM2MWriteAttributesRequest request = TbLwM2MWriteAttributesRequest.builder().versionedId(id) + .attributes(new ObjectAttributes()) + .timeout(clientContext.getRequestTimeout(lwM2mClient)).build(); + downlinkMsgHandler.sendWriteAttributesRequest(lwM2mClient, request, + createDownlinkProxyCallback(() -> { + attrToRemove.remove(id); + if (modelConfig.isEmpty()) { + modelStore.remove(endpoint); + } + }, new TbLwM2MWriteAttributesCallback(logService, lwM2mClient, id)) + ); + }); + + Set toRead = modelConfig.getToRead(); + toRead.forEach(id -> { + TbLwM2MReadRequest request = TbLwM2MReadRequest.builder().versionedId(id) + .timeout(clientContext.getRequestTimeout(lwM2mClient)).build(); + downlinkMsgHandler.sendReadRequest(lwM2mClient, request, + createDownlinkProxyCallback(() -> { + toRead.remove(id); + if (modelConfig.isEmpty()) { + modelStore.remove(endpoint); + } + }, new TbLwM2MReadCallback(uplinkMsgHandler, logService, lwM2mClient, id)) + ); + }); + + Set toObserve = modelConfig.getToObserve(); + toObserve.forEach(id -> { + TbLwM2MObserveRequest request = TbLwM2MObserveRequest.builder().versionedId(id) + .timeout(clientContext.getRequestTimeout(lwM2mClient)).build(); + downlinkMsgHandler.sendObserveRequest(lwM2mClient, request, + createDownlinkProxyCallback(() -> { + toObserve.remove(id); + if (modelConfig.isEmpty()) { + modelStore.remove(endpoint); + } + }, new TbLwM2MObserveCallback(uplinkMsgHandler, logService, lwM2mClient, id)) + ); + }); + + Set toCancelObserve = modelConfig.getToCancelObserve(); + toCancelObserve.forEach(id -> { + TbLwM2MCancelObserveRequest request = TbLwM2MCancelObserveRequest.builder().versionedId(id) + .timeout(clientContext.getRequestTimeout(lwM2mClient)).build(); + downlinkMsgHandler.sendCancelObserveRequest(lwM2mClient, request, + createDownlinkProxyCallback(() -> { + toCancelObserve.remove(id); + if (modelConfig.isEmpty()) { + modelStore.remove(endpoint); + } + }, new TbLwM2MCancelObserveCallback(logService, lwM2mClient, id)) + ); + }); + } + + private DownlinkRequestCallback createDownlinkProxyCallback(Runnable processRemove, DownlinkRequestCallback callback) { + return new DownlinkRequestCallback<>() { + @Override + public void onSuccess(R request, T response) { + processRemove.run(); + callback.onSuccess(request, response); + } + + @Override + public void onValidationError(String params, String msg) { + processRemove.run(); + callback.onValidationError(params, msg); + } + + @Override + public void onError(String params, Exception e) { + try { + if (e instanceof TimeoutException) { + return; + } + processRemove.run(); + } finally { + callback.onError(params, e); + } + } + + }; + } + + @Override + public void persistUpdates(String endpoint) { + LwM2MModelConfig modelConfig = currentModelConfigs.get(endpoint); + if (modelConfig != null && !modelConfig.isEmpty()) { + modelStore.put(modelConfig); + } + } + + @Override + public void removeUpdates(String endpoint) { + currentModelConfigs.remove(endpoint); + } + + @PreDestroy + private void destroy() { + currentModelConfigs.values().forEach(model -> { + if (model != null && !model.isEmpty()) { + modelStore.put(model); + } + }); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbDummyLwM2MModelConfigStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbDummyLwM2MModelConfigStore.java new file mode 100644 index 0000000000..06e55ca629 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbDummyLwM2MModelConfigStore.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2021 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.transport.lwm2m.server.store; + +import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfig; + +import java.util.Collections; +import java.util.List; + +public class TbDummyLwM2MModelConfigStore implements TbLwM2MModelConfigStore { + @Override + public List getAll() { + return Collections.emptyList(); + } + + @Override + public void put(LwM2MModelConfig modelConfig) { + + } + + @Override + public void remove(String endpoint) { + + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2MModelConfigStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2MModelConfigStore.java new file mode 100644 index 0000000000..74d7566279 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2MModelConfigStore.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2021 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.transport.lwm2m.server.store; + +import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfig; + +import java.util.List; + +public interface TbLwM2MModelConfigStore { + List getAll(); + + void put(LwM2MModelConfig modelConfig); + + void remove(String endpoint); +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java index 9156d73181..de2f0c8de0 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java @@ -62,6 +62,11 @@ public class TbLwM2mStoreFactory { return isRedis() ? new TbRedisLwM2MClientStore(getConnectionFactory()) : new TbDummyLwM2MClientStore(); } + @Bean + private TbLwM2MModelConfigStore modelConfigStore() { + return isRedis() ? new TbRedisLwM2MModelConfigStore(getConnectionFactory()) : new TbDummyLwM2MModelConfigStore(); + } + @Bean private TbLwM2MClientOtaInfoStore otaStore() { return isRedis() ? new TbLwM2mRedisClientOtaInfoStore(getConnectionFactory()) : new TbDummyLwM2MClientOtaInfoStore(); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MModelConfigStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MModelConfigStore.java new file mode 100644 index 0000000000..9cf016f7ba --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MModelConfigStore.java @@ -0,0 +1,79 @@ +/** + * Copyright © 2016-2021 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.transport.lwm2m.server.store; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.RedisClusterConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanOptions; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfig; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@AllArgsConstructor +public class TbRedisLwM2MModelConfigStore implements TbLwM2MModelConfigStore { + private static final String MODEL_EP = "MODEL#EP#"; + private final RedisConnectionFactory connectionFactory; + + @Override + public List getAll() { + try (var connection = connectionFactory.getConnection()) { + List configs = new ArrayList<>(); + ScanOptions scanOptions = ScanOptions.scanOptions().count(100).match(MODEL_EP + "*").build(); + List> scans = new ArrayList<>(); + if (connection instanceof RedisClusterConnection) { + ((RedisClusterConnection) connection).clusterGetNodes().forEach(node -> { + scans.add(((RedisClusterConnection) connection).scan(node, scanOptions)); + }); + } else { + scans.add(connection.scan(scanOptions)); + } + + scans.forEach(scan -> { + scan.forEachRemaining(key -> { + byte[] element = connection.get(key); + configs.add(JacksonUtil.fromBytes(element, LwM2MModelConfig.class)); + }); + }); + return configs; + } + } + + @Override + public void put(LwM2MModelConfig modelConfig) { + byte[] clientSerialized = JacksonUtil.writeValueAsBytes(modelConfig); + try (var connection = connectionFactory.getConnection()) { + connection.getSet(getKey(modelConfig.getEndpoint()), clientSerialized); + } + } + + @Override + public void remove(String endpoint) { + try (var connection = connectionFactory.getConnection()) { + connection.del(getKey(endpoint)); + } + } + + private byte[] getKey(String endpoint) { + return (MODEL_EP + endpoint).getBytes(); + } + +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2MUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2MUplinkMsgHandler.java index 3a24a19d9b..0ce98333ee 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2MUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2MUplinkMsgHandler.java @@ -83,6 +83,8 @@ import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MReadRequest import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttributesCallback; import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttributesRequest; import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService; +import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfig; +import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfigService; import org.thingsboard.server.transport.lwm2m.server.ota.LwM2MOtaUpdateService; import org.thingsboard.server.transport.lwm2m.server.session.LwM2MSessionManager; import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MDtlsSessionStore; @@ -106,6 +108,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static org.thingsboard.common.util.CollectionsUtil.diffSets; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LWM2M_ERROR; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LWM2M_INFO; @@ -147,6 +150,7 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl private final LwM2mVersionedModelProvider modelProvider; private final RegistrationStore registrationStore; private final TbLwM2mSecurityStore securityStore; + private final LwM2MModelConfigService modelConfigService; public DefaultLwM2MUplinkMsgHandler(TransportService transportService, LwM2MTransportServerConfig config, @@ -161,7 +165,8 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl TbLwM2MDtlsSessionStore sessionStore, LwM2mVersionedModelProvider modelProvider, RegistrationStore registrationStore, - TbLwM2mSecurityStore securityStore) { + TbLwM2mSecurityStore securityStore, + LwM2MModelConfigService modelConfigService) { this.transportService = transportService; this.sessionManager = sessionManager; this.attributesService = attributesService; @@ -176,6 +181,7 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl this.modelProvider = modelProvider; this.registrationStore = registrationStore; this.securityStore = securityStore; + this.modelConfigService = modelConfigService; } @PostConstruct @@ -764,7 +770,6 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl //TODO: review and optimize the logic to minimize number of the requests to device. private void onDeviceProfileUpdate(List clients, Lwm2mDeviceProfileTransportConfiguration oldProfile, DeviceProfile deviceProfile) { if (clientContext.profileUpdate(deviceProfile) != null) { - // #1 TelemetryMappingConfiguration oldTelemetryParams = oldProfile.getObserveAttr(); Set attributeSetOld = oldTelemetryParams.getAttribute(); Set telemetrySetOld = oldTelemetryParams.getTelemetry(); @@ -784,46 +789,42 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl Set observeToRemove = diffSets(observeNew, observeOld); Set newObjectsToRead = new HashSet<>(); + Set newObjectsToCancelRead = new HashSet<>(); - // #3.1 if (!attributeSetOld.equals(attributeSetNew)) { newObjectsToRead.addAll(diffSets(attributeSetOld, attributeSetNew)); + newObjectsToCancelRead.addAll(diffSets(attributeSetNew, attributeSetOld)); + } - // #3.2 if (!telemetrySetOld.equals(telemetrySetNew)) { newObjectsToRead.addAll(diffSets(telemetrySetOld, telemetrySetNew)); + newObjectsToCancelRead.addAll(diffSets(telemetrySetNew, telemetrySetOld)); } - // #3.3 if (!keyNameOld.equals(keyNameNew)) { ParametersAnalyzeResult keyNameChange = this.getAnalyzerKeyName(keyNameOld, keyNameNew); newObjectsToRead.addAll(keyNameChange.getPathPostParametersAdd()); } - // #3.4, #6 - if (!attributeLwm2mOld.equals(attributeLwm2mNew)) { - this.compareAndSendWriteAttributes(clients, attributeLwm2mOld, attributeLwm2mNew); - } + ParametersAnalyzeResult analyzerParameters = getAttributesAnalyzer(attributeLwm2mOld, attributeLwm2mNew); - // #4.1 add - if (!newObjectsToRead.isEmpty()) { - Set newObjectsToReadButNotNewInObserve = diffSets(observeToAdd, newObjectsToRead); - // update value in Resources - for (String versionedId : newObjectsToReadButNotNewInObserve) { - clients.forEach(client -> sendReadRequest(client, versionedId)); - } - } + clients.forEach(client -> { + LwM2MModelConfig modelConfig = new LwM2MModelConfig(client.getEndpoint()); + modelConfig.getToRead().addAll(diffSets(observeToAdd, newObjectsToRead)); + modelConfig.getToCancelRead().addAll(newObjectsToCancelRead); + modelConfig.getToCancelObserve().addAll(observeToRemove); + modelConfig.getToObserve().addAll(observeToAdd); - // Calculating difference between old and new flags. - if (!observeToAdd.isEmpty()) { - for (String targetId : observeToAdd) { - clients.forEach(client -> sendObserveRequest(client, targetId)); - } - } - if (!observeToRemove.isEmpty()) { - for (String targetId : observeToRemove) { - clients.forEach(client -> sendCancelObserveRequest(targetId, client)); - } - } + Set clientObjects = clientContext.getSupportedIdVerInClient(client); + Set pathToAdd = analyzerParameters.getPathPostParametersAdd().stream().filter(target -> clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1])) + .collect(Collectors.toUnmodifiableSet()); + modelConfig.getAttributesToAdd().putAll(pathToAdd.stream().collect(Collectors.toMap(t -> t, attributeLwm2mNew::get))); + + Set pathToRemove = analyzerParameters.getPathPostParametersDel().stream().filter(target -> clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1])) + .collect(Collectors.toUnmodifiableSet()); + modelConfig.getAttributesToRemove().addAll(pathToRemove); + + modelConfigService.sendUpdates(client, modelConfig); + }); // update value in fwInfo OtherConfiguration newLwM2mSettings = newProfile.getClientLwM2mSettings(); @@ -842,13 +843,6 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl } } - /** - * Returns new set with elements that are present in set B(new) but absent in set A(old). - */ - private static Set diffSets(Set a, Set b) { - return b.stream().filter(p -> !a.contains(p)).collect(Collectors.toSet()); - } - private ParametersAnalyzeResult getAnalyzerKeyName(Map keyNameOld, Map keyNameNew) { ParametersAnalyzeResult analyzerParameters = new ParametersAnalyzeResult(); Set paths = keyNameNew.entrySet() @@ -859,14 +853,10 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl return analyzerParameters; } - /** - * #6.1 - send update WriteAttribute - * #6.2 - send empty WriteAttribute - */ - private void compareAndSendWriteAttributes(List clients, Map lwm2mAttributesOld, Map lwm2mAttributesNew) { + private ParametersAnalyzeResult getAttributesAnalyzer(Map attributeLwm2mOld, Map attributeLwm2mNew) { ParametersAnalyzeResult analyzerParameters = new ParametersAnalyzeResult(); - Set pathOld = lwm2mAttributesOld.keySet(); - Set pathNew = lwm2mAttributesNew.keySet(); + Set pathOld = attributeLwm2mOld.keySet(); + Set pathNew = attributeLwm2mNew.keySet(); analyzerParameters.setPathPostParametersAdd(pathNew .stream().filter(p -> !pathOld.contains(p)).collect(Collectors.toSet())); analyzerParameters.setPathPostParametersDel(pathOld @@ -874,31 +864,13 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl Set pathCommon = pathNew .stream().filter(pathOld::contains).collect(Collectors.toSet()); Set pathCommonChange = pathCommon - .stream().filter(p -> !lwm2mAttributesOld.get(p).equals(lwm2mAttributesNew.get(p))).collect(Collectors.toSet()); + .stream().filter(p -> !attributeLwm2mOld.get(p).equals(attributeLwm2mNew.get(p))).collect(Collectors.toSet()); analyzerParameters.getPathPostParametersAdd().addAll(pathCommonChange); - // #6 - // #6.2 - if (analyzerParameters.getPathPostParametersAdd().size() > 0) { - clients.forEach(client -> { - Set clientObjects = clientContext.getSupportedIdVerInClient(client); - Set pathSend = analyzerParameters.getPathPostParametersAdd().stream().filter(target -> clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1])) - .collect(Collectors.toUnmodifiableSet()); - if (!pathSend.isEmpty()) { - pathSend.forEach(target -> sendWriteAttributesRequest(client, target, lwm2mAttributesNew.get(target))); - } - }); - } - // #6.2 - if (analyzerParameters.getPathPostParametersDel().size() > 0) { - clients.forEach(client -> { - Set clientObjects = clientContext.getSupportedIdVerInClient(client); - Set pathSend = analyzerParameters.getPathPostParametersDel().stream().filter(target -> clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1])) - .collect(Collectors.toUnmodifiableSet()); - if (!pathSend.isEmpty()) { - pathSend.forEach(target -> sendWriteAttributesRequest(client, target, new ObjectAttributes())); - } - }); - } + return analyzerParameters; + } + + private void compareAndSetWriteAttributes(LwM2mClient client, ParametersAnalyzeResult analyzerParameters, Map lwm2mAttributesNew, LwM2MModelConfig modelConfig) { + } /** diff --git a/common/util/src/main/java/org/thingsboard/common/util/CollectionsUtil.java b/common/util/src/main/java/org/thingsboard/common/util/CollectionsUtil.java new file mode 100644 index 0000000000..8a2228e554 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/CollectionsUtil.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2021 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.common.util; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +public class CollectionsUtil { + public static boolean isEmpty(Collection collection) { + return collection == null || collection.isEmpty(); + } + + public static boolean isNotEmpty(Collection collection) { + return !isEmpty(collection); + } + + /** + * Returns new set with elements that are present in set B(new) but absent in set A(old). + */ + public static Set diffSets(Set a, Set b) { + return b.stream().filter(p -> !a.contains(p)).collect(Collectors.toSet()); + } +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index da50963bd4..6e8e5610eb 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -123,4 +123,12 @@ public class JacksonUtil { return OBJECT_MAPPER.treeToValue(tree, type); } + public static byte[] writeValueAsBytes(T value) { + try { + return OBJECT_MAPPER.writeValueAsBytes(value); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("The given Json object value: " + + value + " cannot be transformed to a String", e); + } + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index 44bdc7f5d9..19f613bd30 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -36,6 +36,9 @@ import java.util.stream.Collectors; public abstract class DaoUtil { + public static final String DEFAULT_SORT_PROPERTY = "id"; + public static final Sort DEFAULT_SORT = Sort.by(Sort.Direction.ASC, DEFAULT_SORT_PROPERTY); + private DaoUtil() { } @@ -70,7 +73,7 @@ public abstract class DaoUtil { public static Sort toSort(SortOrder sortOrder, Map columnMap) { if (sortOrder == null) { - return Sort.unsorted(); + return DEFAULT_SORT; } else { String property = sortOrder.getProperty(); if (columnMap.containsKey(property)) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index 3255bf75b4..f70d1c1ffe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -336,12 +336,14 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return savedDevice; } + @Transactional @Override - public void deleteDevice(TenantId tenantId, DeviceId deviceId) { + public void deleteDevice(final TenantId tenantId, final DeviceId deviceId) { log.trace("Executing deleteDevice [{}]", deviceId); validateId(deviceId, INCORRECT_DEVICE_ID + deviceId); Device device = deviceDao.findById(tenantId, deviceId.getId()); + final String deviceName = device.getName(); try { List entityViews = entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(device.getTenantId(), deviceId).get(); if (entityViews != null && !entityViews.isEmpty()) { @@ -358,10 +360,10 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe } deleteEntityRelations(tenantId, deviceId); - removeDeviceFromCacheByName(tenantId, device.getName()); - removeDeviceFromCacheById(tenantId, device.getId()); - deviceDao.removeById(tenantId, deviceId.getId()); + + removeDeviceFromCacheByName(tenantId, deviceName); + removeDeviceFromCacheById(tenantId, deviceId); } private void removeDeviceFromCacheByName(TenantId tenantId, String name) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java index 3937b7c2c5..1c5d28b715 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.sqlts; import com.google.common.collect.Lists; -import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -34,6 +33,7 @@ import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; @@ -45,11 +45,9 @@ import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; import org.thingsboard.server.dao.sqlts.insert.latest.InsertLatestTsRepository; import org.thingsboard.server.dao.sqlts.latest.SearchTsKvLatestRepository; import org.thingsboard.server.dao.sqlts.latest.TsKvLatestRepository; -import org.thingsboard.server.dao.timeseries.SimpleListenableFuture; import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao; import org.thingsboard.server.dao.util.SqlTsLatestAnyDao; -import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.ArrayList; @@ -59,7 +57,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.ExecutionException; import java.util.function.Function; import java.util.stream.Collectors; @@ -147,7 +144,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme } @Override - public ListenableFuture removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + public ListenableFuture removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { return getRemoveLatestFuture(tenantId, entityId, query); } @@ -175,15 +172,16 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme return tsKvLatestRepository.findAllKeysByEntityIds(entityIds.stream().map(EntityId::getId).collect(Collectors.toList())); } - private ListenableFuture getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + private ListenableFuture getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { ListenableFuture> future = findNewLatestEntryFuture(tenantId, entityId, query); return Futures.transformAsync(future, entryList -> { if (entryList.size() == 1) { - return getSaveLatestFuture(entityId, entryList.get(0)); + TsKvEntry entry = entryList.get(0); + return Futures.transform(getSaveLatestFuture(entityId, entry), v -> new TsKvLatestRemovingResult(entry), MoreExecutors.directExecutor()); } else { log.trace("Could not find new latest value for [{}], key - {}", entityId, query.getKey()); } - return Futures.immediateFuture(null); + return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), true)); }, service); } @@ -212,7 +210,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme return Futures.immediateFuture(result); } - protected ListenableFuture getRemoveLatestFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + protected ListenableFuture getRemoveLatestFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { ListenableFuture latestFuture = getFindLatestFuture(entityId, query.getKey()); ListenableFuture booleanFuture = Futures.transform(latestFuture, tsKvEntry -> { @@ -220,47 +218,25 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme return ts > query.getStartTs() && ts <= query.getEndTs(); }, service); - ListenableFuture removedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> { + ListenableFuture removedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> { if (isRemove) { TsKvLatestEntity latestEntity = new TsKvLatestEntity(); latestEntity.setEntityId(entityId.getId()); latestEntity.setKey(getOrSaveKeyId(query.getKey())); return service.submit(() -> { tsKvLatestRepository.delete(latestEntity); - return null; + return true; }); } - return Futures.immediateFuture(null); + return Futures.immediateFuture(false); }, service); - final SimpleListenableFuture resultFuture = new SimpleListenableFuture<>(); - Futures.addCallback(removedLatestFuture, new FutureCallback() { - @Override - public void onSuccess(@Nullable Void result) { - if (query.getRewriteLatestIfDeleted()) { - ListenableFuture savedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> { - if (isRemove) { - return getNewLatestEntryFuture(tenantId, entityId, query); - } - return Futures.immediateFuture(null); - }, service); - - try { - resultFuture.set(savedLatestFuture.get()); - } catch (InterruptedException | ExecutionException e) { - log.warn("Could not get latest saved value for [{}], {}", entityId, query.getKey(), e); - } - } else { - resultFuture.set(null); - } - } - - @Override - public void onFailure(Throwable t) { - log.warn("[{}] Failed to process remove of the latest value", entityId, t); + return Futures.transformAsync(removedLatestFuture, isRemoved -> { + if (isRemoved && query.getRewriteLatestIfDeleted()) { + return getNewLatestEntryFuture(tenantId, entityId, query); } + return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), isRemoved)); }, MoreExecutors.directExecutor()); - return resultFuture; } protected ListenableFuture> getFindAllLatestFuture(EntityId entityId) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index da3fe05024..d590aad864 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.IncorrectParameterException; @@ -218,10 +219,10 @@ public class BaseTimeseriesService implements TimeseriesService { } @Override - public ListenableFuture> remove(TenantId tenantId, EntityId entityId, List deleteTsKvQueries) { + public ListenableFuture> remove(TenantId tenantId, EntityId entityId, List deleteTsKvQueries) { validate(entityId); deleteTsKvQueries.forEach(BaseTimeseriesService::validate); - List> futures = Lists.newArrayListWithExpectedSize(deleteTsKvQueries.size() * DELETES_PER_ENTRY); + List> futures = Lists.newArrayListWithExpectedSize(deleteTsKvQueries.size() * DELETES_PER_ENTRY); for (DeleteTsKvQuery tsKvQuery : deleteTsKvQueries) { deleteAndRegisterFutures(tenantId, futures, entityId, tsKvQuery); } @@ -229,9 +230,9 @@ public class BaseTimeseriesService implements TimeseriesService { } @Override - public ListenableFuture> removeLatest(TenantId tenantId, EntityId entityId, Collection keys) { + public ListenableFuture> removeLatest(TenantId tenantId, EntityId entityId, Collection keys) { validate(entityId); - List> futures = Lists.newArrayListWithExpectedSize(keys.size()); + List> futures = Lists.newArrayListWithExpectedSize(keys.size()); for (String key : keys) { DeleteTsKvQuery query = new BaseDeleteTsKvQuery(key, 0, System.currentTimeMillis(), false); futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query)); @@ -252,10 +253,10 @@ public class BaseTimeseriesService implements TimeseriesService { }, MoreExecutors.directExecutor()); } - private void deleteAndRegisterFutures(TenantId tenantId, List> futures, EntityId entityId, DeleteTsKvQuery query) { - futures.add(timeseriesDao.remove(tenantId, entityId, query)); + private void deleteAndRegisterFutures(TenantId tenantId, List> futures, EntityId entityId, DeleteTsKvQuery query) { + futures.add(Futures.transform(timeseriesDao.remove(tenantId, entityId, query), v -> null, MoreExecutors.directExecutor())); futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query)); - futures.add(timeseriesDao.removePartition(tenantId, entityId, query)); + futures.add(Futures.transform(timeseriesDao.removePartition(tenantId, entityId, query), v -> null, MoreExecutors.directExecutor())); } private static void validate(EntityId entityId) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java index 0b4ae71e85..5b1a4d2eb0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.nosql.TbResultSet; @@ -114,7 +115,7 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes } @Override - public ListenableFuture removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + public ListenableFuture removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { ListenableFuture latestEntryFuture = findLatest(tenantId, entityId, query.getKey()); ListenableFuture booleanFuture = Futures.transform(latestEntryFuture, latestEntry -> { @@ -127,44 +128,22 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes return false; }, readResultsProcessingExecutor); - ListenableFuture removedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> { + ListenableFuture removedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> { if (isRemove) { - return deleteLatest(tenantId, entityId, query.getKey()); + return Futures.transform(deleteLatest(tenantId, entityId, query.getKey()), res -> true, MoreExecutors.directExecutor()); } - return Futures.immediateFuture(null); + return Futures.immediateFuture(false); }, readResultsProcessingExecutor); - final SimpleListenableFuture resultFuture = new SimpleListenableFuture<>(); - Futures.addCallback(removedLatestFuture, new FutureCallback() { - @Override - public void onSuccess(@Nullable Void result) { - if (query.getRewriteLatestIfDeleted()) { - ListenableFuture savedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> { - if (isRemove) { - return getNewLatestEntryFuture(tenantId, entityId, query); - } - return Futures.immediateFuture(null); - }, readResultsProcessingExecutor); - - try { - resultFuture.set(savedLatestFuture.get()); - } catch (InterruptedException | ExecutionException e) { - log.warn("Could not get latest saved value for [{}], {}", entityId, query.getKey(), e); - } - } else { - resultFuture.set(null); - } - } - - @Override - public void onFailure(Throwable t) { - log.warn("[{}] Failed to process remove of the latest value", entityId, t); + return Futures.transformAsync(removedLatestFuture, isRemoved -> { + if (isRemoved && query.getRewriteLatestIfDeleted()) { + return getNewLatestEntryFuture(tenantId, entityId, query); } + return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), isRemoved)); }, MoreExecutors.directExecutor()); - return resultFuture; } - private ListenableFuture getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + private ListenableFuture getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { long startTs = 0; long endTs = query.getStartTs() - 1; ReadTsKvQuery findNewLatestQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, endTs - startTs, 1, @@ -173,11 +152,12 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes return Futures.transformAsync(future, entryList -> { if (entryList.size() == 1) { - return saveLatest(tenantId, entityId, entryList.get(0)); + TsKvEntry entry = entryList.get(0); + return Futures.transform(saveLatest(tenantId, entityId, entryList.get(0)), v -> new TsKvLatestRemovingResult(entry), MoreExecutors.directExecutor()); } else { log.trace("Could not find new latest value for [{}], key - {}", entityId, query.getKey()); } - return Futures.immediateFuture(null); + return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), true)); }, readResultsProcessingExecutor); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java index f19cc461dc..f83a41c010 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; +import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import java.util.List; @@ -32,7 +33,7 @@ public interface TimeseriesLatestDao { ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry); - ListenableFuture removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query); + ListenableFuture removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query); List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java index ed6b14dd10..8fcadcd812 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java @@ -44,6 +44,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { @@ -58,7 +59,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { private static final ByteBuffer DATA = ByteBuffer.wrap(new byte[]{(int) DATA_SIZE}); private static final String URL = "http://firmware.test.org"; - private IdComparator idComparator = new IdComparator<>(); + private final IdComparator idComparator = new IdComparator<>(); private TenantId tenantId; @@ -565,7 +566,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { Collections.sort(firmwares, idComparator); Collections.sort(loadedFirmwares, idComparator); - Assert.assertEquals(firmwares, loadedFirmwares); + assertThat(firmwares).isEqualTo(loadedFirmwares); otaPackageService.deleteOtaPackagesByTenantId(tenantId); @@ -620,7 +621,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { Collections.sort(firmwares, idComparator); Collections.sort(loadedFirmwares, idComparator); - Assert.assertEquals(firmwares, loadedFirmwares); + assertThat(firmwares).isEqualTo(loadedFirmwares); otaPackageService.deleteOtaPackagesByTenantId(tenantId); diff --git a/pom.xml b/pom.xml index 182491d4f8..b7c873898f 100755 --- a/pom.xml +++ b/pom.xml @@ -47,7 +47,7 @@ 3.3.0 0.7.0 1.7.32 - 2.16.0 + 2.17.0 1.2.6 0.10 4.10.0 diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java index b2fd7d2a97..e3406d4f02 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import java.util.Collection; @@ -56,5 +57,5 @@ public interface RuleEngineTelemetryService { void deleteAllLatest(TenantId tenantId, EntityId entityId, FutureCallback> callback); - + void deleteTimeseriesAndNotify(TenantId tenantId, EntityId entityId, List keys, List deleteTsKvQueries, FutureCallback callback); } diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index d053efb601..ad02f0d7be 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -150,6 +150,7 @@ export type StateControllerHolder = () => IStateController; export interface IStateController { dashboardCtrl: IDashboardController; getStateParams(): StateParams; + stateChanged(): Observable; getStateParamsByStateId(stateId: string): StateParams; openState(id: string, params?: StateParams, openRightLayout?: boolean): void; updateState(id?: string, params?: StateParams, openRightLayout?: boolean): void; diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index aa52c55cbe..b986583171 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -114,6 +114,10 @@ export function isNumeric(value: any): boolean { return (value - parseFloat(value) + 1) >= 0; } +export function isBoolean(value: any): boolean { + return typeof value === 'boolean'; +} + export function isString(value: any): boolean { return typeof value === 'string'; } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 37ef5e0429..4b11f29de0 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -17,7 +17,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, - Component, ElementRef, + Component, ElementRef, HostBinding, Inject, Injector, Input, @@ -48,7 +48,7 @@ import { } from '@app/shared/models/dashboard.models'; import { WINDOW } from '@core/services/window.service'; import { WindowMessage } from '@shared/models/window-message.model'; -import { deepClone, isDefined, isDefinedAndNotNull } from '@app/core/utils'; +import { deepClone, guid, hashCode, isDefined, isDefinedAndNotNull, isNotEmptyStr } from '@app/core/utils'; import { DashboardContext, DashboardPageLayout, @@ -132,6 +132,8 @@ import { DashboardImageDialogData, DashboardImageDialogResult } from '@home/components/dashboard-page/dashboard-image-dialog.component'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import cssjs from '@core/css/css'; +import { DOCUMENT } from '@angular/common'; // @dynamic @Component({ @@ -147,6 +149,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC authUser: AuthUser = this.authState.authUser; + @HostBinding('class') + dashboardPageClass: string; + @Input() embedded = false; @@ -228,6 +233,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC dashboardTimewindow: null, state: null, stateController: null, + stateChanged: null, aliasController: null, runChangeDetection: this.runChangeDetection.bind(this) }; @@ -302,6 +308,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC constructor(protected store: Store, @Inject(WINDOW) private window: Window, + @Inject(DOCUMENT) private document: Document, private breakpointObserver: BreakpointObserver, private route: ActivatedRoute, private router: Router, @@ -419,6 +426,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC this.dashboardConfiguration.entityAliases, this.dashboardConfiguration.filters); + this.updateDashboardCss(); + if (this.widgetEditMode) { const message: WindowMessage = { type: 'widgetEditModeInited' @@ -427,6 +436,27 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } } + private updateDashboardCss() { + this.cleanupDashboardCss(); + const cssString = this.dashboardConfiguration.settings.dashboardCss; + if (isNotEmptyStr(cssString)) { + const cssParser = new cssjs(); + cssParser.testMode = false; + this.dashboardPageClass = 'tb-dashboard-page-css-' + guid(); + cssParser.cssPreviewNamespace = this.dashboardPageClass; + cssParser.createStyleElement(this.dashboardPageClass, cssString); + } + } + + private cleanupDashboardCss() { + if (this.dashboardPageClass) { + const el = this.document.getElementById(this.dashboardPageClass); + if (el) { + el.parentNode.removeChild(el); + } + } + } + private reset() { this.dashboard = null; this.translatedDashboardTitle = null; @@ -466,6 +496,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } ngOnDestroy(): void { + this.cleanupDashboardCss(); if (this.isMobileApp && this.syncStateWithQueryParam) { this.mobileService.unregisterToggleLayoutFunction(); } @@ -729,6 +760,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC if (data) { this.dashboard.configuration.settings = data.settings; this.dashboardLogoCache = undefined; + this.updateDashboardCss(); const newGridSettings = data.gridSettings; if (newGridSettings) { const layout = this.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout]; @@ -893,6 +925,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC this.dashboardLogoCache = undefined; this.dashboardConfiguration = this.dashboard.configuration; this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow; + this.updateDashboardCss(); this.entityAliasesUpdated(); this.filtersUpdated(); this.updateLayouts(); diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts index c4f7a2965a..ddef622f99 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts @@ -20,6 +20,7 @@ import { Timewindow } from '@shared/models/time/time.models'; import { IAliasController, IStateController } from '@core/api/widget-api.models'; import { ILayoutController } from './layout/layout.models'; import { DashboardContextMenuItem, WidgetContextMenuItem } from '@home/models/dashboard-component.models'; +import { Observable } from 'rxjs'; export declare type DashboardPageScope = 'tenant' | 'customer'; @@ -30,6 +31,7 @@ export interface DashboardContext { dashboardTimewindow: Timewindow; aliasController: IAliasController; stateController: IStateController; + stateChanged: Observable; runChangeDetection: () => void; } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html index 14c2df301b..0c0a430fdb 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html @@ -87,6 +87,19 @@ {{ 'dashboard.display-update-dashboard-image' | translate }} + + + + dashboard.advanced-settings + + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.scss index 5587d9c35a..47845eb9b0 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.scss @@ -30,4 +30,33 @@ .mat-slide-toggle-content { white-space: normal; } + .mat-expansion-panel { + &.tb-settings { + box-shadow: none; + .mat-content { + overflow: visible; + } + .mat-expansion-panel-header { + padding: 0; + &:hover { + background: none; + } + .mat-expansion-indicator { + padding: 2px; + } + } + .mat-expansion-panel-header-description { + align-items: center; + } + .mat-expansion-panel-body{ + padding: 0; + } + .tb-css-content-panel { + margin: 0 0 8px; + } + } + .mat-expansion-panel-content { + font: inherit; + } + } } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts index 5d17d33eaa..c2d01f3d9c 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts @@ -97,7 +97,8 @@ export class DashboardSettingsDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.html new file mode 100644 index 0000000000..24c3eadb08 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.html @@ -0,0 +1,25 @@ + + + diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.ts new file mode 100644 index 0000000000..fb22d9f3c2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.ts @@ -0,0 +1,93 @@ +/// +/// Copyright © 2016-2021 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 { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Dashboard } from '@shared/models/dashboard.models'; +import { StateObject } from '@core/api/widget-api.models'; +import { updateEntityParams, WidgetContext } from '@home/models/widget-component.models'; +import { deepClone, objToBase64 } from '@core/utils'; +import { IDashboardComponent } from '@home/models/dashboard-component.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'tb-dashboard-state', + templateUrl: './dashboard-state.component.html', + styleUrls: [] +}) +export class DashboardStateComponent extends PageComponent implements OnInit, OnDestroy { + + @Input() + ctx: WidgetContext; + + @Input() + stateId: string; + + @Input() + syncParentStateParams = false; + + @Input() + entityParamName: string; + + @Input() + entityId: EntityId; + + currentState: string; + + dashboard: Dashboard; + + parentDashboard: IDashboardComponent; + + private stateSubscription: Subscription; + + constructor(protected store: Store, + private cd: ChangeDetectorRef) { + super(store); + } + + ngOnInit(): void { + this.dashboard = deepClone(this.ctx.stateController.dashboardCtrl.dashboardCtx.getDashboard()); + this.updateCurrentState(); + this.parentDashboard = this.ctx.parentDashboard ? + this.ctx.parentDashboard : this.ctx.dashboard; + if (this.syncParentStateParams) { + this.stateSubscription = this.ctx.stateController.dashboardCtrl.dashboardCtx.stateChanged.subscribe(() => { + this.updateCurrentState(); + this.cd.markForCheck(); + }); + } + } + + ngOnDestroy(): void { + if (this.stateSubscription) { + this.stateSubscription.unsubscribe(); + } + } + + private updateCurrentState(): void { + const stateObject: StateObject = {}; + const params = deepClone(this.ctx.stateController.getStateParams()); + updateEntityParams(params, this.entityParamName, this.entityId); + stateObject.params = params; + if (this.stateId) { + stateObject.id = this.stateId; + } + this.currentState = objToBase64([stateObject]); + } +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.ts index 674386f108..67e24b628c 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.ts @@ -54,7 +54,7 @@ export class DefaultStateControllerComponent extends StateControllerComponent im super.ngOnDestroy(); } - protected init() { + public init() { if (this.preservedState) { this.stateObject = this.preservedState; setTimeout(() => { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.ts index f12dd033aa..c559459819 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.ts @@ -58,7 +58,7 @@ export class EntityStateControllerComponent extends StateControllerComponent imp super.ngOnDestroy(); } - protected init() { + public init() { if (this.preservedState) { this.stateObject = this.preservedState; this.selectedStateIndex = this.stateObject.length - 1; diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.component.ts index 5acf57095f..ef66915a9e 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.component.ts @@ -17,7 +17,7 @@ import { IStateControllerComponent, StateControllerState } from '@home/components/dashboard-page/states/state-controller.models'; import { IDashboardController } from '../dashboard-page.models'; import { DashboardState } from '@app/shared/models/dashboard.models'; -import { Subscription } from 'rxjs'; +import { Observable, Subject, Subscription } from 'rxjs'; import { NgZone, OnDestroy, OnInit, Directive } from '@angular/core'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { StatesControllerService } from '@home/components/dashboard-page/states/states-controller.service'; @@ -27,6 +27,7 @@ import { StateObject, StateParams } from '@app/core/api/widget-api.models'; @Directive() export abstract class StateControllerComponent implements IStateControllerComponent, OnInit, OnDestroy { + private stateChangedSubject = new Subject(); stateObject: StateControllerState = []; dashboardCtrl: IDashboardController; preservedState: any; @@ -108,6 +109,7 @@ export abstract class StateControllerComponent implements IStateControllerCompon const newState = this.decodeStateParam(paramMap.get('state')); if (this.currentState !== newState) { this.currentState = newState; + this.stateChangedSubject.next(this.currentState); if (this.inited) { this.onStateChanged(); } @@ -124,6 +126,7 @@ export abstract class StateControllerComponent implements IStateControllerCompon subscription.unsubscribe(); }); this.rxSubscriptions.length = 0; + this.stateChangedSubject.complete(); } protected updateStateParam(newState: string, replaceCurrentHistoryUrl = false) { @@ -142,6 +145,11 @@ export abstract class StateControllerComponent implements IStateControllerCompon }); }); } + this.stateChangedSubject.next(this.currentState); + } + + public stateChanged(): Observable { + return this.stateChangedSubject.asObservable(); } public openRightLayout(): void { @@ -159,6 +167,7 @@ export abstract class StateControllerComponent implements IStateControllerCompon public reInit() { this.preservedState = null; this.currentState = this.decodeStateParam(this.route.snapshot.queryParamMap.get('state')); + this.stateChangedSubject.next(this.currentState); this.init(); } @@ -166,7 +175,7 @@ export abstract class StateControllerComponent implements IStateControllerCompon return stateURI !== null ? decodeURIComponent(stateURI) : null; } - protected abstract init(); + public abstract init(); protected abstract onMobileChanged(); diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.models.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.models.ts index 6a21ac564f..333a66b3d1 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.models.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.models.ts @@ -30,4 +30,5 @@ export interface IStateControllerComponent extends IStateController { dashboardId: string; preservedState: any; reInit(): void; + init(): void; } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/states-component.directive.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/states/states-component.directive.ts index b6911ed9d8..5ddfa7ea71 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/states-component.directive.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/states-component.directive.ts @@ -28,6 +28,7 @@ import { DashboardState } from '@shared/models/dashboard.models'; import { IDashboardController } from '@home/components/dashboard-page/dashboard-page.models'; import { StatesControllerService } from '@home/components/dashboard-page/states/states-controller.service'; import { IStateControllerComponent } from '@home/components/dashboard-page/states/state-controller.models'; +import { Subject } from 'rxjs'; @Directive({ // tslint:disable-next-line:directive-selector @@ -62,6 +63,8 @@ export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges { stateControllerComponentRef: ComponentRef; stateControllerComponent: IStateControllerComponent; + private stateChangedSubject = new Subject(); + constructor(private viewContainerRef: ViewContainerRef, private statesControllerService: StatesControllerService) { } @@ -72,10 +75,12 @@ export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges { ngOnDestroy(): void { this.destroy(); + this.stateChangedSubject.complete(); } ngOnChanges(changes: SimpleChanges): void { let reInitController = false; + let initController = false; for (const propName of Object.keys(changes)) { const change = changes[propName]; if (!change.firstChange && change.currentValue !== change.previousValue) { @@ -92,12 +97,15 @@ export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges { this.stateControllerComponent.state = this.state; } else if (propName === 'currentState') { this.stateControllerComponent.currentState = this.currentState; + initController = true; } else if (propName === 'syncStateWithQueryParam') { this.stateControllerComponent.syncStateWithQueryParam = this.syncStateWithQueryParam; } } } - if (reInitController) { + if (initController) { + this.stateControllerComponent.init(); + } else if (reInitController) { this.stateControllerComponent.reInit(); } } @@ -119,6 +127,10 @@ export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges { this.stateControllerComponentRef = this.viewContainerRef.createComponent(stateControllerFactory); this.stateControllerComponent = this.stateControllerComponentRef.instance; this.dashboardCtrl.dashboardCtx.stateController = this.stateControllerComponent; + this.dashboardCtrl.dashboardCtx.stateChanged = this.stateChangedSubject.asObservable(); + this.stateControllerComponent.stateChanged().subscribe((state) => { + this.stateChangedSubject.next(state); + }); this.stateControllerComponent.preservedState = preservedState; this.stateControllerComponent.dashboardCtrl = this.dashboardCtrl; this.stateControllerComponent.stateControllerInstanceId = stateControllerInstanceId; diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html index c48030050d..dafb67df0b 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html @@ -63,8 +63,9 @@
- + > = new InjectionToken>('SHARED_HOME_COMPONENTS_MODULE_TOKEN'); +export const HOME_COMPONENTS_MODULE_TOKEN: InjectionToken> = + new InjectionToken>('HOME_COMPONENTS_MODULE_TOKEN'); + export const COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN: InjectionToken> = new InjectionToken>('COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN'); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts index 27d6fb5edc..78f459990e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import L, { FeatureGroup, Icon, LatLngBounds, LatLngTuple, Projection } from 'leaflet'; +import L, { FeatureGroup, LatLngBounds, LatLngTuple, Projection } from 'leaflet'; import tinycolor from 'tinycolor2'; import 'leaflet-providers'; import { MarkerClusterGroup, MarkerClusterGroupOptions } from 'leaflet.markercluster/dist/leaflet.markercluster'; @@ -24,7 +24,7 @@ import { defaultSettings, FormattedData, MapSettings, - MarkerSettings, + MarkerSettings, MarkerIconInfo, MarkerImageInfo, PolygonSettings, PolylineSettings, ReplaceInfo, @@ -64,11 +64,13 @@ export default abstract class LeafletMap { points: FeatureGroup; markersData: FormattedData[] = []; polygonsData: FormattedData[] = []; - defaultMarkerIconInfo: { size: number[], icon: Icon }; + defaultMarkerIconInfo: MarkerIconInfo; loadingDiv: JQuery; loading = false; replaceInfoLabelMarker: Array = []; markerLabelText: string; + polygonLabelText: string; + replaceInfoLabelPolygon: Array = []; replaceInfoTooltipMarker: Array = []; markerTooltipText: string; drawRoutes: boolean; @@ -248,8 +250,16 @@ export default abstract class LeafletMap { this.saveLocation(this.selectedEntity, this.convertToCustomFormat(e.layer.getLatLng())).subscribe(() => { }); } else if (e.shape === 'tbRectangle' || e.shape === 'tbPolygon') { - // @ts-ignore - this.saveLocation(this.selectedEntity, this.convertPolygonToCustomFormat(e.layer.getLatLngs()[0])).subscribe(() => { + let coordinates; + if (e.shape === 'tbRectangle') { + // @ts-ignore + const bounds: L.LatLngBounds = e.layer.getBounds(); + coordinates = [bounds.getNorthWest(), bounds.getSouthEast()]; + } else { + // @ts-ignore + coordinates = e.layer.getLatLngs()[0]; + } + this.saveLocation(this.selectedEntity, this.convertPolygonToCustomFormat(coordinates)).subscribe(() => { }); } // @ts-ignore @@ -281,7 +291,7 @@ export default abstract class LeafletMap { result = iterator.next(); } this.saveLocation(result.value.data, this.convertToCustomFormat(null)).subscribe(() => {}); - } else if (e.shape === 'Polygon') { + } else if (e.shape === 'Polygon' || e.shape === 'Rectangle') { const iterator = this.polygons.values(); let result = iterator.next(); while (!result.done && e.layer !== result.value.leafletPoly) { @@ -333,6 +343,7 @@ export default abstract class LeafletMap { this.map.scrollWheelZoom.disable(); } if (this.options.draggableMarker || this.editPolygons) { + map.pm.setGlobalOptions({ snappable: false } as L.PM.GlobalOptions); this.addEditControl(); } else { this.map.pm.disableDraw(); @@ -525,16 +536,16 @@ export default abstract class LeafletMap { let m: Marker; rawMarkers.forEach(data => { if (data.rotationAngle || data.rotationAngle === 0) { - const currentImage = this.options.useMarkerImageFunction ? + const currentImage: MarkerImageInfo = this.options.useMarkerImageFunction ? safeExecute(this.options.markerImageFunction, [data, this.options.markerImages, markersData, data.dsIndex]) : this.options.currentImage; const style = currentImage ? 'background-image: url(' + currentImage.url + ');' : ''; - this.options.icon = L.divIcon({ + this.options.icon = { icon: L.divIcon({ html: `
` - }); + style="transform: translate(-10px, -10px) + rotate(${data.rotationAngle}deg); + ${style}">
` + }), size: [30, 30]}; } else { this.options.icon = null; } @@ -764,6 +775,14 @@ export default abstract class LeafletMap { if (coordinates.length === 1) { coordinates = coordinates[0]; } + if (e.shape === 'Rectangle' && coordinates.length === 1) { + // @ts-ignore + const bounds: L.LatLngBounds = e.layer.getBounds(); + const boundsArray = [bounds.getNorthWest(), bounds.getNorthEast(), bounds.getSouthWest(), bounds.getSouthEast()]; + if (coordinates.every(point => boundsArray.find(boundPoint => boundPoint.equals(point)) !== undefined)) { + coordinates = [bounds.getNorthWest(), bounds.getSouthEast()]; + } + } this.saveLocation(data, this.convertPolygonToCustomFormat(coordinates)).subscribe(() => {}); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts index 5b99b792e9..e65479f588 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts @@ -17,13 +17,15 @@ import { Datasource } from '@app/shared/models/widget.models'; import { EntityType } from '@shared/models/entity-type.models'; import tinycolor from 'tinycolor2'; +import { BaseIconOptions, Icon } from 'leaflet'; export const DEFAULT_MAP_PAGE_SIZE = 16384; export const DEFAULT_ZOOM_LEVEL = 8; export type GenericFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string; -export type MarkerImageFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string; +export type MarkerImageFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => MarkerImageInfo; export type PosFuncton = (origXPos, origYPos) => { x, y }; +export type MarkerIconReadyFunction = (icon: MarkerIconInfo) => void; export type MapSettings = { draggableMarker: boolean; @@ -51,7 +53,6 @@ export type MapSettings = { useDefaultCenterPosition?: boolean; gmDefaultMapType?: string; useLabelFunction: boolean; - icon?: any; zoomOnClick: boolean, maxZoom: number, showCoverageOnHover: boolean, @@ -73,10 +74,22 @@ export enum MapProviders { tencent = 'tencent-map' } +export type MarkerImageInfo = { + url: string; + size: number; + markerOffset?: [number, number]; + tooltipOffset?: [number, number]; +}; + +export type MarkerIconInfo = { + icon: Icon; + size: [number, number]; +}; + export type MarkerSettings = { tooltipPattern?: any; tooltipAction: { [name: string]: actionsHandler }; - icon?: any; + icon?: MarkerIconInfo; showLabel?: boolean; label: string; labelColor: string; @@ -91,7 +104,7 @@ export type MarkerSettings = { autocloseTooltip: boolean; showTooltipAction: string; useClusterMarkers: boolean; - currentImage?: string; + currentImage?: MarkerImageInfo; useMarkerImageFunction?: boolean; markerImages?: string[]; markerImageSize: number; @@ -104,6 +117,8 @@ export type MarkerSettings = { markerImageFunction?: MarkerImageFunction; markerOffsetX: number; markerOffsetY: number; + tooltipOffsetX: number; + tooltipOffsetY: number; }; export interface FormattedData { @@ -131,6 +146,11 @@ export type PolygonSettings = { polygonStrokeWeight: number; polygonStrokeColor: string; polygonColor: string; + showPolygonLabel?: boolean; + polygonLabel: string; + polygonLabelColor: string; + polygonLabelText: string; + usePolygonLabelFunction: boolean; showPolygonTooltip: boolean; autocloseTooltip: boolean; showTooltipAction: string; @@ -139,8 +159,11 @@ export type PolygonSettings = { usePolygonTooltipFunction: boolean; polygonClick: { [name: string]: actionsHandler }; usePolygonColorFunction: boolean; + usePolygonStrokeColorFunction: boolean; polygonTooltipFunction: GenericFunction; polygonColorFunction?: GenericFunction; + polygonStrokeColorFunction?: GenericFunction; + polygonLabelFunction?: GenericFunction; editablePolygon: boolean; }; @@ -215,6 +238,8 @@ export const defaultSettings: any = { yPosKeyName: 'yPos', markerOffsetX: 0.5, markerOffsetY: 1, + tooltipOffsetX: 0, + tooltipOffsetY: -1, latKeyName: 'latitude', lngKeyName: 'longitude', polygonKeyName: 'coordinates', @@ -227,8 +252,10 @@ export const defaultSettings: any = { showPolygon: false, labelColor: '#000000', color: '#FE7569', + showPolygonLabel: false, polygonColor: '#0000ff', polygonStrokeColor: '#fe0001', + polygonLabelColor: '#000000', polygonOpacity: 0.5, polygonStrokeOpacity: 1, polygonStrokeWeight: 1, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts index a2385b2877..565887b160 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts @@ -258,17 +258,20 @@ export class MapWidgetController implements MapWidgetInterface { settings.mapProviderHere = hereProviders[0]; } } - const customOptions = { + const customOptions: Partial = { provider: this.provider, mapUrl: settings?.mapImageUrl, labelFunction: parseFunction(settings.labelFunction, functionParams), tooltipFunction: parseFunction(settings.tooltipFunction, functionParams), colorFunction: parseFunction(settings.colorFunction, functionParams), colorPointFunction: parseFunction(settings.colorPointFunction, functionParams), + polygonLabelFunction: parseFunction(settings.polygonLabelFunction, functionParams), polygonColorFunction: parseFunction(settings.polygonColorFunction, functionParams), + polygonStrokeColorFunction: parseFunction(settings.polygonStrokeColorFunction, functionParams), polygonTooltipFunction: parseFunction(settings.polygonTooltipFunction, functionParams), markerImageFunction: parseFunction(settings.markerImageFunction, ['data', 'images', 'dsData', 'dsIndex']), labelColor: this.ctx.widgetConfig.color, + polygonLabelColor: this.ctx.widgetConfig.color, polygonKeyName: settings.polKeyName ? settings.polKeyName : settings.polygonKeyName, tooltipPattern: settings.tooltipPattern || '${entityName}

Latitude: ${' + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts index c76a6de48e..9c2fbcdcd5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts @@ -56,3 +56,10 @@ export function bindPopupActions(popup: L.Popup, settings: MarkerSettings | Poly } }); } + +export function isCutPolygon(data): boolean { + if (Array.isArray(data[0]) && Array.isArray(data[0][0])) { + return true; + } + return false; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss index 147386685c..f45d08f9e9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss @@ -29,6 +29,15 @@ box-shadow: none; } +.tb-polygon-label { + border: none; + background: none; + box-shadow: none; + &:before { + content: none; + } +} + .leaflet-container { background-color: white; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts index 5550706402..85020b7f33 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts @@ -15,7 +15,7 @@ /// import L, { Icon, LeafletMouseEvent } from 'leaflet'; -import { FormattedData, MarkerSettings } from './map-models'; +import { FormattedData, MarkerIconInfo, MarkerIconReadyFunction, MarkerImageInfo, MarkerSettings } from './map-models'; import { bindPopupActions, createTooltip, @@ -33,6 +33,7 @@ import LeafletMap from './leaflet-map'; export class Marker { leafletMarker: L.Marker; + labelOffset: L.LatLngTuple; tooltipOffset: L.LatLngTuple; markerOffset: L.LatLngTuple; tooltip: L.Popup; @@ -49,11 +50,12 @@ export class Marker { isDefined(settings.markerOffsetY) ? settings.markerOffsetY : 1, ]; - this.createMarkerIcon((iconInfo) => { - this.leafletMarker.setIcon(iconInfo.icon); - this.tooltipOffset = [0, -iconInfo.size[1] * this.markerOffset[1] + 10]; - this.updateMarkerLabel(settings); - }); + this.tooltipOffset = [ + isDefined(settings.tooltipOffsetX) ? settings.tooltipOffsetX : 0, + isDefined(settings.tooltipOffsetY) ? settings.tooltipOffsetY : -1, + ]; + + this.updateMarkerIcon(settings); if (settings.showTooltip) { this.tooltip = createTooltip(this.leafletMarker, settings, data.$datasource); @@ -111,7 +113,7 @@ export class Marker { } settings.labelText = fillPattern(this.map.markerLabelText, this.map.replaceInfoLabelMarker, this.data); this.leafletMarker.bindTooltip(`
${settings.labelText}
`, - { className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.tooltipOffset }); + { className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.labelOffset }); } } @@ -124,20 +126,22 @@ export class Marker { updateMarkerIcon(settings: MarkerSettings) { this.createMarkerIcon((iconInfo) => { this.leafletMarker.setIcon(iconInfo.icon); - this.tooltipOffset = [0, -iconInfo.size[1] * this.markerOffset[1] + 10]; + const anchor = iconInfo.icon.options.iconAnchor; + if (anchor && Array.isArray(anchor)) { + this.labelOffset = [iconInfo.size[0] / 2 - anchor[0], 10 - anchor[1]]; + } else { + this.labelOffset = [0, -iconInfo.size[1] * this.markerOffset[1] + 10]; + } this.updateMarkerLabel(settings); }); } - createMarkerIcon(onMarkerIconReady) { + private createMarkerIcon(onMarkerIconReady: MarkerIconReadyFunction) { if (this.settings.icon) { - onMarkerIconReady({ - size: [30, 30], - icon: this.settings.icon, - }); - return; + onMarkerIconReady(this.settings.icon); + return; } - const currentImage = this.settings.useMarkerImageFunction ? + const currentImage: MarkerImageInfo = this.settings.useMarkerImageFunction ? safeExecute(this.settings.markerImageFunction, [this.data, this.settings.markerImages, this.dataSources, this.data.dsIndex]) : this.settings.currentImage; let currentColor = this.settings.tinyColor; @@ -161,13 +165,21 @@ export class Marker { width = currentImage.size * aspect; height = currentImage.size; } + let iconAnchor = currentImage.markerOffset; + let popupAnchor = currentImage.tooltipOffset; + if (!iconAnchor) { + iconAnchor = [width * this.markerOffset[0], height * this.markerOffset[1]]; + } + if (!popupAnchor) { + popupAnchor = [width * this.tooltipOffset[0], height * this.tooltipOffset[1]]; + } const icon = L.icon({ iconUrl: currentImage.url, iconSize: [width, height], - iconAnchor: [width * this.markerOffset[0], height * this.markerOffset[1]], - popupAnchor: [0, -height] + iconAnchor, + popupAnchor }); - const iconInfo = { + const iconInfo: MarkerIconInfo = { size: [width, height], icon }; @@ -182,8 +194,8 @@ export class Marker { } } - createDefaultMarkerIcon(color: tinycolor.Instance, onMarkerIconReady) { - let icon: { size: number[], icon: Icon }; + createDefaultMarkerIcon(color: tinycolor.Instance, onMarkerIconReady: MarkerIconReadyFunction) { + let icon: MarkerIconInfo; if (!tinycolor.equals(color, this.settings.tinyColor)) { icon = this.createColoredMarkerIcon(color); } else { @@ -195,7 +207,7 @@ export class Marker { onMarkerIconReady(icon); } - createColoredMarkerIcon(color: tinycolor.Instance): { size: number[], icon: Icon } { + createColoredMarkerIcon(color: tinycolor.Instance): MarkerIconInfo { return { size: [21, 34], icon: L.icon({ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts index d87d764d0a..908952e7d2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts @@ -15,9 +15,15 @@ /// import L, { LatLngExpression, LeafletMouseEvent } from 'leaflet'; -import { createTooltip } from './maps-utils'; -import { functionValueCalculator, parseWithTranslation, safeExecute } from './common-maps-utils'; -import { FormattedData, PolygonSettings } from './map-models'; +import { createTooltip, isCutPolygon } from './maps-utils'; +import { + fillPattern, + functionValueCalculator, + parseWithTranslation, + processPattern, + safeExecute +} from './common-maps-utils'; +import { FormattedData, MarkerSettings, PolygonSettings } from './map-models'; export class Polygon { @@ -26,38 +32,47 @@ export class Polygon { data: FormattedData; dataSources: FormattedData[]; - constructor(public map, polyData: FormattedData, dataSources: FormattedData[], private settings: PolygonSettings, onDragendListener?) { + constructor(public map, data: FormattedData, dataSources: FormattedData[], private settings: PolygonSettings, + private onDragendListener?) { this.dataSources = dataSources; - this.data = polyData; + this.data = data; const polygonColor = this.getPolygonColor(settings); - this.leafletPoly = L.polygon(polyData[this.settings.polygonKeyName], { + const polygonStrokeColor = this.getPolygonStrokeColor(settings); + const polyData = data[this.settings.polygonKeyName]; + const polyConstructor = isCutPolygon(polyData) || polyData.length > 2 ? L.polygon : L.rectangle; + this.leafletPoly = polyConstructor(polyData, { fill: true, fillColor: polygonColor, - color: settings.polygonStrokeColor, + color: polygonStrokeColor, weight: settings.polygonStrokeWeight, fillOpacity: settings.polygonOpacity, opacity: settings.polygonStrokeOpacity, pmIgnore: !settings.editablePolygon }).addTo(this.map); - if (settings.editablePolygon && onDragendListener) { - this.leafletPoly.on('pm:edit', (e) => onDragendListener(e, this.data)); - } - + this.updateLabel(settings); if (settings.showPolygonTooltip) { - this.tooltip = createTooltip(this.leafletPoly, settings, polyData.$datasource); - this.updateTooltip(polyData); - } - if (settings.polygonClick) { - this.leafletPoly.on('click', (event: LeafletMouseEvent) => { - for (const action in this.settings.polygonClick) { - if (typeof (this.settings.polygonClick[action]) === 'function') { - this.settings.polygonClick[action](event.originalEvent, polyData.$datasource); - } - } - }); + this.tooltip = createTooltip(this.leafletPoly, settings, data.$datasource); + this.updateTooltip(data); } + this.createEventListeners(); + } + + private createEventListeners() { + if (this.settings.editablePolygon && this.onDragendListener) { + this.leafletPoly.on('pm:edit', (e) => this.onDragendListener(e, this.data)); + } + + if (this.settings.polygonClick) { + this.leafletPoly.on('click', (event: LeafletMouseEvent) => { + for (const action in this.settings.polygonClick) { + if (typeof (this.settings.polygonClick[action]) === 'function') { + this.settings.polygonClick[action](event.originalEvent, this.data.$datasource); + } + } + }); + } } updateTooltip(data: FormattedData) { @@ -67,13 +82,54 @@ export class Polygon { this.tooltip.setContent(parseWithTranslation.parseTemplate(pattern, data, true)); } + updateLabel(settings: PolygonSettings) { + this.leafletPoly.unbindTooltip(); + if (settings.showPolygonLabel) { + if (!this.map.polygonLabelText || settings.usePolygonLabelFunction) { + const pattern = settings.usePolygonLabelFunction ? + safeExecute(settings.polygonLabelFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.polygonLabel; + this.map.polygonLabelText = parseWithTranslation.prepareProcessPattern(pattern, true); + this.map.replaceInfoLabelPolygon = processPattern(this.map.polygonLabelText, this.data); + } + settings.polygonLabelText = fillPattern(this.map.polygonLabelText, this.map.replaceInfoLabelPolygon, this.data); + this.leafletPoly.bindTooltip(`
${settings.polygonLabelText}
`, + { className: 'tb-polygon-label', permanent: true, sticky: true, direction: 'center' }) + .openTooltip(this.leafletPoly.getBounds().getCenter()); + } + } + updatePolygon(data: FormattedData, dataSources: FormattedData[], settings: PolygonSettings) { this.data = data; this.dataSources = dataSources; - this.leafletPoly.setLatLngs(data[this.settings.polygonKeyName]); + const polyData = data[this.settings.polygonKeyName]; + if (isCutPolygon(polyData) || polyData.length > 2) { + if (this.leafletPoly instanceof L.Rectangle) { + this.map.removeLayer(this.leafletPoly); + const polygonColor = this.getPolygonColor(settings); + const polygonStrokeColor = this.getPolygonStrokeColor(settings); + this.leafletPoly = L.polygon(polyData, { + fill: true, + fillColor: polygonColor, + color: polygonStrokeColor, + weight: settings.polygonStrokeWeight, + fillOpacity: settings.polygonOpacity, + opacity: settings.polygonStrokeOpacity, + pmIgnore: !settings.editablePolygon + }).addTo(this.map); + } else { + this.leafletPoly.setLatLngs(polyData); + } + } else if (polyData.length === 2) { + const bounds = new L.LatLngBounds(polyData); + // @ts-ignore + this.leafletPoly.setBounds(bounds); + } if (settings.showPolygonTooltip) { this.updateTooltip(this.data); } + if (settings.showPolygonLabel) { + this.updateLabel(settings); + } this.updatePolygonColor(settings); } @@ -83,10 +139,11 @@ export class Polygon { updatePolygonColor(settings: PolygonSettings) { const polygonColor = this.getPolygonColor(settings); + const polygonStrokeColor = this.getPolygonStrokeColor(settings); const style: L.PathOptions = { fill: true, fillColor: polygonColor, - color: settings.polygonStrokeColor, + color: polygonStrokeColor, weight: settings.polygonStrokeWeight, fillOpacity: settings.polygonOpacity, opacity: settings.polygonStrokeOpacity @@ -107,4 +164,9 @@ export class Polygon { return functionValueCalculator(settings.usePolygonColorFunction, settings.polygonColorFunction, [this.data, this.dataSources, this.data.dsIndex], settings.polygonColor); } + + private getPolygonStrokeColor(settings: PolygonSettings): string | null { + return functionValueCalculator(settings.usePolygonStrokeColorFunction, settings.polygonStrokeColorFunction, + [this.data, this.dataSources, this.data.dsIndex], settings.polygonStrokeColor); + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts index ff52ece929..2924ff688e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts @@ -343,15 +343,25 @@ export const commonMapSettingsSchema = default: 'return {x: origXPos, y: origYPos};' }, markerOffsetX: { - title: 'Marker X offset relative to position', + title: 'Marker X offset relative to position multiplied by marker width', type: 'number', default: 0.5 }, markerOffsetY: { - title: 'Marker Y offset relative to position', + title: 'Marker Y offset relative to position multiplied by marker height', type: 'number', default: 1 }, + tooltipOffsetX: { + title: 'Tooltip X offset relative to marker anchor multiplied by marker width', + type: 'number', + default: 0 + }, + tooltipOffsetY: { + title: 'Tooltip Y offset relative to marker anchor multiplied by marker height', + type: 'number', + default: -1 + }, color: { title: 'Color', type: 'string' @@ -482,13 +492,15 @@ export const commonMapSettingsSchema = condition: 'model.showTooltip === true && model.useTooltipFunction === true' }, { - key: 'markerOffsetX', - condition: 'model.provider === "image-map"' + key: 'tooltipOffsetX', + condition: 'model.showTooltip === true' }, { - key: 'markerOffsetY', - condition: 'model.provider === "image-map"' + key: 'tooltipOffsetY', + condition: 'model.showTooltip === true' }, + 'markerOffsetX', + 'markerOffsetY', { key: 'posFunction', type: 'javascript', @@ -556,6 +568,25 @@ export const mapPolygonSchema = type: 'boolean', default: false }, + showPolygonLabel: { + title: 'Show polygon label', + type: 'boolean', + default: false + }, + polygonLabel: { + title: 'Polygon label (pattern examples: \'${entityName}\', \'${entityName}: (Text ${keyName} units.)\' )', + type: 'string', + default: '${entityName}' + }, + usePolygonLabelFunction: { + title: 'Use polygon label function', + type: 'boolean', + default: false + }, + polygonLabelFunction: { + title: 'Polygon label function: f(data, dsData, dsIndex)', + type: 'string' + }, polygonColor: { title: 'Polygon color', type: 'string' @@ -607,6 +638,15 @@ export const mapPolygonSchema = title: 'Polygon Color function: f(data, dsData, dsIndex)', type: 'string' }, + usePolygonStrokeColorFunction: { + title: 'Use polygon stroke color function', + type: 'boolean', + default: false + }, + polygonStrokeColorFunction: { + title: 'Polygon Stroke Color function: f(data, dsData, dsIndex)', + type: 'string' + } }, required: [] }, @@ -614,6 +654,21 @@ export const mapPolygonSchema = 'showPolygon', 'polygonKeyName', 'editablePolygon', + 'showPolygonLabel', + { + key: 'usePolygonLabelFunction', + condition: 'model.showPolygonLabel === true' + }, + { + key: 'polygonLabel', + condition: 'model.showPolygonLabel === true && model.usePolygonLabelFunction !== true' + }, + { + key: 'polygonLabelFunction', + type: 'javascript', + helpId: 'widget/lib/map/label_fn', + condition: 'model.showPolygonLabel === true && model.usePolygonLabelFunction === true' + }, { key: 'polygonColor', type: 'color' @@ -630,6 +685,13 @@ export const mapPolygonSchema = key: 'polygonStrokeColor', type: 'color' }, + 'usePolygonStrokeColorFunction', + { + key: 'polygonStrokeColorFunction', + helpId: 'widget/lib/map/polygon_color_fn', + type: 'javascript', + condition: 'model.usePolygonStrokeColorFunction === true' + }, 'polygonStrokeOpacity', 'polygonStrokeWeight', 'showPolygonTooltip', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html index 46069c15f0..b808897845 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html @@ -15,4 +15,5 @@ limitations under the License. --> - + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts index fd0197996e..1b23584a0d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, HostBinding, Input, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, HostBinding, Inject, Input, OnInit, Type } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { WidgetContext } from '@home/models/widget-component.models'; import { Store } from '@ngrx/store'; @@ -32,6 +32,8 @@ import { FormattedData } from '@home/components/widget/lib/maps/map-models'; import { hashCode, isNotEmptyStr } from '@core/utils'; import cssjs from '@core/css/css'; import { UtilsService } from '@core/services/utils.service'; +import { HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; +import { EntityDataPageLink } from '@shared/models/query/query.models'; interface MarkdownWidgetSettings { markdownTextPattern: string; @@ -62,6 +64,7 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { constructor(protected store: Store, private utils: UtilsService, + @Inject(HOME_COMPONENTS_MODULE_TOKEN) public homeComponentsModule: Type, private cd: ChangeDetectorRef) { super(store); } @@ -79,6 +82,13 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { cssParser.cssPreviewNamespace = this.markdownClass; cssParser.createStyleElement(this.markdownClass, cssString); } + const pageLink: EntityDataPageLink = { + page: 0, + pageSize: 16384, + textSearch: null, + dynamic: true + }; + this.ctx.defaultSubscription.subscribeAllForPaginatedData(pageLink, null); } public onDataUpdated() { 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 index ce0bdc4acd..6907a8afbf 100644 --- 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 @@ -37,7 +37,12 @@ type="text" (focus)="key.isFocused = true; focusInputElement($event)" (blur)="key.isFocused = false; inputChanged(source, key)"> - {{key.settings.icon}} + + {{key.settings.icon}} + + icon + + {{ getErrorMessageText(key.settings, 'required') }} @@ -57,7 +62,12 @@ max="{{key.settings.maxValue}}" (focus)="key.isFocused = true; focusInputElement($event)" (blur)="key.isFocused = false; inputChanged(source, key)"> - {{key.settings.icon}} + + {{key.settings.icon}} + + icon + + {{ getErrorMessageText(key.settings,'required') }} @@ -77,7 +87,14 @@
+ + {{key.settings.icon}} + + icon + + {{key.label}}
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 index cdf83e2e38..eaec893836 100644 --- 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 @@ -64,3 +64,17 @@ } } } + +:host ::ng-deep { + .tb-multiple-input { + .mat-slide-toggle-content { + width: 100%; + display: flex; + align-items: center; + + .mat-icon { + margin-right: 8px; + } + } + } +} 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 index 016e04392e..cac977dd4c 100644 --- 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 @@ -24,7 +24,14 @@ 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 { createLabelFromDatasource, isDefinedAndNotNull, isEqual, isNotEmptyStr, isUndefined } from '@core/utils'; +import { + createLabelFromDatasource, + isBoolean, isDefined, + isDefinedAndNotNull, + isEqual, + isNotEmptyStr, + isUndefined +} from '@core/utils'; import { EntityType } from '@shared/models/entity-type.models'; import * as _moment from 'moment'; import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; @@ -35,6 +42,7 @@ import { forkJoin, Observable, Subject } from 'rxjs'; import { EntityId } from '@shared/models/id/entity-id'; import { ResizeObserver } from '@juggle/resize-observer'; import { takeUntil } from 'rxjs/operators'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; type FieldAlignment = 'row' | 'column'; @@ -44,6 +52,9 @@ type MultipleInputWidgetDataKeyValueType = 'string' | 'double' | 'integer' | 'dateTime' | 'date' | 'time' | 'select'; type MultipleInputWidgetDataKeyEditableType = 'editable' | 'disabled' | 'readonly'; +type ConvertGetValueFunction = (value: any, ctx: WidgetContext) => any; +type ConvertSetValueFunction = (value: any, originValue: any, ctx: WidgetContext) => any; + interface MultipleInputWidgetSettings { widgetTitle: string; showActionButtons: boolean; @@ -66,6 +77,7 @@ interface MultipleInputWidgetSelectOption { interface MultipleInputWidgetDataKeySettings { dataKeyType: MultipleInputWidgetDataKeyType; dataKeyValueType: MultipleInputWidgetDataKeyValueType; + slideToggleLabelPosition?: 'after' | 'before'; selectOptions: MultipleInputWidgetSelectOption[]; required: boolean; isEditable: MultipleInputWidgetDataKeyEditableType; @@ -78,10 +90,19 @@ interface MultipleInputWidgetDataKeySettings { invalidDateErrorMessage?: string; minValueErrorMessage?: string; maxValueErrorMessage?: string; + useCustomIcon: boolean; icon: string; + customIcon: string ; + safeCustomIcon?: SafeUrl; inputTypeNumber?: boolean; readOnly?: boolean; disabledOnCondition?: boolean; + useGetValueFunction?: boolean; + getValueFunctionBody?: string; + getValueFunction?: ConvertGetValueFunction; + useSetValueFunction?: boolean; + setValueFunctionBody?: string; + setValueFunction?: ConvertSetValueFunction; } interface MultipleInputWidgetDataKey extends DataKey { @@ -139,7 +160,8 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni private utils: UtilsService, private fb: FormBuilder, private attributeService: AttributeService, - private translate: TranslateService) { + private translate: TranslateService, + private sanitizer: DomSanitizer) { super(store); } @@ -265,6 +287,34 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni }); } + if (dataKey.settings.dataKeyValueType === 'booleanSwitch' && isUndefined(dataKey.settings.slideToggleLabelPosition)) { + dataKey.settings.slideToggleLabelPosition = 'after'; + } + + if (dataKey.settings.useCustomIcon && isDefinedAndNotNull(dataKey.settings.customIcon)) { + dataKey.settings.safeCustomIcon = this.sanitizer.bypassSecurityTrustUrl(dataKey.settings.customIcon); + } + + if (dataKey.settings.useGetValueFunction && dataKey.settings.getValueFunctionBody.length) { + try { + dataKey.settings.getValueFunction = + new Function('value, ctx', dataKey.settings.getValueFunctionBody) as ConvertGetValueFunction; + } catch (e) { + console.warn(`Parse getValue function in key ${dataKey.label}`, e); + dataKey.settings.getValueFunction = null; + } + } + + if (dataKey.settings.useSetValueFunction && dataKey.settings.setValueFunctionBody.length) { + try { + dataKey.settings.setValueFunction = + new Function('value, originValue, ctx', dataKey.settings.setValueFunctionBody) as ConvertSetValueFunction; + } catch (e) { + console.warn(`Parse setValue function in key ${dataKey.label}`, e); + dataKey.settings.setValueFunction = null; + } + } + source.keys.push(dataKey); }); } else { @@ -342,28 +392,41 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni source.keys.forEach((key) => { const keyData = data[dataIndex].data; if (keyData && keyData.length) { - let value; + let value = keyData[0][1]; + const keyValue = this.getKeyValue(value, key.settings); switch (key.settings.dataKeyValueType) { case 'dateTime': case 'date': - if (isDefinedAndNotNull(keyData[0][1]) && keyData[0][1] !== '') { - value = _moment(keyData[0][1]).toDate(); + if (isDefinedAndNotNull(keyValue) && keyValue !== '') { + if (keyValue instanceof Date) { + value = keyValue; + } else { + value = _moment(keyValue).toDate(); + } } else { value = null; } break; case 'time': - value = _moment().startOf('day').add(keyData[0][1], 'ms').toDate(); + if (keyValue instanceof Date) { + value = keyValue; + } else { + value = _moment().startOf('day').add(keyValue, 'ms').toDate(); + } break; case 'booleanCheckbox': case 'booleanSwitch': - value = (keyData[0][1] === 'true'); + if (isBoolean(keyValue)) { + value = keyValue; + } else { + value = (keyValue === 'true'); + } break; case 'select': - value = keyData[0][1].toString(); + value = keyValue.toString(); break; default: - value = keyData[0][1]; + value = keyValue; } key.value = value; } @@ -399,6 +462,18 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni }); } + private getKeyValue(data: any, keySetting: MultipleInputWidgetDataKeySettings) { + if (isDefined(keySetting.getValueFunction)) { + try { + return keySetting.getValueFunction(data, this.ctx); + } catch (e) { + console.warn(`Call function getValue`, e); + return data; + } + } + return data; + } + private updateWidgetDisplaying() { this.changeAlignment = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 620); } @@ -493,7 +568,8 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni if (!this.settings.showActionButtons && !this.isSavingInProgress) { this.isSavingInProgress = true; const currentValue = this.multipleInputFormGroup.get(key.formId).value; - if (!key.settings.required || (key.settings.required && isDefinedAndNotNull(currentValue) && isNotEmptyStr(currentValue.toString()))) { + if (!key.settings.required || + (key.settings.required && isDefinedAndNotNull(currentValue) && isNotEmptyStr(currentValue.toString()))) { const dataToSave: MultipleInputWidgetSource = { datasource: source.datasource, keys: [key] @@ -523,8 +599,9 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni const sharedAttributes: AttributeData[] = []; const telemetry: AttributeData[] = []; for (const key of toSave.keys) { - const currentValue = key.settings.dataKeyHidden ? key.value : this.multipleInputFormGroup.get(key.formId).value; + let currentValue = key.settings.dataKeyHidden ? key.value : this.multipleInputFormGroup.get(key.formId).value; if (!isEqual(currentValue, key.value) || this.settings.updateAllValues) { + currentValue = this.setKeyValue(currentValue, key, toSave); const attribute: AttributeData = { key: key.name, value: null @@ -615,6 +692,21 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni } } + private setKeyValue(value: any, key: MultipleInputWidgetDataKey, source: MultipleInputWidgetSource) { + if (isDefined(key.settings.setValueFunction)) { + const currentDatasourceData = this.subscription.data + .find(dsData => dsData.datasource === source.datasource && dsData.dataKey.name === key.name); + const originValue = currentDatasourceData.data[0][1]; + try { + return key.settings.setValueFunction(value, originValue, this.ctx); + } catch (e) { + console.warn(`Call function setValue`, e); + return value; + } + } + return value; + } + public discardAll() { this.multipleInputFormGroup.reset(undefined, {emitEvent: false}); this.sources.forEach((source) => { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index 90b3087ced..7dcac82833 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -45,6 +45,7 @@ import { MODULES_MAP } from '@shared/public-api'; import * as tinycolor_ from 'tinycolor2'; import moment from 'moment'; import { IModulesMap } from '@modules/common/modules-map.models'; +import { HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; const tinycolor = tinycolor_; @@ -66,6 +67,7 @@ export class WidgetComponentService { constructor(@Inject(WINDOW) private window: Window, @Optional() @Inject(MODULES_MAP) private modulesMap: IModulesMap, + @Inject(HOME_COMPONENTS_MODULE_TOKEN) private homeComponentsModule: Type, private dynamicComponentFactoryService: DynamicComponentFactoryService, private widgetService: WidgetService, private utils: UtilsService, @@ -177,8 +179,10 @@ export class WidgetComponentService { forkJoin(widgetModulesTasks).subscribe( () => { const loadDefaultWidgetInfoTasks = [ - this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type', [SharedModule, WidgetComponentsModule]), - this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type', [SharedModule, WidgetComponentsModule]), + this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type', + [SharedModule, WidgetComponentsModule, this.homeComponentsModule]), + this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type', + [SharedModule, WidgetComponentsModule, this.homeComponentsModule]), ]; forkJoin(loadDefaultWidgetInfoTasks).subscribe( () => { @@ -274,7 +278,7 @@ export class WidgetComponentService { } if (widgetControllerDescriptor) { const widgetNamespace = `widget-type-${(isSystem ? 'sys-' : '')}${bundleAlias}-${widgetInfo.alias}`; - this.loadWidgetResources(widgetInfo, widgetNamespace, [SharedModule, WidgetComponentsModule]).subscribe( + this.loadWidgetResources(widgetInfo, widgetNamespace, [SharedModule, WidgetComponentsModule, this.homeComponentsModule]).subscribe( () => { if (widgetControllerDescriptor.settingsSchema) { widgetInfo.typeSettingsSchema = widgetControllerDescriptor.settingsSchema; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 97542fd3cb..1073b32e42 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -418,6 +418,10 @@ label="{{ 'widget-config.widget-style' | translate }}" formControlName="widgetStyle" > +
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss index c29afa5629..e6fd1a509e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss @@ -133,7 +133,7 @@ .mat-expansion-panel-body{ padding: 0; } - .tb-json-object-panel { + .tb-json-object-panel, .tb-css-content-panel { margin: 0 0 8px; } .mat-checkbox-layout { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 7d779a3a6c..29af888e37 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -206,6 +206,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont padding: [null, []], margin: [null, []], widgetStyle: [null, []], + widgetCss: [null, []], titleStyle: [null, []], units: [null, []], decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]], @@ -406,6 +407,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont padding: config.padding, margin: config.margin, widgetStyle: isDefined(config.widgetStyle) ? config.widgetStyle : {}, + widgetCss: isDefined(config.widgetCss) ? config.widgetCss : '', titleStyle: isDefined(config.titleStyle) ? config.titleStyle : { fontSize: '16px', fontWeight: 400 diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html index 09f3199b06..3a2712f4f6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
= new EventEmitter(); + private cssClass: string; + constructor(protected store: Store, - private cd: ChangeDetectorRef) { + private cd: ChangeDetectorRef, + private renderer: Renderer2, + @Inject(DOCUMENT) private document: Document) { super(store); } ngOnInit(): void { this.widget.widgetContext.containerChangeDetector = this.cd; + const cssString = this.widget.widget.config.widgetCss; + if (isNotEmptyStr(cssString)) { + const cssParser = new cssjs(); + cssParser.testMode = false; + this.cssClass = 'tb-widget-css-' + guid(); + this.renderer.addClass(this.gridsterItem.el, this.cssClass); + cssParser.cssPreviewNamespace = this.cssClass; + cssParser.createStyleElement(this.cssClass, cssString); + } + } + + ngOnDestroy(): void { + if (this.cssClass) { + const el = this.document.getElementById(this.cssClass); + if (el) { + el.parentNode.removeChild(el); + } + } } isHighlighted(widget: DashboardWidget) { @@ -105,6 +141,11 @@ export class WidgetContainerComponent extends PageComponent implements OnInit { } onFullscreenChanged(expanded: boolean) { + if (expanded) { + this.renderer.addClass(this.tbWidgetElement.nativeElement, this.cssClass); + } else { + this.renderer.removeClass(this.tbWidgetElement.nativeElement, this.cssClass); + } this.widgetFullscreenChanged.emit(expanded); } 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 c1208d0241..e85d507e9b 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 @@ -65,7 +65,7 @@ import { validateEntityId } from '@core/utils'; import { - IDynamicWidgetComponent, ShowWidgetHeaderActionFunction, + IDynamicWidgetComponent, ShowWidgetHeaderActionFunction, updateEntityParams, WidgetContext, WidgetHeaderAction, WidgetInfo, @@ -1070,7 +1070,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI case WidgetActionType.updateDashboardState: let targetDashboardStateId = descriptor.targetDashboardStateId; const params = deepClone(this.widgetContext.stateController.getStateParams()); - this.updateEntityParams(params, targetEntityParamName, targetEntityId, entityName, entityLabel); + updateEntityParams(params, targetEntityParamName, targetEntityId, entityName, entityLabel); if (type === WidgetActionType.openDashboardState) { if (descriptor.openInPopover) { this.openDashboardStateInPopover($event, descriptor.targetDashboardStateId, params, @@ -1091,7 +1091,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI targetDashboardStateId = descriptor.targetDashboardStateId; const stateObject: StateObject = {}; stateObject.params = {}; - this.updateEntityParams(stateObject.params, targetEntityParamName, targetEntityId, entityName, entityLabel); + updateEntityParams(stateObject.params, targetEntityParamName, targetEntityId, entityName, entityLabel); if (targetDashboardStateId) { stateObject.id = targetDashboardStateId; } @@ -1360,7 +1360,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.widgetContentContainer, this.dashboardPageComponent, preferredPlacement, hideOnClickOutside, injector, { - embed: true, + embedded: true, syncStateWithQueryParam: false, hideToolbar: hideDashboardToolbar, currentState: objToBase64([stateObject]), @@ -1443,30 +1443,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } } - private updateEntityParams(params: StateParams, targetEntityParamName?: string, targetEntityId?: EntityId, - entityName?: string, entityLabel?: string) { - if (targetEntityId) { - let targetEntityParams: StateParams; - if (targetEntityParamName && targetEntityParamName.length) { - targetEntityParams = params[targetEntityParamName]; - if (!targetEntityParams) { - targetEntityParams = {}; - params[targetEntityParamName] = targetEntityParams; - params.targetEntityParamName = targetEntityParamName; - } - } else { - targetEntityParams = params; - } - targetEntityParams.entityId = targetEntityId; - if (entityName) { - targetEntityParams.entityName = entityName; - } - if (entityLabel) { - targetEntityParams.entityLabel = entityLabel; - } - } - } - private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array): Observable { if (isDefined(customCss) && customCss.length > 0) { this.cssParser.cssPreviewNamespace = actionNamespace; diff --git a/ui-ngx/src/app/modules/home/models/datasource/attribute-datasource.ts b/ui-ngx/src/app/modules/home/models/datasource/attribute-datasource.ts index 89a0c8f5b2..9b5cbd1598 100644 --- a/ui-ngx/src/app/modules/home/models/datasource/attribute-datasource.ts +++ b/ui-ngx/src/app/modules/home/models/datasource/attribute-datasource.ts @@ -88,7 +88,10 @@ export class AttributeDatasource implements DataSource { fetchAttributes(entityId: EntityId, attributesScope: TelemetryType, pageLink: PageLink): Observable> { return this.getAllAttributes(entityId, attributesScope).pipe( - map((data) => pageLink.filterData(data)) + map((data) => { + const filteredData = data.filter(attrData => attrData.lastUpdateTs !== 0 && attrData.value !== null); + return pageLink.filterData(filteredData); + }) ); } 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 2a530c8b4e..402e414f02 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 @@ -37,7 +37,7 @@ import { IStateController, IWidgetSubscription, IWidgetUtils, - RpcApi, + RpcApi, StateParams, SubscriptionEntityInfo, TimewindowFunctions, WidgetActionsApi, @@ -81,6 +81,7 @@ import { Router } from '@angular/router'; import { catchError, map, mergeMap, switchMap } from 'rxjs/operators'; import { FormattedData } from '@home/components/widget/lib/maps/map-models'; import { TbPopoverComponent } from '@shared/components/popover.component'; +import { EntityId } from '@shared/models/id/entity-id'; export interface IWidgetAction { name: string; @@ -545,3 +546,27 @@ export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: descriptor }; } + +export function updateEntityParams(params: StateParams, targetEntityParamName?: string, targetEntityId?: EntityId, + entityName?: string, entityLabel?: string) { + if (targetEntityId) { + let targetEntityParams: StateParams; + if (targetEntityParamName && targetEntityParamName.length) { + targetEntityParams = params[targetEntityParamName]; + if (!targetEntityParams) { + targetEntityParams = {}; + params[targetEntityParamName] = targetEntityParams; + params.targetEntityParamName = targetEntityParamName; + } + } else { + targetEntityParams = params; + } + targetEntityParams.entityId = targetEntityId; + if (entityName) { + targetEntityParams.entityName = entityName; + } + if (entityLabel) { + targetEntityParams.entityLabel = entityLabel; + } + } +} diff --git a/ui-ngx/src/app/shared/components/css.component.html b/ui-ngx/src/app/shared/components/css.component.html new file mode 100644 index 0000000000..33d19672c3 --- /dev/null +++ b/ui-ngx/src/app/shared/components/css.component.html @@ -0,0 +1,41 @@ + +
+
+ + + +
+
+ +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/shared/components/css.component.scss b/ui-ngx/src/app/shared/components/css.component.scss new file mode 100644 index 0000000000..dd045bbd2d --- /dev/null +++ b/ui-ngx/src/app/shared/components/css.component.scss @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2021 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. + */ +.tb-css { + position: relative; + + &.tb-disabled { + color: rgba(0, 0, 0, .38); + } + + &.fill-height { + height: 100%; + } + + .tb-css-content-panel { + height: calc(100% - 80px); + margin-left: 15px; + border: 1px solid #c0c0c0; + + #tb-css-input { + width: 100%; + min-width: 200px; + height: 100%; + + &:not(.fill-height) { + min-height: 200px; + } + } + } + + .tb-css-toolbar { + & > * { + &:not(:last-child) { + margin-right: 4px; + } + } + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { + background: rgba(220, 220, 220, .35); + align-items: center; + vertical-align: middle; + min-width: 32px; + min-height: 15px; + padding: 4px; + font-size: .8rem; + line-height: 15px; + &:not(.tb-help-popup-button) { + color: #7b7b7b; + } + } + .tb-help-popup-button-loading { + background: #f3f3f3; + } + } +} diff --git a/ui-ngx/src/app/shared/components/css.component.ts b/ui-ngx/src/app/shared/components/css.component.ts new file mode 100644 index 0000000000..c1367d04ee --- /dev/null +++ b/ui-ngx/src/app/shared/components/css.component.ts @@ -0,0 +1,207 @@ +/// +/// Copyright © 2016-2021 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, + forwardRef, + Input, + OnDestroy, + OnInit, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; +import { Ace } from 'ace-builds'; +import { getAce } from '@shared/models/ace/ace.models'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { beautifyCss } from '@shared/models/beautify.models'; + +@Component({ + selector: 'tb-css', + templateUrl: './css.component.html', + styleUrls: ['./css.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CssComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CssComponent), + multi: true, + } + ], + encapsulation: ViewEncapsulation.None +}) +export class CssComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator { + + @ViewChild('cssEditor', {static: true}) + cssEditorElmRef: ElementRef; + + private cssEditor: Ace.Editor; + private editorsResizeCaf: CancelAnimationFrame; + private editorResize$: ResizeObserver; + private ignoreChange = false; + + @Input() label: string; + + @Input() disabled: boolean; + + @Input() fillHeight: boolean; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + fullscreen = false; + + modelValue: string; + + hasErrors = false; + + private propagateChange = null; + + constructor(public elementRef: ElementRef, + private utils: UtilsService, + private translate: TranslateService, + protected store: Store, + private raf: RafService) { + } + + ngOnInit(): void { + const editorElement = this.cssEditorElmRef.nativeElement; + let editorOptions: Partial = { + mode: 'ace/mode/css', + showGutter: true, + showPrintMargin: true, + readOnly: this.disabled + }; + + const advancedOptions = { + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }; + + editorOptions = {...editorOptions, ...advancedOptions}; + getAce().subscribe( + (ace) => { + this.cssEditor = ace.edit(editorElement, editorOptions); + this.cssEditor.session.setUseWrapMode(true); + this.cssEditor.setValue(this.modelValue ? this.modelValue : '', -1); + this.cssEditor.setReadOnly(this.disabled); + this.cssEditor.on('change', () => { + if (!this.ignoreChange) { + this.updateView(); + } + }); + // @ts-ignore + this.cssEditor.session.on('changeAnnotation', () => { + const annotations = this.cssEditor.session.getAnnotations(); + const hasErrors = annotations.filter(annotation => annotation.type === 'error').length > 0; + if (this.hasErrors !== hasErrors) { + this.hasErrors = hasErrors; + this.propagateChange(this.modelValue); + } + }); + this.editorResize$ = new ResizeObserver(() => { + this.onAceEditorResize(); + }); + this.editorResize$.observe(editorElement); + } + ); + } + + ngOnDestroy(): void { + if (this.editorResize$) { + this.editorResize$.disconnect(); + } + } + + private onAceEditorResize() { + if (this.editorsResizeCaf) { + this.editorsResizeCaf(); + this.editorsResizeCaf = null; + } + this.editorsResizeCaf = this.raf.raf(() => { + this.cssEditor.resize(); + this.cssEditor.renderer.updateFull(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.cssEditor) { + this.cssEditor.setReadOnly(this.disabled); + } + } + + public validate(c: FormControl) { + return (!this.hasErrors) ? null : { + css: { + valid: false, + }, + }; + } + + beautifyCss() { + beautifyCss(this.modelValue, {indent_size: 4}).subscribe( + (res) => { + if (this.modelValue !== res) { + this.cssEditor.setValue(res ? res : '', -1); + this.updateView(); + } + } + ); + } + + writeValue(value: string): void { + this.modelValue = value; + if (this.cssEditor) { + this.ignoreChange = true; + this.cssEditor.setValue(this.modelValue ? this.modelValue : '', -1); + this.ignoreChange = false; + } + } + + updateView() { + const editorValue = this.cssEditor.getValue(); + if (this.modelValue !== editorValue) { + this.modelValue = editorValue; + this.propagateChange(this.modelValue); + } + } +} diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-base-component.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-base-component.tsx index ce1f570f7f..f0f5456191 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-base-component.tsx +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-base-component.tsx @@ -16,6 +16,7 @@ import * as React from 'react'; import JsonFormUtils from './json-form-utils'; import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; +import { isDefinedAndNotNull } from '@core/utils'; export default ThingsboardBaseComponent => class

extends React.Component { @@ -67,10 +68,10 @@ export default ThingsboardBaseComponent => class

defaultValue() { let value = JsonFormUtils.selectOrSet(this.props.form.key, this.props.model); if (this.props.form.schema.type === 'boolean') { - if (typeof value !== 'boolean' && this.props.form.default) { + if (typeof value !== 'boolean' && typeof this.props.form.default === 'boolean') { value = this.props.form.default; } - if (typeof value !== 'boolean' && this.props.form.schema && this.props.form.schema.default) { + if (typeof value !== 'boolean' && this.props.form.schema && typeof this.props.form.schema.default === 'boolean') { value = this.props.form.schema.default; } if (typeof value !== 'boolean' && @@ -79,13 +80,13 @@ export default ThingsboardBaseComponent => class

value = false; } } else if (this.props.form.schema.type === 'integer' || this.props.form.schema.type === 'number') { - if (typeof value !== 'number' && this.props.form.default) { + if (typeof value !== 'number' && typeof this.props.form.default === 'number') { value = this.props.form.default; } - if (typeof value !== 'number' && this.props.form.schema && this.props.form.schema.default) { + if (typeof value !== 'number' && this.props.form.schema && typeof this.props.form.schema.default === 'number') { value = this.props.form.schema.default; } - if (typeof value !== 'number' && this.props.form.titleMap && this.props.form.titleMap[0].value) { + if (typeof value !== 'number' && this.props.form.titleMap && typeof this.props.form.titleMap[0].value === 'number') { value = this.props.form.titleMap[0].value; } if (value && typeof value === 'string') { @@ -96,13 +97,13 @@ export default ThingsboardBaseComponent => class

} } } else { - if (!value && this.props.form.default) { + if (!value && isDefinedAndNotNull(this.props.form.default)) { value = this.props.form.default; } - if (!value && this.props.form.schema && this.props.form.schema.default) { + if (!value && this.props.form.schema && isDefinedAndNotNull(this.props.form.schema.default)) { value = this.props.form.schema.default; } - if (!value && this.props.form.titleMap && this.props.form.titleMap[0].value) { + if (!value && this.props.form.titleMap && isDefinedAndNotNull(this.props.form.titleMap[0].value)) { value = this.props.form.titleMap[0].value; } } diff --git a/ui-ngx/src/app/shared/components/markdown.component.ts b/ui-ngx/src/app/shared/components/markdown.component.ts index 1abfc5b9e6..29933cba21 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.ts +++ b/ui-ngx/src/app/shared/components/markdown.component.ts @@ -47,6 +47,10 @@ export class TbMarkdownComponent implements OnChanges { @Input() data: string | undefined; + @Input() context: any; + + @Input() additionalCompileModules: Type[]; + @Input() markdownClass: string | undefined; @Input() style: { [klass: string]: any } = {}; @@ -94,6 +98,10 @@ export class TbMarkdownComponent implements OnChanges { this.markdownContainer.clear(); const parent = this; let readyObservable: Observable; + let compileModules = [this.sharedModule]; + if (this.additionalCompileModules) { + compileModules = compileModules.concat(this.additionalCompileModules); + } this.dynamicComponentFactoryService.createDynamicComponentFactory( class TbMarkdownInstance { ngOnDestroy(): void { @@ -101,7 +109,7 @@ export class TbMarkdownComponent implements OnChanges { } }, template, - [this.sharedModule], + compileModules, true ).subscribe((factory) => { this.tbMarkdownInstanceComponentFactory = factory; @@ -109,6 +117,11 @@ export class TbMarkdownComponent implements OnChanges { try { this.tbMarkdownInstanceComponentRef = this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentFactory, 0, injector); + if (this.context) { + for (const propName of Object.keys(this.context)) { + this.tbMarkdownInstanceComponentRef.instance[propName] = this.context[propName]; + } + } this.tbMarkdownInstanceComponentRef.instance.style = this.style; this.handlePlugins(this.tbMarkdownInstanceComponentRef.location.nativeElement); this.markdownService.highlight(this.tbMarkdownInstanceComponentRef.location.nativeElement); diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index db20056fde..f00e0e3878 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -97,6 +97,7 @@ export interface DashboardSettings { toolbarAlwaysOpen?: boolean; hideToolbar?: boolean; titleColor?: string; + dashboardCss?: string; } export interface DashboardConfiguration { diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 0b0353c7a9..8e8aed3e53 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -506,6 +506,7 @@ export interface WidgetConfig { padding?: string; margin?: string; widgetStyle?: {[klass: string]: any}; + widgetCss?: string; titleStyle?: {[klass: string]: any}; units?: string; decimals?: number; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 1f66aedf47..086768c854 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -156,6 +156,7 @@ import { TbPopoverService } from '@shared/components/popover.service'; import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/components/tokens'; import { TbMarkdownComponent } from '@shared/components/markdown.component'; import { ProtobufContentComponent } from '@shared/components/protobuf-content.component'; +import { CssComponent } from '@shared/components/css.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -233,6 +234,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) JsonObjectEditComponent, JsonContentComponent, JsFuncComponent, + CssComponent, FabTriggerDirective, FabActionsDirective, FabToolbarComponent, @@ -378,6 +380,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) JsonObjectEditComponent, JsonContentComponent, JsFuncComponent, + CssComponent, FabTriggerDirective, FabActionsDirective, FabToolbarComponent, 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 8567fbd629..092f944ece 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -811,6 +811,8 @@ "dashboard-logo-settings": "Dashboard logo settings", "display-dashboard-logo": "Display logo in dashboard fullscreen mode", "dashboard-logo-image": "Dashboard logo image", + "advanced-settings": "Advanced settings", + "dashboard-css": "Dashboard CSS", "import": "Import dashboard", "export": "Export dashboard", "export-failed-error": "Unable to export dashboard: {{error}}", @@ -3085,6 +3087,7 @@ "padding": "Padding", "margin": "Margin", "widget-style": "Widget style", + "widget-css": "Widget CSS", "title-style": "Title style", "mobile-mode-settings": "Mobile mode", "order": "Order",