diff --git a/application/src/main/data/json/system/scada_symbols/dynamic-horizontal-scale-hp.svg b/application/src/main/data/json/system/scada_symbols/dynamic-horizontal-scale-hp.svg
index 17d2a21ed9..6a060c4e63 100644
--- a/application/src/main/data/json/system/scada_symbols/dynamic-horizontal-scale-hp.svg
+++ b/application/src/main/data/json/system/scada_symbols/dynamic-horizontal-scale-hp.svg
@@ -1,4 +1,5 @@
-
\ No newline at end of file
+
+
+
+
+
+
+
diff --git a/application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg b/application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg
index db5c5736a7..1310ee78b6 100644
--- a/application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg
+++ b/application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg
@@ -1,4 +1,5 @@
-
\ No newline at end of file
+
+
+
diff --git a/application/src/main/data/json/system/scada_symbols/filter-hp.svg b/application/src/main/data/json/system/scada_symbols/filter-hp.svg
index c101b6f65d..3bec273373 100644
--- a/application/src/main/data/json/system/scada_symbols/filter-hp.svg
+++ b/application/src/main/data/json/system/scada_symbols/filter-hp.svg
@@ -1,4 +1,5 @@
-
\ No newline at end of file
+
diff --git a/application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg b/application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg
index b4959787dd..1c663c9b29 100644
--- a/application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg
+++ b/application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg
@@ -1,5 +1,6 @@
-
\ No newline at end of file
+
diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg b/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg
index 53b0f7cc8a..232159f2a5 100644
--- a/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg
+++ b/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg
@@ -1,5 +1,6 @@
-
\ No newline at end of file
+
diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-valve-hp.svg b/application/src/main/data/json/system/scada_symbols/horizontal-valve-hp.svg
index c8aa016020..c032a4b957 100644
--- a/application/src/main/data/json/system/scada_symbols/horizontal-valve-hp.svg
+++ b/application/src/main/data/json/system/scada_symbols/horizontal-valve-hp.svg
@@ -1,4 +1,5 @@
-
\ No newline at end of file
+
diff --git a/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg
index 0e48099e89..4376bb2348 100644
--- a/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg
+++ b/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg
@@ -1,5 +1,6 @@
-
\ No newline at end of file
+
+
+
diff --git a/application/src/main/data/json/system/scada_symbols/pool-hp.svg b/application/src/main/data/json/system/scada_symbols/pool-hp.svg
index 83fb19b868..d6aed15dbf 100644
--- a/application/src/main/data/json/system/scada_symbols/pool-hp.svg
+++ b/application/src/main/data/json/system/scada_symbols/pool-hp.svg
@@ -1,7 +1,6 @@
-
diff --git a/application/src/main/data/json/system/scada_symbols/sand-filter.svg b/application/src/main/data/json/system/scada_symbols/sand-filter.svg
index 59aa228a6f..243d5ed6e8 100644
--- a/application/src/main/data/json/system/scada_symbols/sand-filter.svg
+++ b/application/src/main/data/json/system/scada_symbols/sand-filter.svg
@@ -1,5 +1,5 @@
-
-
+
+}]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- Filter
+ Filter
- Backwash
+ Backwash
- Rinse
+ Rinse
- Waste
+ Waste
- Recirculate
+ Recirculate
- Closed
+ Closed
-
+
+
@@ -646,4 +701,4 @@
-
\ No newline at end of file
+
diff --git a/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg b/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg
index 1e75c10e27..3d32e49ab8 100644
--- a/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg
+++ b/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg
@@ -1,5 +1,6 @@
-
+ t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(182, minorY, 202, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
+ "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 594 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(170, y, 202, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 160, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 160, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 160, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(182, minorY, 202, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null
},
{
@@ -454,83 +455,90 @@
}
]
}]]>
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
+
+
+
+
-
+
+
+
-
+
+
-
\ No newline at end of file
+
diff --git a/application/src/main/data/json/system/scada_symbols/simple-horizontal-scale-hp.svg b/application/src/main/data/json/system/scada_symbols/simple-horizontal-scale-hp.svg
index 0a2afd9519..1052d1361c 100644
--- a/application/src/main/data/json/system/scada_symbols/simple-horizontal-scale-hp.svg
+++ b/application/src/main/data/json/system/scada_symbols/simple-horizontal-scale-hp.svg
@@ -1,4 +1,5 @@
-
+
-Outdoor°C
- 100
- 0
-
-
- 26
-
+ Outdoor
+ °C
+
+ 100
+ 0
+
+
+
+ 26
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
+
+
diff --git a/application/src/main/data/json/system/scada_symbols/vertical-scale-hp.svg b/application/src/main/data/json/system/scada_symbols/vertical-scale-hp.svg
index 82fc59bae2..f1e1f2f178 100644
--- a/application/src/main/data/json/system/scada_symbols/vertical-scale-hp.svg
+++ b/application/src/main/data/json/system/scada_symbols/vertical-scale-hp.svg
@@ -1,4 +1,5 @@
-
+
-Outdoor°C
+ Outdoor
+ °C
+
@@ -738,14 +741,18 @@
-
- 100
- 0
-
+
+
+ 100
+ 0
+
+
26
-
+
+
-
-
\ No newline at end of file
+
+
+
diff --git a/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg b/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg
index 4749dc5ce2..33148d66a5 100644
--- a/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg
+++ b/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg
@@ -1,5 +1,6 @@
-
+
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
+
+
+
+
-
+
+
+
-
+
+
-
\ No newline at end of file
+
diff --git a/application/src/main/data/json/system/scada_symbols/vertical-valve-hp.svg b/application/src/main/data/json/system/scada_symbols/vertical-valve-hp.svg
index 41ab891821..370bcb38ba 100644
--- a/application/src/main/data/json/system/scada_symbols/vertical-valve-hp.svg
+++ b/application/src/main/data/json/system/scada_symbols/vertical-valve-hp.svg
@@ -1,4 +1,5 @@
-
+ {\n ctx.api.setValue('opened', !opened);\n }\n});"
}
}
},
@@ -76,6 +77,74 @@
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
+ {
+ "id": "open",
+ "name": "{i18n:scada.symbol.open}",
+ "hint": "{i18n:scada.symbol.open-hint}",
+ "group": null,
+ "type": "action",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": {
+ "action": "SET_ATTRIBUTE",
+ "executeRpc": {
+ "method": "setState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "setAttribute": {
+ "scope": "SHARED_SCOPE",
+ "key": "open"
+ },
+ "putTimeSeries": {
+ "key": "state"
+ },
+ "valueToData": {
+ "type": "CONSTANT",
+ "constantValue": true,
+ "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;"
+ }
+ },
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "close",
+ "name": "{i18n:scada.symbol.close}",
+ "hint": "{i18n:scada.symbol.close-hint}",
+ "group": null,
+ "type": "action",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": {
+ "action": "SET_ATTRIBUTE",
+ "executeRpc": {
+ "method": "setState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "setAttribute": {
+ "scope": "SHARED_SCOPE",
+ "key": "open"
+ },
+ "putTimeSeries": {
+ "key": "state"
+ },
+ "valueToData": {
+ "type": "CONSTANT",
+ "constantValue": false,
+ "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;"
+ }
+ },
+ "defaultWidgetActionSettings": null
+ },
{
"id": "warning",
"name": "{i18n:scada.symbol.warning-state}",
@@ -220,26 +289,6 @@
"setEntityId": false,
"stateEntityParamName": null
}
- },
- {
- "id": "click",
- "name": "{i18n:scada.symbol.on-click}",
- "hint": "{i18n:scada.symbol.on-click-hint}",
- "group": null,
- "type": "widgetAction",
- "valueType": "BOOLEAN",
- "trueLabel": null,
- "falseLabel": null,
- "stateLabel": null,
- "defaultGetValueSettings": null,
- "defaultSetValueSettings": null,
- "defaultWidgetActionSettings": {
- "type": "doNothing",
- "targetDashboardStateId": null,
- "openRightLayout": false,
- "setEntityId": false,
- "stateEntityParamName": null
- }
}
],
"properties": [
@@ -309,20 +358,26 @@
}
]
}]]>
-
-
-
+
+
+
+
-
+
+
+
+
+
-
+
+
-
\ No newline at end of file
+
diff --git a/application/src/main/data/json/system/widget_types/update_location_timeseries.json b/application/src/main/data/json/system/widget_types/update_location_timeseries.json
index 03d38ece27..9724e8e6b6 100644
--- a/application/src/main/data/json/system/widget_types/update_location_timeseries.json
+++ b/application/src/main/data/json/system/widget_types/update_location_timeseries.json
@@ -9,7 +9,7 @@
"sizeX": 7.5,
"sizeY": 3,
"resources": [],
- "templateHtml": "
",
+ "templateHtml": "",
"templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-button.getLocation {\n margin-right: 10px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form mat-form-field{\n width: 100%;\n padding-right: 5px;\n}\n\n.attribute-update-form.small-width mat-form-field{\n width: 150px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}",
"controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\n\r\nfunction init() {\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n \r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.showGetLocation = utils.defaultValue(settings.showGetLocation, true);\r\n settings.enableHighAccuracy = utils.defaultValue(settings.enableHighAccuracy, false);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false; \r\n\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row');\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || translate.instant('widgets.input-widgets.longitude');\r\n\r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentLat: [undefined, [$scope.validators.required,\r\n $scope.validators.min(-90),\r\n $scope.validators.max(90)]],\r\n currentLng: [undefined, [$scope.validators.required,\r\n $scope.validators.min(-180),\r\n $scope.validators.max(180)]]}\r\n );\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != \"timeseries\"){\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n $scope.isFocused = false;\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityTimeseries(\r\n datasource.entity.id,\r\n 'scope',\r\n [\r\n {\r\n key: settings.latKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLat').value\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLng').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalLat = $scope.attributeUpdateFormGroup.get('currentLat').value;\r\n $scope.originalLng = $scope.attributeUpdateFormGroup.get('currentLng').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n };\r\n \r\n $scope.discardChange = function() {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n 'currentLat': $scope.originalLat,\r\n 'currentLng': $scope.originalLng\r\n });\r\n $scope.isFocused = false;\r\n $scope.attributeUpdateFormGroup.markAsPristine();\r\n self.onDataUpdated();\r\n };\r\n \r\n $scope.disableButton = function () {\r\n return $scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null;\r\n };\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.blocked-location'), \r\n 'bottom', 'left', $scope.toastTargetId);\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.no-support-geolocation'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n };\r\n \r\n function showPosition(position) {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n currentLat: correctValue(position.coords.latitude),\r\n currentLng: correctValue(position.coords.longitude)\r\n });\r\n $scope.attributeUpdateFormGroup.markAsDirty();\r\n $scope.isFocused = true;\r\n }\r\n \r\n self.onResize();\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName && $scope.attributeUpdateFormGroup.get('currentLat').pristine){\r\n $scope.originalLat = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLat').patchValue(correctValue($scope.originalLat));\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName && $scope.attributeUpdateFormGroup.get('currentLng').pristine){\r\n $scope.originalLng = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLng').patchValue(correctValue($scope.originalLng));\r\n }\r\n }\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nfunction correctValue(value) {\r\n if (typeof value !== \"number\") {\r\n return 0;\r\n }\r\n return value;\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container && self.ctx.$container[0].offsetWidth < 320);\r\n $scope.changeAlignment = ($scope.isHorizontal && self.ctx.$container && self.ctx.$container[0].offsetWidth < 480);\r\n self.ctx.detectChanges();\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 2,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n\r\n};",
"settingsSchema": "",
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index cc9c989cfa..8470566af3 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -90,6 +90,7 @@ import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.MobileAppId;
+import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.QueueId;
@@ -105,6 +106,7 @@ import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle;
+import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
@@ -141,6 +143,7 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.mobile.MobileAppBundleService;
import org.thingsboard.server.dao.mobile.MobileAppService;
import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.oauth2.OAuth2ClientService;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
import org.thingsboard.server.dao.ota.OtaPackageService;
@@ -355,6 +358,9 @@ public abstract class BaseController {
@Autowired
protected TbServiceInfoProvider serviceInfoProvider;
+ @Autowired
+ protected NotificationTargetService notificationTargetService;
+
@Value("${server.log_controller_error_stack_trace}")
@Getter
private boolean logControllerErrorStackTrace;
@@ -852,6 +858,10 @@ public abstract class BaseController {
return checkEntityId(mobileAppBundleId, mobileAppBundleService::findMobileAppBundleById, operation);
}
+ NotificationTarget checkNotificationTargetId(NotificationTargetId notificationTargetId, Operation operation) throws ThingsboardException {
+ return checkEntityId(notificationTargetId, notificationTargetService::findNotificationTargetById, operation);
+ }
+
protected I emptyId(EntityType entityType) {
return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID);
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java
index c1248a1fbf..e055c6d6ae 100644
--- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java
@@ -266,6 +266,12 @@ public class NotificationController extends BaseController {
}
notificationRequest.setTenantId(user.getTenantId());
checkEntity(notificationRequest.getId(), notificationRequest, NOTIFICATION);
+ List targets = notificationRequest.getTargets().stream()
+ .map(NotificationTargetId::new)
+ .toList();
+ for (NotificationTargetId targetId : targets) {
+ checkNotificationTargetId(targetId, Operation.READ);
+ }
notificationRequest.setOriginatorEntityId(user.getId());
notificationRequest.setInfo(null);
@@ -316,6 +322,8 @@ public class NotificationController extends BaseController {
Map recipientsCountByTarget = new LinkedHashMap<>();
Map firstRecipient = new HashMap<>();
for (NotificationTarget target : targets) {
+ checkEntity(getCurrentUser(), target, Operation.READ);
+
int recipientsCount;
List recipientsPart;
NotificationTargetType targetType = target.getConfiguration().getType();
diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java
index 7634819e39..6a45b9e4e5 100644
--- a/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java
@@ -127,7 +127,7 @@ public class NotificationTargetController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationTarget getNotificationTargetById(@PathVariable UUID id) throws ThingsboardException {
NotificationTargetId notificationTargetId = new NotificationTargetId(id);
- return checkEntityId(notificationTargetId, notificationTargetService::findNotificationTargetById, Operation.READ);
+ return checkNotificationTargetId(notificationTargetId, Operation.READ);
}
@ApiOperation(value = "Get recipients for notification target config (getRecipientsForNotificationTargetConfig)",
@@ -214,7 +214,7 @@ public class NotificationTargetController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public void deleteNotificationTargetById(@PathVariable UUID id) throws Exception {
NotificationTargetId notificationTargetId = new NotificationTargetId(id);
- NotificationTarget notificationTarget = checkEntityId(notificationTargetId, notificationTargetService::findNotificationTargetById, Operation.DELETE);
+ NotificationTarget notificationTarget = checkNotificationTargetId(notificationTargetId, Operation.DELETE);
doDeleteAndLog(EntityType.NOTIFICATION_TARGET, notificationTarget, notificationTargetService::deleteNotificationTargetById);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
index 71e2b576ed..311fe6cb2c 100644
--- a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
+++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
@@ -40,7 +40,9 @@ import org.thingsboard.server.common.data.notification.NotificationRequestConfig
import org.thingsboard.server.common.data.notification.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
import org.thingsboard.server.common.data.notification.NotificationStatus;
+import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.GeneralNotificationInfo;
+import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings;
@@ -217,6 +219,21 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
}
}
+ @Override
+ public void sendSystemNotification(TenantId tenantId, NotificationTargetId targetId, NotificationType type, NotificationInfo info) {
+ log.debug("[{}] Sending {} system notification to {}: {}", tenantId, type, targetId, info);
+ NotificationTemplate notificationTemplate = notificationTemplateService.findTenantOrSystemNotificationTemplate(tenantId, type)
+ .orElseThrow(() -> new IllegalArgumentException("No notification template found for type " + type));
+ NotificationRequest notificationRequest = NotificationRequest.builder()
+ .tenantId(TenantId.SYS_TENANT_ID)
+ .targets(List.of(targetId.getId()))
+ .templateId(notificationTemplate.getId())
+ .info(info)
+ .originatorEntityId(TenantId.SYS_TENANT_ID)
+ .build();
+ processNotificationRequest(TenantId.SYS_TENANT_ID, notificationRequest, null);
+ }
+
private void processNotificationRequestAsync(NotificationProcessingContext ctx, List targets, FutureCallback callback) {
notificationExecutor.submit(() -> {
long startTs = System.currentTimeMillis();
@@ -271,7 +288,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
}, 256);
} else {
recipients = new PageDataIterable<>(pageLink -> {
- return notificationTargetService.findRecipientsForNotificationTargetConfig(ctx.getTenantId(), targetConfig, pageLink);
+ return notificationTargetService.findRecipientsForNotificationTargetConfig(target.getTenantId(), targetConfig, pageLink);
}, 256);
}
}
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
index 3302fe4fc2..aef4772053 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
@@ -23,6 +23,7 @@ import com.google.common.util.concurrent.ListeningExecutorService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.RandomStringUtils;
import org.awaitility.Awaitility;
import org.hamcrest.Matcher;
import org.hibernate.exception.ConstraintViolationException;
@@ -104,6 +105,21 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.notification.Notification;
+import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
+import org.thingsboard.server.common.data.notification.NotificationType;
+import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
+import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
+import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter;
+import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter;
+import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
+import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate;
+import org.thingsboard.server.common.data.notification.template.HasSubject;
+import org.thingsboard.server.common.data.notification.template.MobileAppDeliveryMethodNotificationTemplate;
+import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
+import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig;
+import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate;
+import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.oauth2.MapperType;
import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig;
@@ -121,6 +137,7 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.config.ThingsboardSecurityConfiguration;
import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.device.ClaimDevicesService;
import org.thingsboard.server.dao.tenant.TenantProfileService;
@@ -142,6 +159,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -1189,4 +1207,76 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return readResponse(mockMvc.perform(request).andExpect(status().isOk()), TbResourceInfo.class);
}
+ protected NotificationTarget createNotificationTarget(UserId... usersIds) {
+ UserListFilter filter = new UserListFilter();
+ filter.setUsersIds(DaoUtil.toUUIDs(List.of(usersIds)));
+ return createNotificationTarget(filter);
+ }
+
+ protected NotificationTarget createNotificationTarget(UsersFilter usersFilter) {
+ NotificationTarget notificationTarget = new NotificationTarget();
+ notificationTarget.setName(usersFilter.toString() + RandomStringUtils.randomNumeric(5));
+ PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig();
+ targetConfig.setUsersFilter(usersFilter);
+ notificationTarget.setConfiguration(targetConfig);
+ return saveNotificationTarget(notificationTarget);
+ }
+
+ protected NotificationTarget saveNotificationTarget(NotificationTarget notificationTarget) {
+ return doPost("/api/notification/target", notificationTarget, NotificationTarget.class);
+ }
+
+ protected NotificationTemplate createNotificationTemplate(NotificationType notificationType, String subject,
+ String text, NotificationDeliveryMethod... deliveryMethods) {
+ NotificationTemplate notificationTemplate = new NotificationTemplate();
+ notificationTemplate.setTenantId(tenantId);
+ notificationTemplate.setName("Notification template: " + text);
+ notificationTemplate.setNotificationType(notificationType);
+ NotificationTemplateConfig config = new NotificationTemplateConfig();
+ config.setDeliveryMethodsTemplates(new HashMap<>());
+ for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) {
+ DeliveryMethodNotificationTemplate deliveryMethodNotificationTemplate;
+ switch (deliveryMethod) {
+ case WEB: {
+ deliveryMethodNotificationTemplate = new WebDeliveryMethodNotificationTemplate();
+ break;
+ }
+ case EMAIL: {
+ deliveryMethodNotificationTemplate = new EmailDeliveryMethodNotificationTemplate();
+ break;
+ }
+ case SMS: {
+ deliveryMethodNotificationTemplate = new SmsDeliveryMethodNotificationTemplate();
+ break;
+ }
+ case MOBILE_APP:
+ deliveryMethodNotificationTemplate = new MobileAppDeliveryMethodNotificationTemplate();
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported delivery method " + deliveryMethod);
+ }
+ deliveryMethodNotificationTemplate.setEnabled(true);
+ deliveryMethodNotificationTemplate.setBody(text);
+ if (deliveryMethodNotificationTemplate instanceof HasSubject) {
+ ((HasSubject) deliveryMethodNotificationTemplate).setSubject(subject);
+ }
+ config.getDeliveryMethodsTemplates().put(deliveryMethod, deliveryMethodNotificationTemplate);
+ }
+ notificationTemplate.setConfiguration(config);
+ return saveNotificationTemplate(notificationTemplate);
+ }
+
+ protected NotificationTemplate saveNotificationTemplate(NotificationTemplate notificationTemplate) {
+ return doPost("/api/notification/template", notificationTemplate, NotificationTemplate.class);
+ }
+
+ protected List getMyNotifications(boolean unreadOnly, int limit) throws Exception {
+ return getMyNotifications(NotificationDeliveryMethod.WEB, unreadOnly, limit);
+ }
+
+ protected List getMyNotifications(NotificationDeliveryMethod deliveryMethod, boolean unreadOnly, int limit) throws Exception {
+ return doGetTypedWithPageLink("/api/notifications?unreadOnly={unreadOnly}&deliveryMethod={deliveryMethod}&", new TypeReference>() {},
+ new PageLink(limit, 0), unreadOnly, deliveryMethod).getData();
+ }
+
}
diff --git a/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java
index 6d4ebb3676..3dab0a369a 100644
--- a/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java
+++ b/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java
@@ -274,14 +274,6 @@ public class NotificationEdgeTest extends AbstractEdgeTest {
return saveNotificationRule(notificationRule);
}
- private NotificationTemplate saveNotificationTemplate(NotificationTemplate notificationTemplate) {
- return doPost("/api/notification/template", notificationTemplate, NotificationTemplate.class);
- }
-
- private NotificationTarget saveNotificationTarget(NotificationTarget notificationTarget) {
- return doPost("/api/notification/target", notificationTarget, NotificationTarget.class);
- }
-
private NotificationRule saveNotificationRule(NotificationRule notificationRule) {
return doPost("/api/notification/rule", notificationRule, NotificationRule.class);
}
diff --git a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
index bbe31f3e94..fcdf82bd52 100644
--- a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
@@ -30,8 +30,6 @@ import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.NotificationTemplateId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
-import org.thingsboard.server.common.data.id.UserId;
-import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
@@ -44,18 +42,7 @@ import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.settings.NotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
-import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
-import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
-import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter;
-import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter;
-import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
-import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate;
-import org.thingsboard.server.common.data.notification.template.HasSubject;
-import org.thingsboard.server.common.data.notification.template.MobileAppDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
-import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig;
-import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate;
-import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
@@ -71,7 +58,6 @@ import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import java.net.URISyntaxException;
import java.util.Arrays;
-import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@@ -120,25 +106,6 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
notificationSettingsService.deleteNotificationSettings(TenantId.SYS_TENANT_ID);
}
- protected NotificationTarget createNotificationTarget(UserId... usersIds) {
- UserListFilter filter = new UserListFilter();
- filter.setUsersIds(DaoUtil.toUUIDs(List.of(usersIds)));
- return createNotificationTarget(filter);
- }
-
- protected NotificationTarget createNotificationTarget(UsersFilter usersFilter) {
- NotificationTarget notificationTarget = new NotificationTarget();
- notificationTarget.setName(usersFilter.toString());
- PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig();
- targetConfig.setUsersFilter(usersFilter);
- notificationTarget.setConfiguration(targetConfig);
- return saveNotificationTarget(notificationTarget);
- }
-
- protected NotificationTarget saveNotificationTarget(NotificationTarget notificationTarget) {
- return doPost("/api/notification/target", notificationTarget, NotificationTarget.class);
- }
-
protected NotificationRequest submitNotificationRequest(NotificationTargetId targetId, String text, NotificationDeliveryMethod... deliveryMethods) {
return submitNotificationRequest(targetId, text, 0, deliveryMethods);
}
@@ -178,50 +145,6 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
return findNotificationRequest(notificationRequestId).getStats();
}
- protected NotificationTemplate createNotificationTemplate(NotificationType notificationType, String subject,
- String text, NotificationDeliveryMethod... deliveryMethods) {
- NotificationTemplate notificationTemplate = new NotificationTemplate();
- notificationTemplate.setTenantId(tenantId);
- notificationTemplate.setName("Notification template: " + text);
- notificationTemplate.setNotificationType(notificationType);
- NotificationTemplateConfig config = new NotificationTemplateConfig();
- config.setDeliveryMethodsTemplates(new HashMap<>());
- for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) {
- DeliveryMethodNotificationTemplate deliveryMethodNotificationTemplate;
- switch (deliveryMethod) {
- case WEB: {
- deliveryMethodNotificationTemplate = new WebDeliveryMethodNotificationTemplate();
- break;
- }
- case EMAIL: {
- deliveryMethodNotificationTemplate = new EmailDeliveryMethodNotificationTemplate();
- break;
- }
- case SMS: {
- deliveryMethodNotificationTemplate = new SmsDeliveryMethodNotificationTemplate();
- break;
- }
- case MOBILE_APP:
- deliveryMethodNotificationTemplate = new MobileAppDeliveryMethodNotificationTemplate();
- break;
- default:
- throw new IllegalArgumentException("Unsupported delivery method " + deliveryMethod);
- }
- deliveryMethodNotificationTemplate.setEnabled(true);
- deliveryMethodNotificationTemplate.setBody(text);
- if (deliveryMethodNotificationTemplate instanceof HasSubject) {
- ((HasSubject) deliveryMethodNotificationTemplate).setSubject(subject);
- }
- config.getDeliveryMethodsTemplates().put(deliveryMethod, deliveryMethodNotificationTemplate);
- }
- notificationTemplate.setConfiguration(config);
- return saveNotificationTemplate(notificationTemplate);
- }
-
- protected NotificationTemplate saveNotificationTemplate(NotificationTemplate notificationTemplate) {
- return doPost("/api/notification/template", notificationTemplate, NotificationTemplate.class);
- }
-
protected void saveNotificationSettings(NotificationSettings notificationSettings) throws Exception {
doPost("/api/notification/settings", notificationSettings).andExpect(status().isOk());
}
@@ -258,15 +181,6 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
doDelete("/api/notification/request/" + id);
}
- protected List getMyNotifications(boolean unreadOnly, int limit) throws Exception {
- return getMyNotifications(NotificationDeliveryMethod.WEB, unreadOnly, limit);
- }
-
- protected List getMyNotifications(NotificationDeliveryMethod deliveryMethod, boolean unreadOnly, int limit) throws Exception {
- return doGetTypedWithPageLink("/api/notifications?unreadOnly={unreadOnly}&deliveryMethod={deliveryMethod}&", new TypeReference>() {},
- new PageLink(limit, 0), unreadOnly, deliveryMethod).getData();
- }
-
protected NotificationRule createNotificationRule(NotificationRuleTriggerConfig triggerConfig, String subject, String text, NotificationTargetId... targets) {
return createNotificationRule(triggerConfig, subject, text, List.of(targets), NotificationDeliveryMethod.WEB);
}
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java
index 9221dcb2cf..09443e8c63 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java
@@ -15,11 +15,15 @@
*/
package org.thingsboard.server.dao.notification;
+import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings;
+import java.util.Map;
+
public interface NotificationSettingsService {
void saveNotificationSettings(TenantId tenantId, NotificationSettings settings);
@@ -36,4 +40,6 @@ public interface NotificationSettingsService {
void updateDefaultNotificationConfigs(TenantId tenantId);
+ void moveMailTemplatesToNotificationCenter(TenantId tenantId, JsonNode mailTemplates, Map mailTemplatesNames);
+
}
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateService.java
index 664f976904..d41295f907 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateService.java
@@ -22,7 +22,9 @@ import org.thingsboard.server.common.data.notification.template.NotificationTemp
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
+import java.util.Collection;
import java.util.List;
+import java.util.Optional;
public interface NotificationTemplateService {
@@ -32,7 +34,11 @@ public interface NotificationTemplateService {
PageData findNotificationTemplatesByTenantIdAndNotificationTypes(TenantId tenantId, List notificationTypes, PageLink pageLink);
- int countNotificationTemplatesByTenantIdAndNotificationTypes(TenantId tenantId, List notificationTypes);
+ Optional findTenantOrSystemNotificationTemplate(TenantId tenantId, NotificationType notificationType);
+
+ Optional findNotificationTemplateByTenantIdAndType(TenantId tenantId, NotificationType notificationType);
+
+ int countNotificationTemplatesByTenantIdAndNotificationTypes(TenantId tenantId, Collection notificationTypes);
void deleteNotificationTemplateById(TenantId tenantId, NotificationTemplateId id);
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java
index ec66a3d53a..1ddc17f36a 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java
@@ -15,6 +15,12 @@
*/
package org.thingsboard.server.common.data.notification;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@AllArgsConstructor
+@NoArgsConstructor
public enum NotificationType {
GENERAL,
@@ -31,6 +37,9 @@ public enum NotificationType {
RATE_LIMITS,
EDGE_CONNECTION,
EDGE_COMMUNICATION_FAILURE,
- TASK_PROCESSING_FAILURE
+ TASK_PROCESSING_FAILURE;
+
+ @Getter
+ private boolean system; // for future use and compatibility with PE
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java
index 10438c484a..1404eaff67 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java
@@ -15,8 +15,11 @@
*/
package org.thingsboard.server.dao.notification;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@@ -41,7 +44,9 @@ import org.thingsboard.server.common.data.notification.targets.platform.SystemAd
import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType;
+import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
+import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsType;
@@ -249,6 +254,52 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
}
}
+ @Override
+ public void moveMailTemplatesToNotificationCenter(TenantId tenantId, JsonNode mailTemplates, Map mailTemplatesNames) {
+ mailTemplatesNames.forEach((mailTemplateName, notificationType) -> {
+ moveMailTemplateToNotificationCenter(tenantId, mailTemplates, mailTemplateName, notificationType);
+ });
+ }
+
+ private void moveMailTemplateToNotificationCenter(TenantId tenantId, JsonNode mailTemplates, String mailTemplateName, NotificationType notificationType) {
+ JsonNode mailTemplate = mailTemplates.get(mailTemplateName);
+ if (mailTemplate == null || mailTemplate.isNull() || !mailTemplate.has("subject") || !mailTemplate.has("body")) {
+ return;
+ }
+
+ String subject = mailTemplate.get("subject").asText();
+ String body = mailTemplate.get("body").asText();
+ body = body.replace("targetEmail", "recipientEmail");
+
+ NotificationTemplate notificationTemplate = null;
+ if (tenantId.isSysTenantId()) {
+ // updating system notification template, not touching tenants' templates
+ notificationTemplate = notificationTemplateService.findNotificationTemplateByTenantIdAndType(tenantId, notificationType).orElse(null);
+ }
+ if (notificationTemplate == null) {
+ log.debug("[{}] Creating {} template", tenantId, notificationType);
+ notificationTemplate = new NotificationTemplate();
+ } else {
+ log.debug("[{}] Updating {} template", tenantId, notificationType);
+ }
+ String name = StringUtils.capitalize(notificationType.name().toLowerCase().replaceAll("_", " ")) + " notification";
+ notificationTemplate.setName(name);
+ notificationTemplate.setTenantId(tenantId);
+ notificationTemplate.setNotificationType(notificationType);
+ NotificationTemplateConfig notificationTemplateConfig = new NotificationTemplateConfig();
+
+ EmailDeliveryMethodNotificationTemplate emailNotificationTemplate = new EmailDeliveryMethodNotificationTemplate();
+ emailNotificationTemplate.setEnabled(true);
+ emailNotificationTemplate.setSubject(subject);
+ emailNotificationTemplate.setBody(body);
+
+ notificationTemplateConfig.setDeliveryMethodsTemplates(Map.of(NotificationDeliveryMethod.EMAIL, emailNotificationTemplate));
+ notificationTemplate.setConfiguration(notificationTemplateConfig);
+ notificationTemplateService.saveNotificationTemplate(tenantId, notificationTemplate);
+
+ ((ObjectNode) mailTemplates).remove(mailTemplateName);
+ }
+
private boolean isNotificationConfigured(TenantId tenantId, NotificationType... notificationTypes) {
return notificationTemplateService.countNotificationTemplatesByTenantIdAndNotificationTypes(tenantId, List.of(notificationTypes)) > 0;
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java
index a6ab6e08ff..9b5b9ca85b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java
@@ -109,7 +109,7 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl
NotificationTarget notificationTarget = findNotificationTargetById(tenantId, targetId);
Objects.requireNonNull(notificationTarget, "Notification target [" + targetId + "] not found");
NotificationTargetConfig configuration = notificationTarget.getConfiguration();
- return findRecipientsForNotificationTargetConfig(tenantId, (PlatformUsersNotificationTargetConfig) configuration, pageLink);
+ return findRecipientsForNotificationTargetConfig(notificationTarget.getTenantId(), (PlatformUsersNotificationTargetConfig) configuration, pageLink);
}
@Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java
index 14b9d176df..ed2a8d0c91 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java
@@ -32,6 +32,7 @@ import org.thingsboard.server.dao.entity.EntityDaoService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -50,11 +51,19 @@ public class DefaultNotificationTemplateService extends AbstractEntityService im
@Override
public NotificationTemplate saveNotificationTemplate(TenantId tenantId, NotificationTemplate notificationTemplate) {
+ NotificationType notificationType = notificationTemplate.getNotificationType();
if (notificationTemplate.getId() != null) {
NotificationTemplate oldNotificationTemplate = findNotificationTemplateById(tenantId, notificationTemplate.getId());
- if (notificationTemplate.getNotificationType() != oldNotificationTemplate.getNotificationType()) {
+ if (notificationType != oldNotificationTemplate.getNotificationType()) {
throw new IllegalArgumentException("Notification type cannot be updated");
}
+ } else {
+ if (notificationType.isSystem()) {
+ int systemTemplatesCount = countNotificationTemplatesByTenantIdAndNotificationTypes(tenantId, List.of(notificationType));
+ if (systemTemplatesCount > 0) {
+ throw new IllegalArgumentException("There can only be one notification template of this type");
+ }
+ }
}
try {
NotificationTemplate savedTemplate = notificationTemplateDao.saveAndFlush(tenantId, notificationTemplate);
@@ -75,7 +84,19 @@ public class DefaultNotificationTemplateService extends AbstractEntityService im
}
@Override
- public int countNotificationTemplatesByTenantIdAndNotificationTypes(TenantId tenantId, List notificationTypes) {
+ public Optional findTenantOrSystemNotificationTemplate(TenantId tenantId, NotificationType notificationType) {
+ return findNotificationTemplateByTenantIdAndType(tenantId, notificationType)
+ .or(() -> findNotificationTemplateByTenantIdAndType(TenantId.SYS_TENANT_ID, notificationType));
+ }
+
+ @Override
+ public Optional findNotificationTemplateByTenantIdAndType(TenantId tenantId, NotificationType notificationType) {
+ return findNotificationTemplatesByTenantIdAndNotificationTypes(tenantId, List.of(notificationType), new PageLink(1)).getData()
+ .stream().findFirst();
+ }
+
+ @Override
+ public int countNotificationTemplatesByTenantIdAndNotificationTypes(TenantId tenantId, Collection notificationTypes) {
return notificationTemplateDao.countByTenantIdAndNotificationTypes(tenantId, notificationTypes);
}
@@ -86,8 +107,16 @@ public class DefaultNotificationTemplateService extends AbstractEntityService im
@Override
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
- if (!force && notificationRequestDao.existsByTenantIdAndStatusAndTemplateId(tenantId, NotificationRequestStatus.SCHEDULED, (NotificationTemplateId) id)) {
- throw new IllegalArgumentException("Notification template is referenced by scheduled notification request");
+ if (!force) {
+ if (notificationRequestDao.existsByTenantIdAndStatusAndTemplateId(tenantId, NotificationRequestStatus.SCHEDULED, (NotificationTemplateId) id)) {
+ throw new IllegalArgumentException("Notification template is referenced by scheduled notification request");
+ }
+ if (tenantId.isSysTenantId()) {
+ NotificationTemplate notificationTemplate = findNotificationTemplateById(tenantId, (NotificationTemplateId) id);
+ if (notificationTemplate.getNotificationType().isSystem()) {
+ throw new IllegalArgumentException("System notification template cannot be deleted");
+ }
+ }
}
try {
notificationTemplateDao.removeById(tenantId, id.getId());
diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateDao.java b/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateDao.java
index 136bc4d472..d3f79f0ed2 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateDao.java
@@ -24,13 +24,14 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.ExportableEntityDao;
+import java.util.Collection;
import java.util.List;
public interface NotificationTemplateDao extends Dao, ExportableEntityDao {
PageData findByTenantIdAndNotificationTypesAndPageLink(TenantId tenantId, List notificationTypes, PageLink pageLink);
- int countByTenantIdAndNotificationTypes(TenantId tenantId, List notificationTypes);
+ int countByTenantIdAndNotificationTypes(TenantId tenantId, Collection notificationTypes);
void removeByTenantId(TenantId tenantId);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTemplateDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTemplateDao.java
index f0ca725951..ae46efb2f7 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTemplateDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTemplateDao.java
@@ -31,6 +31,7 @@ import org.thingsboard.server.dao.notification.NotificationTemplateDao;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
+import java.util.Collection;
import java.util.List;
import java.util.UUID;
@@ -53,7 +54,7 @@ public class JpaNotificationTemplateDao extends JpaAbstractDao notificationTypes) {
+ public int countByTenantIdAndNotificationTypes(TenantId tenantId, Collection notificationTypes) {
return notificationTemplateRepository.countByTenantIdAndNotificationTypes(tenantId.getId(), notificationTypes);
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTemplateRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTemplateRepository.java
index bb45c10d06..bffdf98d43 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTemplateRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTemplateRepository.java
@@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.dao.ExportableEntityRepository;
import org.thingsboard.server.dao.model.sql.NotificationTemplateEntity;
+import java.util.Collection;
import java.util.List;
import java.util.UUID;
@@ -45,7 +46,7 @@ public interface NotificationTemplateRepository extends JpaRepository notificationTypes);
+ @Param("notificationTypes") Collection notificationTypes);
@Transactional
@Modifying
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java
index ce2ff08734..02ee53f3ad 100644
--- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java
@@ -18,12 +18,15 @@ package org.thingsboard.rule.engine.api;
import com.google.common.util.concurrent.FutureCallback;
import org.thingsboard.server.common.data.id.NotificationId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
+import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestStats;
+import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.GeneralNotificationInfo;
+import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
@@ -35,6 +38,8 @@ public interface NotificationCenter {
void sendGeneralWebNotification(TenantId tenantId, UsersFilter recipients, NotificationTemplate template, GeneralNotificationInfo info);
+ void sendSystemNotification(TenantId tenantId, NotificationTargetId targetId, NotificationType type, NotificationInfo info); // for future use and compatibility with PE
+
void deleteNotificationRequest(TenantId tenantId, NotificationRequestId notificationRequestId);
void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId);
diff --git a/ui-ngx/src/app/core/settings/settings.effects.ts b/ui-ngx/src/app/core/settings/settings.effects.ts
index 1564814a74..3ce59f9f6e 100644
--- a/ui-ngx/src/app/core/settings/settings.effects.ts
+++ b/ui-ngx/src/app/core/settings/settings.effects.ts
@@ -15,7 +15,7 @@
///
import { ActivationEnd, Router } from '@angular/router';
-import { Injectable } from '@angular/core';
+import { Inject, Injectable } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { TranslateService } from '@ngx-translate/core';
@@ -31,6 +31,7 @@ import { updateUserLang } from '@app/core/settings/settings.utils';
import { UtilsService } from '@core/services/utils.service';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { ActionAuthUpdateLastPublicDashboardId } from '../auth/auth.actions';
+import { DOCUMENT } from '@angular/common';
export const SETTINGS_KEY = 'SETTINGS';
@@ -43,7 +44,8 @@ export class SettingsEffects {
private router: Router,
private localStorageService: LocalStorageService,
private titleService: TitleService,
- private translate: TranslateService
+ private translate: TranslateService,
+ @Inject(DOCUMENT) private document: Document,
) {
}
@@ -56,7 +58,7 @@ export class SettingsEffects {
distinctUntilChanged((a, b) => a?.userLang === b?.userLang),
tap(setting => {
this.localStorageService.setItem(SETTINGS_KEY, setting);
- updateUserLang(this.translate, setting.userLang);
+ updateUserLang(this.translate, this.document, setting.userLang);
})
), {dispatch: false});
diff --git a/ui-ngx/src/app/core/settings/settings.utils.ts b/ui-ngx/src/app/core/settings/settings.utils.ts
index 8f0a261d08..fa7ede8559 100644
--- a/ui-ngx/src/app/core/settings/settings.utils.ts
+++ b/ui-ngx/src/app/core/settings/settings.utils.ts
@@ -19,7 +19,7 @@ import { TranslateService } from '@ngx-translate/core';
import _moment from 'moment';
import { Observable } from 'rxjs';
-export function updateUserLang(translate: TranslateService, userLang: string, translations = env.supportedLangs): Observable {
+export function updateUserLang(translate: TranslateService, document: Document, userLang: string, translations = env.supportedLangs): Observable {
let targetLang = userLang;
if (!translations) {
translations = env.supportedLangs;
@@ -37,6 +37,7 @@ export function updateUserLang(translate: TranslateService, userLang: string, tr
if (!env.production) {
console.log(`Detected supported lang: ${detectedSupportedLang}`);
}
+ document.documentElement.lang = detectedSupportedLang.replace('_', '-');
_moment.locale([detectedSupportedLang]);
return translate.use(detectedSupportedLang);
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts
index ed023c1a0d..efb25a0258 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts
@@ -112,6 +112,9 @@ export class TimeSeriesChartWidgetComponent implements OnInit, OnDestroy, AfterV
legendKey.dataKey.settings = mergeDeep({} as TimeSeriesChartKeySettings,
timeSeriesChartKeyDefaultSettings, legendKey.dataKey.settings);
legendKey.dataKey.hidden = legendKey.dataKey.settings.dataHiddenByDefault;
+ if (this.settings.yAxes[legendKey.dataKey.settings.yAxisId]) {
+ this.settings.yAxes[legendKey.dataKey.settings.yAxisId].show = !legendKey.dataKey.settings.dataHiddenByDefault;
+ }
});
this.legendKeys = this.legendKeys.filter(legendKey => legendKey.dataKey.settings.showInLegend);
if (!this.legendKeys.length) {
diff --git a/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html
index 0cd5247719..f8456d190a 100644
--- a/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html
+++ b/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html
@@ -62,6 +62,7 @@
diff --git a/ui-ngx/src/app/shared/components/image/image-gallery.component.html b/ui-ngx/src/app/shared/components/image/image-gallery.component.html
index 685c53416c..a45b9a982c 100644
--- a/ui-ngx/src/app/shared/components/image/image-gallery.component.html
+++ b/ui-ngx/src/app/shared/components/image/image-gallery.component.html
@@ -132,7 +132,7 @@
-