diff --git a/application/src/main/data/json/system/scada_symbols/apartments-hp.svg b/application/src/main/data/json/system/scada_symbols/apartments-hp.svg index 9e2c965f79..97ac492626 100644 --- a/application/src/main/data/json/system/scada_symbols/apartments-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/apartments-hp.svg @@ -330,7 +330,7 @@ } ] }]]> - + diff --git a/application/src/main/data/json/system/scada_symbols/bottom-light-bulb-hp.svg b/application/src/main/data/json/system/scada_symbols/bottom-light-bulb-hp.svg index 4f0603b904..db107240a0 100644 --- a/application/src/main/data/json/system/scada_symbols/bottom-light-bulb-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/bottom-light-bulb-hp.svg @@ -332,7 +332,7 @@ - + diff --git a/application/src/main/data/json/system/scada_symbols/conical-tank.svg b/application/src/main/data/json/system/scada_symbols/conical-tank.svg index 7ae8f41934..6b37adc52e 100644 --- a/application/src/main/data/json/system/scada_symbols/conical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/conical-tank.svg @@ -371,7 +371,7 @@ - + diff --git a/application/src/main/data/json/system/scada_symbols/consumers-hp.svg b/application/src/main/data/json/system/scada_symbols/consumers-hp.svg index e4005d7ae2..8333d6bf55 100644 --- a/application/src/main/data/json/system/scada_symbols/consumers-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/consumers-hp.svg @@ -332,7 +332,7 @@ - + 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 e782b07609..b3506ae54f 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 @@ -791,5 +791,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 960abed340..3c52dae1c3 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 @@ -791,5 +791,5 @@ 26 - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/electrical-distribution-board-hp.svg b/application/src/main/data/json/system/scada_symbols/electrical-distribution-board-hp.svg index 0fcfe4dde0..6e4175c6d7 100644 --- a/application/src/main/data/json/system/scada_symbols/electrical-distribution-board-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/electrical-distribution-board-hp.svg @@ -318,7 +318,7 @@ }]]> - + 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 58ff3951e5..1f4516723e 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 @@ -273,7 +273,7 @@ } ] }]]> - + diff --git a/application/src/main/data/json/system/scada_symbols/four-rate-energy-meter-hp.svg b/application/src/main/data/json/system/scada_symbols/four-rate-energy-meter-hp.svg index de4fb13836..bba67e5fe3 100644 --- a/application/src/main/data/json/system/scada_symbols/four-rate-energy-meter-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/four-rate-energy-meter-hp.svg @@ -862,7 +862,7 @@ } ] }]]> -T1T2T3Export000223000223000223000223kWh +T1T2T3Export000223000223000223000223kWh diff --git a/application/src/main/data/json/system/scada_symbols/gas-preventer-hp.svg b/application/src/main/data/json/system/scada_symbols/gas-preventer-hp.svg index ea5a479eda..487b5e54d9 100644 --- a/application/src/main/data/json/system/scada_symbols/gas-preventer-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/gas-preventer-hp.svg @@ -347,7 +347,7 @@ - + diff --git a/application/src/main/data/json/system/scada_symbols/heat-exchanger-hp.svg b/application/src/main/data/json/system/scada_symbols/heat-exchanger-hp.svg index 797220ea96..c1c3d4647e 100644 --- a/application/src/main/data/json/system/scada_symbols/heat-exchanger-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/heat-exchanger-hp.svg @@ -349,7 +349,7 @@ - + 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 1d7babc3b0..eed54682cd 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 @@ -587,7 +587,7 @@ - + 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 18f194684b..bb6f5e55a9 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 @@ -502,7 +502,7 @@ - + diff --git a/application/src/main/data/json/system/scada_symbols/house-hp.svg b/application/src/main/data/json/system/scada_symbols/house-hp.svg index d95c47bb8b..e3556d4d9a 100644 --- a/application/src/main/data/json/system/scada_symbols/house-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/house-hp.svg @@ -335,7 +335,7 @@ - + diff --git a/application/src/main/data/json/system/scada_symbols/industrial-fuel-generator-hp.svg b/application/src/main/data/json/system/scada_symbols/industrial-fuel-generator-hp.svg index 2b7a476a92..e0ee85b8cf 100644 --- a/application/src/main/data/json/system/scada_symbols/industrial-fuel-generator-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/industrial-fuel-generator-hp.svg @@ -344,7 +344,7 @@ } ] }]]> - + diff --git a/application/src/main/data/json/system/scada_symbols/large-horizontal-separator-hp.svg b/application/src/main/data/json/system/scada_symbols/large-horizontal-separator-hp.svg index 79f6745a73..8179549c54 100644 --- a/application/src/main/data/json/system/scada_symbols/large-horizontal-separator-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/large-horizontal-separator-hp.svg @@ -336,7 +336,7 @@ } ] }]]> - + diff --git a/application/src/main/data/json/system/scada_symbols/large-inverter-hp.svg b/application/src/main/data/json/system/scada_symbols/large-inverter-hp.svg index 11bc5da0a6..bf18d64040 100644 --- a/application/src/main/data/json/system/scada_symbols/large-inverter-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/large-inverter-hp.svg @@ -564,7 +564,7 @@ } ] }]]> - + diff --git a/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg index 11bd47916f..09c0e2a9e1 100644 --- a/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg @@ -565,7 +565,7 @@ 1660 gal - + diff --git a/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg index d9c05bde40..be8b1207a0 100644 --- a/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg @@ -569,7 +569,7 @@ 1660 gal - + diff --git a/application/src/main/data/json/system/scada_symbols/large-vertical-separator-hp.svg b/application/src/main/data/json/system/scada_symbols/large-vertical-separator-hp.svg index 99ff420530..1951b741bf 100644 --- a/application/src/main/data/json/system/scada_symbols/large-vertical-separator-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/large-vertical-separator-hp.svg @@ -336,7 +336,7 @@ } ] }]]> - + diff --git a/application/src/main/data/json/system/scada_symbols/leak-sensor.svg b/application/src/main/data/json/system/scada_symbols/leak-sensor.svg index 0fce8be608..813116a1f5 100644 --- a/application/src/main/data/json/system/scada_symbols/leak-sensor.svg +++ b/application/src/main/data/json/system/scada_symbols/leak-sensor.svg @@ -135,7 +135,7 @@ } ] }]]> - + diff --git a/application/src/main/data/json/system/scada_symbols/low-voltage-tower-hp.svg b/application/src/main/data/json/system/scada_symbols/low-voltage-tower-hp.svg index 812e616433..002bd9cfae 100644 --- a/application/src/main/data/json/system/scada_symbols/low-voltage-tower-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/low-voltage-tower-hp.svg @@ -264,7 +264,7 @@ - + diff --git a/application/src/main/data/json/system/scada_symbols/meter.svg b/application/src/main/data/json/system/scada_symbols/meter.svg index 02b2833133..d5fa9f7cd7 100644 --- a/application/src/main/data/json/system/scada_symbols/meter.svg +++ b/application/src/main/data/json/system/scada_symbols/meter.svg @@ -713,7 +713,7 @@ 37% - + diff --git a/application/src/main/data/json/system/scada_symbols/oil-pump-hp.svg b/application/src/main/data/json/system/scada_symbols/oil-pump-hp.svg index e7dfa8f0c8..b1ff6ea2c0 100644 --- a/application/src/main/data/json/system/scada_symbols/oil-pump-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/oil-pump-hp.svg @@ -343,7 +343,7 @@ - + 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 925ea8906a..0ce78af7d9 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 @@ -501,7 +501,7 @@ - + diff --git a/application/src/main/data/json/system/scada_symbols/pool.svg b/application/src/main/data/json/system/scada_symbols/pool.svg index f5d0bd7ad7..6f8b12737c 100644 --- a/application/src/main/data/json/system/scada_symbols/pool.svg +++ b/application/src/main/data/json/system/scada_symbols/pool.svg @@ -292,7 +292,7 @@ 1660 gal - + diff --git a/application/src/main/data/json/system/scada_symbols/power-socket-hp.svg b/application/src/main/data/json/system/scada_symbols/power-socket-hp.svg index 5524b3bb14..2331242475 100644 --- a/application/src/main/data/json/system/scada_symbols/power-socket-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/power-socket-hp.svg @@ -357,7 +357,7 @@ - + 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 046c5c5802..cd15a16577 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 @@ -502,7 +502,7 @@ - + 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 26cee5cd30..1b91a0cd4a 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 @@ -719,5 +719,5 @@ - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg b/application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg index fa0c0fc4d1..7b8e7299b8 100644 --- a/application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg @@ -719,5 +719,5 @@ 26 - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg index d4ddda1895..1d6785d0c1 100644 --- a/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg @@ -537,7 +537,7 @@ 1660 gal - + diff --git a/application/src/main/data/json/system/scada_symbols/small-horizontal-separator-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/small-horizontal-separator-connector-hp.svg index dedc4dc81e..abb6d9b4ab 100644 --- a/application/src/main/data/json/system/scada_symbols/small-horizontal-separator-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/small-horizontal-separator-connector-hp.svg @@ -328,7 +328,7 @@ } ] }]]> - + diff --git a/application/src/main/data/json/system/scada_symbols/small-left-meter.svg b/application/src/main/data/json/system/scada_symbols/small-left-meter.svg index a480412366..129006ffd7 100644 --- a/application/src/main/data/json/system/scada_symbols/small-left-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/small-left-meter.svg @@ -713,5 +713,5 @@ 37% - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/small-vertical-separator-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/small-vertical-separator-connector-hp.svg index b248996612..e9cc074cb6 100644 --- a/application/src/main/data/json/system/scada_symbols/small-vertical-separator-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/small-vertical-separator-connector-hp.svg @@ -328,7 +328,7 @@ } ] }]]> - + diff --git a/application/src/main/data/json/system/scada_symbols/small-vertical-separator-hp.svg b/application/src/main/data/json/system/scada_symbols/small-vertical-separator-hp.svg index 25aec080b3..0c7455e534 100644 --- a/application/src/main/data/json/system/scada_symbols/small-vertical-separator-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/small-vertical-separator-hp.svg @@ -328,7 +328,7 @@ } ] }]]> - + diff --git a/application/src/main/data/json/system/scada_symbols/spherical-tank.svg b/application/src/main/data/json/system/scada_symbols/spherical-tank.svg index cba49c63e0..f5d679fa96 100644 --- a/application/src/main/data/json/system/scada_symbols/spherical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/spherical-tank.svg @@ -572,7 +572,7 @@ 1660 gal - + diff --git a/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg index a78060dfaf..f5b8e8a892 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg @@ -567,7 +567,7 @@ 1660 gal - + diff --git a/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg index 96df175589..0d56901bfe 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg @@ -1364,5 +1364,5 @@ - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/top-light-bulb-hp.svg b/application/src/main/data/json/system/scada_symbols/top-light-bulb-hp.svg index ed6855884a..fb419bac2a 100644 --- a/application/src/main/data/json/system/scada_symbols/top-light-bulb-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/top-light-bulb-hp.svg @@ -332,7 +332,7 @@ - + diff --git a/application/src/main/data/json/system/scada_symbols/turbine-hp.svg b/application/src/main/data/json/system/scada_symbols/turbine-hp.svg index d798b09477..2e0e176cfd 100644 --- a/application/src/main/data/json/system/scada_symbols/turbine-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/turbine-hp.svg @@ -363,7 +363,7 @@ - + diff --git a/application/src/main/data/json/system/scada_symbols/vertical-energy-system-controller-hp.svg b/application/src/main/data/json/system/scada_symbols/vertical-energy-system-controller-hp.svg index cebe949c36..6da68556a2 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-energy-system-controller-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-energy-system-controller-hp.svg @@ -364,7 +364,7 @@ } ] }]]> -Connected +Connected 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 491fb4477e..9dee6cc2af 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 @@ -502,7 +502,7 @@ - + diff --git a/application/src/main/data/json/system/scada_symbols/voltage-stabilizer-hp.svg b/application/src/main/data/json/system/scada_symbols/voltage-stabilizer-hp.svg index aafc2af5a0..2ccad581d4 100644 --- a/application/src/main/data/json/system/scada_symbols/voltage-stabilizer-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/voltage-stabilizer-hp.svg @@ -570,7 +570,7 @@ } ] }]]> -220230inout +220230inout diff --git a/application/src/main/data/json/system/scada_symbols/wind-turbine-cluster-hp.svg b/application/src/main/data/json/system/scada_symbols/wind-turbine-cluster-hp.svg index 74855dd35a..bdc4d23803 100644 --- a/application/src/main/data/json/system/scada_symbols/wind-turbine-cluster-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/wind-turbine-cluster-hp.svg @@ -349,14 +349,14 @@ - + - + diff --git a/application/src/main/data/json/system/scada_symbols/wind-turbine-hp.svg b/application/src/main/data/json/system/scada_symbols/wind-turbine-hp.svg index a4282c16e7..b2c65988da 100644 --- a/application/src/main/data/json/system/scada_symbols/wind-turbine-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/wind-turbine-hp.svg @@ -346,7 +346,7 @@ - + diff --git a/application/src/main/data/json/system/widget_types/bar_chart.json b/application/src/main/data/json/system/widget_types/bar_chart.json index 01f16d96b5..58f6244fd3 100644 --- a/application/src/main/data/json/system/widget_types/bar_chart.json +++ b/application/src/main/data/json/system/widget_types/bar_chart.json @@ -9,9 +9,9 @@ "sizeX": 8, "sizeY": 5, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.timeSeriesChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n chartType: 'bar',\n previewWidth: '80%',\n embedTitlePanel: true,\n hasAdditionalLatestDataKeys: true,\n dataKeySettingsFunction: TbTimeSeriesChart.dataKeySettings('bar'),\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.timeSeriesChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n chartType: 'bar',\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n hasAdditionalLatestDataKeys: true,\n dataKeySettingsFunction: TbTimeSeriesChart.dataKeySettings('bar'),\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", "latestDataKeySettingsSchema": "{}", diff --git a/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json b/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json index a2fc5f5c9d..6972e859b0 100644 --- a/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json +++ b/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json @@ -9,9 +9,9 @@ "sizeX": 8, "sizeY": 5, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.barChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.barChartWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '80%',\n embedTitlePanel: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'humidity', label: 'Humidity', type: 'timeseries' }];\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.barChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.barChartWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'humidity', label: 'Humidity', type: 'timeseries' }];\n }\n };\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", "latestDataKeySettingsSchema": "{}", diff --git a/application/src/main/data/json/system/widget_types/bars.json b/application/src/main/data/json/system/widget_types/bars.json index 481288da9b..ce640c95b1 100644 --- a/application/src/main/data/json/system/widget_types/bars.json +++ b/application/src/main/data/json/system/widget_types/bars.json @@ -9,9 +9,9 @@ "sizeX": 5, "sizeY": 4, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.barChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.barChartWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n previewWidth: '500px',\n previewHeight: '380px',\n embedTitlePanel: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'windPower', label: 'Wind', type: 'timeseries', color: '#08872B' },\n { name: 'solarPower', label: 'Solar', type: 'timeseries', color: '#FF4D5A' },\n { name: 'hydroelectricPower', label: 'Hydroelectric', type: 'timeseries', color: '#FFDE30' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.barChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.barChartWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n previewWidth: '500px',\n previewHeight: '380px',\n embedTitlePanel: true,\n embedActionsPanel: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'windPower', label: 'Wind', type: 'timeseries', color: '#08872B' },\n { name: 'solarPower', label: 'Solar', type: 'timeseries', color: '#FF4D5A' },\n { name: 'hydroelectricPower', label: 'Hydroelectric', type: 'timeseries', color: '#FFDE30' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-bar-chart-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/doughnut.json b/application/src/main/data/json/system/widget_types/doughnut.json index 5065100a91..bf03a75b09 100644 --- a/application/src/main/data/json/system/widget_types/doughnut.json +++ b/application/src/main/data/json/system/widget_types/doughnut.json @@ -9,9 +9,9 @@ "sizeX": 4, "sizeY": 3, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.doughnutWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.doughnutWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n previewWidth: '380px',\n previewHeight: '300px',\n embedTitlePanel: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'windPower', label: 'Wind power', type: 'timeseries', color: '#08872B' },\n { name: 'solarPower', label: 'Solar power', type: 'timeseries', color: '#FF4D5A' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.doughnutWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.doughnutWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n previewWidth: '380px',\n previewHeight: '300px',\n embedTitlePanel: true,\n embedActionsPanel: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'windPower', label: 'Wind power', type: 'timeseries', color: '#08872B' },\n { name: 'solarPower', label: 'Solar power', type: 'timeseries', color: '#FF4D5A' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-doughnut-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/horizontal_doughnut.json b/application/src/main/data/json/system/widget_types/horizontal_doughnut.json index a6e7f4d3bd..050984268c 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_doughnut.json +++ b/application/src/main/data/json/system/widget_types/horizontal_doughnut.json @@ -9,9 +9,9 @@ "sizeX": 4, "sizeY": 2.5, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.doughnutWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.doughnutWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n previewWidth: '380px',\n previewHeight: '220px',\n embedTitlePanel: true,\n horizontal: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'windPower', label: 'Wind power', type: 'timeseries', color: '#08872B' },\n { name: 'solarPower', label: 'Solar power', type: 'timeseries', color: '#FF4D5A' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.doughnutWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.doughnutWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n previewWidth: '380px',\n previewHeight: '220px',\n embedTitlePanel: true,\n embedActionsPanel: true,\n horizontal: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'windPower', label: 'Wind power', type: 'timeseries', color: '#08872B' },\n { name: 'solarPower', label: 'Solar power', type: 'timeseries', color: '#FF4D5A' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-doughnut-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/image_map.json b/application/src/main/data/json/system/widget_types/image_map.json index 2a4fe71696..991c0b463f 100644 --- a/application/src/main/data/json/system/widget_types/image_map.json +++ b/application/src/main/data/json/system/widget_types/image_map.json @@ -9,9 +9,9 @@ "sizeX": 8.5, "sizeY": 6, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true,\n additionalWidgetActionTypes: ['placeMapItem']\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n datasourcesOptional: true,\n additionalWidgetActionTypes: ['placeMapItem']\n };\n}\n", "settingsForm": [], "dataKeySettingsForm": [], "settingsDirective": "tb-map-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/line_chart.json b/application/src/main/data/json/system/widget_types/line_chart.json index 3ef1b83c03..0468c42f8e 100644 --- a/application/src/main/data/json/system/widget_types/line_chart.json +++ b/application/src/main/data/json/system/widget_types/line_chart.json @@ -9,9 +9,9 @@ "sizeX": 8, "sizeY": 5, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.timeSeriesChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n chartType: 'line',\n previewWidth: '80%',\n embedTitlePanel: true,\n hasAdditionalLatestDataKeys: true,\n dataKeySettingsFunction: TbTimeSeriesChart.dataKeySettings('line'),\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.timeSeriesChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n chartType: 'line',\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n hasAdditionalLatestDataKeys: true,\n dataKeySettingsFunction: TbTimeSeriesChart.dataKeySettings('line'),\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", "latestDataKeySettingsSchema": "{}", diff --git a/application/src/main/data/json/system/widget_types/map.json b/application/src/main/data/json/system/widget_types/map.json index b9069d8af6..e498922d36 100644 --- a/application/src/main/data/json/system/widget_types/map.json +++ b/application/src/main/data/json/system/widget_types/map.json @@ -9,9 +9,9 @@ "sizeX": 8.5, "sizeY": 6, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true,\n additionalWidgetActionTypes: ['placeMapItem']\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n datasourcesOptional: true,\n additionalWidgetActionTypes: ['placeMapItem']\n };\n}\n", "settingsForm": [], "dataKeySettingsForm": [], "settingsDirective": "tb-map-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/pie.json b/application/src/main/data/json/system/widget_types/pie.json index 01c96a5077..94e02d06ef 100644 --- a/application/src/main/data/json/system/widget_types/pie.json +++ b/application/src/main/data/json/system/widget_types/pie.json @@ -9,9 +9,9 @@ "sizeX": 5, "sizeY": 4, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.pieChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.pieChartWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n previewWidth: '500px',\n previewHeight: '380px',\n embedTitlePanel: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'windPower', label: 'Wind', type: 'timeseries', color: '#08872B' },\n { name: 'solarPower', label: 'Solar', type: 'timeseries', color: '#FF4D5A' },\n { name: 'hydroelectricPower', label: 'Hydroelectric', type: 'timeseries', color: '#FFDE30' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n\nself.actionSources = function() {\n return {\n 'sliceClick': {\n name: 'widget-action.pie-slice-click',\n multiple: false\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.pieChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.pieChartWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n previewWidth: '500px',\n previewHeight: '380px',\n embedTitlePanel: true,\n embedActionsPanel: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'windPower', label: 'Wind', type: 'timeseries', color: '#08872B' },\n { name: 'solarPower', label: 'Solar', type: 'timeseries', color: '#FF4D5A' },\n { name: 'hydroelectricPower', label: 'Hydroelectric', type: 'timeseries', color: '#FFDE30' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n\nself.actionSources = function() {\n return {\n 'sliceClick': {\n name: 'widget-action.pie-slice-click',\n multiple: false\n }\n };\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-pie-chart-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/point_chart.json b/application/src/main/data/json/system/widget_types/point_chart.json index a3f1ed5f60..74cd137665 100644 --- a/application/src/main/data/json/system/widget_types/point_chart.json +++ b/application/src/main/data/json/system/widget_types/point_chart.json @@ -9,9 +9,9 @@ "sizeX": 8, "sizeY": 5, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.timeSeriesChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n chartType: 'point',\n previewWidth: '80%',\n embedTitlePanel: true,\n hasAdditionalLatestDataKeys: true,\n dataKeySettingsFunction: TbTimeSeriesChart.dataKeySettings('point'),\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.timeSeriesChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n chartType: 'point',\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n hasAdditionalLatestDataKeys: true,\n dataKeySettingsFunction: TbTimeSeriesChart.dataKeySettings('point'),\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", "latestDataKeySettingsSchema": "{}", diff --git a/application/src/main/data/json/system/widget_types/polar_area.json b/application/src/main/data/json/system/widget_types/polar_area.json index 2bdce5d9ad..36fcaee465 100644 --- a/application/src/main/data/json/system/widget_types/polar_area.json +++ b/application/src/main/data/json/system/widget_types/polar_area.json @@ -9,9 +9,9 @@ "sizeX": 5, "sizeY": 4, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.polarAreaChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.polarAreaChartWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n previewWidth: '500px',\n previewHeight: '380px',\n embedTitlePanel: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'windPower', label: 'Wind', type: 'timeseries', color: '#08872B' },\n { name: 'solarPower', label: 'Solar', type: 'timeseries', color: '#FF4D5A' },\n { name: 'hydroelectricPower', label: 'Hydroelectric', type: 'timeseries', color: '#FFDE30' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.polarAreaChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.polarAreaChartWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n previewWidth: '500px',\n previewHeight: '380px',\n embedTitlePanel: true,\n embedActionsPanel: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'windPower', label: 'Wind', type: 'timeseries', color: '#08872B' },\n { name: 'solarPower', label: 'Solar', type: 'timeseries', color: '#FF4D5A' },\n { name: 'hydroelectricPower', label: 'Hydroelectric', type: 'timeseries', color: '#FFDE30' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-polar-area-chart-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/radar.json b/application/src/main/data/json/system/widget_types/radar.json index 07b0fa6981..a6961feb6d 100644 --- a/application/src/main/data/json/system/widget_types/radar.json +++ b/application/src/main/data/json/system/widget_types/radar.json @@ -9,9 +9,9 @@ "sizeX": 5, "sizeY": 4, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.radarChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.radarChartWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n previewWidth: '500px',\n previewHeight: '380px',\n embedTitlePanel: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'windPower', label: 'Wind', type: 'timeseries', color: '#08872B' },\n { name: 'solarPower', label: 'Solar', type: 'timeseries', color: '#FF4D5A' },\n { name: 'hydroelectricPower', label: 'Hydroelectric', type: 'timeseries', color: '#FFDE30' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.radarChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.radarChartWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n previewWidth: '500px',\n previewHeight: '380px',\n embedTitlePanel: true,\n embedActionsPanel: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'windPower', label: 'Wind', type: 'timeseries', color: '#08872B' },\n { name: 'solarPower', label: 'Solar', type: 'timeseries', color: '#FF4D5A' },\n { name: 'hydroelectricPower', label: 'Hydroelectric', type: 'timeseries', color: '#FFDE30' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-radar-chart-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/radial_gauge.json b/application/src/main/data/json/system/widget_types/radial_gauge.json index ce09d31988..b463994347 100644 --- a/application/src/main/data/json/system/widget_types/radial_gauge.json +++ b/application/src/main/data/json/system/widget_types/radial_gauge.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-analogue-radial-gauge-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-radial-gauge-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < -100) {\\n\\tvalue = -100;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":10,\"highlights\":[],\"showUnitTitle\":true,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":10,\"valueInt\":3,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":36,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"minValue\":-100,\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Radial gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < -100) {\\n\\tvalue = -100;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":10,\"highlights\":[],\"showUnitTitle\":true,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":10,\"valueInt\":3,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":36,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"minValue\":-100,\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Radial gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\"}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/range_chart.json b/application/src/main/data/json/system/widget_types/range_chart.json index ea5109873b..911d91876f 100644 --- a/application/src/main/data/json/system/widget_types/range_chart.json +++ b/application/src/main/data/json/system/widget_types/range_chart.json @@ -9,9 +9,9 @@ "sizeX": 8, "sizeY": 5, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.rangeChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.rangeChartWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries' }];\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.rangeChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.rangeChartWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries' }];\n }\n };\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", "latestDataKeySettingsSchema": "{}", diff --git a/application/src/main/data/json/system/widget_types/route_map.json b/application/src/main/data/json/system/widget_types/route_map.json index f1d2cd571c..37346f1157 100644 --- a/application/src/main/data/json/system/widget_types/route_map.json +++ b/application/src/main/data/json/system/widget_types/route_map.json @@ -9,9 +9,9 @@ "sizeX": 8.5, "sizeY": 6, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n trip: true,\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true,\n additionalWidgetActionTypes: ['placeMapItem']\n };\n}", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n trip: true,\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n datasourcesOptional: true,\n additionalWidgetActionTypes: ['placeMapItem']\n };\n}", "settingsForm": [], "dataKeySettingsForm": [], "latestDataKeySettingsForm": [], diff --git a/application/src/main/data/json/system/widget_types/state_chart.json b/application/src/main/data/json/system/widget_types/state_chart.json index ea65f55b43..d2a3031318 100644 --- a/application/src/main/data/json/system/widget_types/state_chart.json +++ b/application/src/main/data/json/system/widget_types/state_chart.json @@ -9,9 +9,9 @@ "sizeX": 8, "sizeY": 5, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.timeSeriesChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n chartType: 'state',\n previewWidth: '80%',\n embedTitlePanel: true,\n hasAdditionalLatestDataKeys: true,\n dataKeySettingsFunction: TbTimeSeriesChart.dataKeySettings('state'),\n defaultDataKeysFunction: function() {\n return [{ name: 'state', label: 'State', type: 'timeseries', units: '', decimals: 0 }];\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.timeSeriesChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n chartType: 'state',\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n hasAdditionalLatestDataKeys: true,\n dataKeySettingsFunction: TbTimeSeriesChart.dataKeySettings('state'),\n defaultDataKeysFunction: function() {\n return [{ name: 'state', label: 'State', type: 'timeseries', units: '', decimals: 0 }];\n }\n };\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", "latestDataKeySettingsSchema": "{}", diff --git a/application/src/main/data/json/system/widget_types/time_series_chart.json b/application/src/main/data/json/system/widget_types/time_series_chart.json index 449fffd11b..d61472b6f9 100644 --- a/application/src/main/data/json/system/widget_types/time_series_chart.json +++ b/application/src/main/data/json/system/widget_types/time_series_chart.json @@ -9,9 +9,9 @@ "sizeX": 8, "sizeY": 5, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.timeSeriesChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '80%',\n embedTitlePanel: true,\n hasAdditionalLatestDataKeys: true,\n dataKeySettingsFunction: TbTimeSeriesChart.dataKeySettings(),\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.timeSeriesChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n hasAdditionalLatestDataKeys: true,\n dataKeySettingsFunction: TbTimeSeriesChart.dataKeySettings(),\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", "latestDataKeySettingsSchema": "{}", diff --git a/application/src/main/data/json/system/widget_types/trip_map.json b/application/src/main/data/json/system/widget_types/trip_map.json index a596e74c1f..8f54f64b6b 100644 --- a/application/src/main/data/json/system/widget_types/trip_map.json +++ b/application/src/main/data/json/system/widget_types/trip_map.json @@ -9,9 +9,9 @@ "sizeX": 8.5, "sizeY": 6, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n trip: true,\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true,\n additionalWidgetActionTypes: ['placeMapItem']\n };\n}", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n trip: true,\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n datasourcesOptional: true,\n additionalWidgetActionTypes: ['placeMapItem']\n };\n}", "settingsForm": [], "dataKeySettingsForm": [], "latestDataKeySettingsForm": [], diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 29c7a084f4..e91fbb823c 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -83,3 +83,13 @@ SET profile_data = profile_data WHERE profile_data->'configuration'->>'maxCalculatedFieldsPerEntity' IS NULL; -- UPDATE TENANT PROFILE CALCULATED FIELD LIMITS END + +-- UPDATE TENANT PROFILE DEBUG DURATION START + +UPDATE tenant_profile +SET profile_data = jsonb_set(profile_data, '{configuration,maxDebugModeDurationMinutes}', '15', true) +WHERE + profile_data->'configuration' ? 'maxDebugModeDurationMinutes' = false + OR (profile_data->'configuration'->>'maxDebugModeDurationMinutes')::int = 0; + +-- UPDATE TENANT PROFILE DEBUG DURATION END diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java index 350a5776cf..2959bfc8eb 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java @@ -21,6 +21,7 @@ import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.TbActorStopReason; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; @@ -47,6 +48,12 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor { } } + @Override + public void destroy(TbActorStopReason stopReason, Throwable cause) throws TbActorException { + log.debug("[{}] Stopping CF entity actor.", processor.tenantId); + processor.stop(); + } + @Override protected boolean doProcessCfMsg(ToCalculatedFieldSystemMsg msg) throws CalculatedFieldException { switch (msg.getMsgType()) { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index a185b71d56..db65248a8f 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -92,6 +92,12 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM this.ctx = ctx; } + public void stop() { + log.info("[{}][{}] Stopping entity actor.", tenantId, entityId); + states.clear(); + ctx.stop(ctx.getSelf()); + } + public void process(CalculatedFieldPartitionChangeMsg msg) { if (!systemContext.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId).isMyPartition()) { log.info("[{}] Stopping entity actor due to change partition event.", entityId); @@ -296,6 +302,8 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, JacksonUtil.writeValueAsString(calculationResult.getResult()), null); } } + } else { + callback.onSuccess(); } } catch (Exception e) { throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArguments()).cause(e).build(); @@ -414,7 +422,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM private Map mapToArgumentsWithFetchedValue(CalculatedFieldCtx ctx, List removedTelemetryKeys) { Map deletedArguments = ctx.getArguments().entrySet().stream() - .filter(entry -> removedTelemetryKeys.contains(entry.getKey())) + .filter(entry -> removedTelemetryKeys.contains(entry.getValue().getRefEntityKey().getKey())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); Map fetchedArgs = cfService.fetchArgsFromDb(tenantId, entityId, deletedArguments); diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java index a5c935e83f..70ed2849e8 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java @@ -20,6 +20,7 @@ import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.TbActorStopReason; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldInitMsg; @@ -52,6 +53,12 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor { } } + @Override + public void destroy(TbActorStopReason stopReason, Throwable cause) throws TbActorException { + log.debug("[{}] Stopping CF manager actor.", processor.tenantId); + processor.stop(); + } + @Override protected boolean doProcessCfMsg(ToCalculatedFieldSystemMsg msg) throws CalculatedFieldException { switch (msg.getMsgType()) { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index fc48e9ad3e..e109e83e4d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -56,7 +56,6 @@ import java.util.concurrent.CopyOnWriteArrayList; import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; - /** * @author Andrew Shvayka */ @@ -92,8 +91,17 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware this.ctx = ctx; } + public void stop() { + log.info("[{}] Stopping CF manager actor.", tenantId); + calculatedFields.values().forEach(CalculatedFieldCtx::stop); + calculatedFields.clear(); + entityIdCalculatedFields.clear(); + entityIdCalculatedFieldLinks.clear(); + ctx.stop(ctx.getSelf()); + } + public void onFieldInitMsg(CalculatedFieldInitMsg msg) throws CalculatedFieldException { - log.info("[{}] Processing CF init message.", msg.getCf().getId()); + log.debug("[{}] Processing CF init message.", msg.getCf().getId()); var cf = msg.getCf(); var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService()); try { @@ -109,7 +117,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } public void onLinkInitMsg(CalculatedFieldLinkInitMsg msg) { - log.info("[{}] Processing CF link init message for entity [{}].", msg.getLink().getCalculatedFieldId(), msg.getLink().getEntityId()); + log.debug("[{}] Processing CF link init message for entity [{}].", msg.getLink().getCalculatedFieldId(), msg.getLink().getEntityId()); var link = msg.getLink(); // We use copy on write lists to safely pass the reference to another actor for the iteration. // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) @@ -122,7 +130,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware var calculatedField = calculatedFields.get(cfId); if (calculatedField != null) { - msg.getState().setRequiredArguments(calculatedField.getArgNames()); + if (msg.getState() != null) { + msg.getState().setRequiredArguments(calculatedField.getArgNames()); + } log.debug("Pushing CF state restore msg to specific actor [{}]", msg.getId().entityId()); getOrCreateActor(msg.getId().entityId()).tell(msg); } else { @@ -131,7 +141,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } public void onEntityLifecycleMsg(CalculatedFieldEntityLifecycleMsg msg) throws CalculatedFieldException { - log.info("Processing entity lifecycle event: [{}] for entity: [{}]", msg.getData().getEvent(), msg.getData().getEntityId()); + log.debug("Processing entity lifecycle event: [{}] for entity: [{}]", msg.getData().getEvent(), msg.getData().getEntityId()); var entityType = msg.getData().getEntityId().getEntityType(); var event = msg.getData().getEvent(); switch (entityType) { @@ -357,6 +367,10 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware log.debug("Received linked telemetry msg from entity [{}]", sourceEntityId); var proto = msg.getProto(); var linksList = proto.getLinksList(); + if (linksList.isEmpty()) { + log.debug("[{}] No CF links to process new telemetry.", msg.getTenantId()); + msg.getCallback().onSuccess(); + } for (var linkProto : linksList) { var link = fromProto(linkProto); var targetEntityId = link.entityId(); diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index 846bde508d..728f715af4 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -28,6 +28,7 @@ import org.thingsboard.server.actors.TbEntityActorId; import org.thingsboard.server.actors.TbEntityTypeActorIdPredicate; import org.thingsboard.server.actors.TbStringActorId; import org.thingsboard.server.actors.calculatedField.CalculatedFieldManagerActorCreator; +import org.thingsboard.server.actors.calculatedField.CalculatedFieldStateRestoreMsg; import org.thingsboard.server.actors.device.DeviceActorCreator; import org.thingsboard.server.actors.ruleChain.RuleChainManagerActor; import org.thingsboard.server.actors.service.ContextBasedCreator; @@ -126,6 +127,9 @@ public class TenantActor extends RuleChainManagerActor { @Override public void destroy(TbActorStopReason stopReason, Throwable cause) { log.info("[{}] Stopping tenant actor.", tenantId); + if (cfActor != null) { + ctx.stop(cfActor.getActorId()); + } } @Override @@ -190,7 +194,11 @@ public class TenantActor extends RuleChainManagerActor { private void onToCalculatedFieldSystemActorMsg(ToCalculatedFieldSystemMsg msg, boolean priority) { if (cfActor == null) { - log.warn("[{}] CF Actor is not initialized.", tenantId); + if (msg instanceof CalculatedFieldStateRestoreMsg) { + log.warn("[{}] CF Actor is not initialized. ToCalculatedFieldSystemMsg: [{}]", tenantId, msg); + } else { + log.debug("[{}] CF Actor is not initialized. ToCalculatedFieldSystemMsg: [{}]", tenantId, msg); + } return; } if (priority) { diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index 2ce4ade587..871798ccd1 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -82,7 +82,7 @@ public class ThingsboardSecurityConfiguration { public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; public static final String WS_ENTRY_POINT = "/api/ws/**"; public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code"; - public static final String DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT = "/api/device-connectivity/mqtts/certificate/download"; + public static final String DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT = "/api/device-connectivity/*/certificate/download"; @Value("${server.http.max_payload_size:/api/image*/**=52428800;/api/resource/**=52428800;/api/**=16777216}") private String maxPayloadSizeConfig; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 0c4352dcea..fdbbb37568 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -122,6 +122,15 @@ public class CalculatedFieldCtx { } } + public void stop() { + if (calculatedFieldScriptEngine != null) { + calculatedFieldScriptEngine.destroy(); + } + if (customExpression != null) { + customExpression.remove(); + } + } + private CalculatedFieldScriptEngine initEngine(TenantId tenantId, String expression, TbelInvokeService tbelInvokeService) { if (tbelInvokeService == null) { throw new IllegalArgumentException("TBEL script engine is disabled!"); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java index 90d4056afc..3620ad3639 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java @@ -97,7 +97,10 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta .scheduler(eventConsumer.getScheduler()) .taskExecutor(eventConsumer.getTaskExecutor()) .build(); - super.stateService = new KafkaQueueStateService<>(eventConsumer, stateConsumer); + super.stateService = KafkaQueueStateService., TbProtoQueueMsg>builder() + .eventConsumer(eventConsumer) + .stateConsumer(stateConsumer) + .build(); this.stateProducer = (TbKafkaProducerTemplate>) queueFactory.createCalculatedFieldStateProducer(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsApiService.java b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsApiService.java index 51c963ed2f..c7e17b62ae 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsApiService.java @@ -76,8 +76,9 @@ public class DefaultEdqsApiService implements EdqsApiService { requestMsg.setCustomerIdLSB(customerId.getId().getLeastSignificantBits()); } - Integer partition = edqsPartitionService.resolvePartition(tenantId); - ListenableFuture> resultFuture = requestTemplate.send(new TbProtoQueueMsg<>(UUID.randomUUID(), requestMsg.build()), partition); + UUID key = UUID.randomUUID(); + Integer partition = edqsPartitionService.resolvePartition(tenantId, key); + ListenableFuture> resultFuture = requestTemplate.send(new TbProtoQueueMsg<>(key, requestMsg.build()), partition); return Futures.transform(resultFuture, msg -> { TransportProtos.EdqsResponseMsg responseMsg = msg.getValue().getResponseMsg(); return JacksonUtil.fromString(responseMsg.getValue(), EdqsResponse.class); diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java index e823dee4e7..cd46c4ba96 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java @@ -59,7 +59,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; import org.thingsboard.server.queue.discovery.HashPartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; -import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.environment.DistributedLock; import org.thingsboard.server.queue.environment.DistributedLockService; import org.thingsboard.server.queue.provider.EdqsClientQueueFactory; @@ -96,10 +95,8 @@ public class DefaultEdqsService implements EdqsService { private void init() { executor = ThingsBoardExecutors.newWorkStealingPool(12, getClass()); eventsProducer = EdqsProducer.builder() - .queue(EdqsQueue.EVENTS) + .producer(queueFactory.createEdqsEventsProducer()) .partitionService(edqsPartitionService) - .topicService(topicService) - .producer(queueFactory.createEdqsMsgProducer(EdqsQueue.EVENTS)) .build(); syncLock = distributedLockService.getLock("edqs_sync"); } @@ -150,9 +147,12 @@ public class DefaultEdqsService implements EdqsService { syncLock.lock(); try { EdqsSyncState syncState = getSyncState(); - if (syncState != null && syncState.getStatus() == EdqsSyncStatus.FINISHED) { - log.info("EDQS sync is already finished"); - return; + if (syncState != null) { + EdqsSyncStatus status = syncState.getStatus(); + if (status == EdqsSyncStatus.FINISHED || status == EdqsSyncStatus.FAILED) { + log.info("EDQS sync is already " + status + ", ignoring the msg"); + return; + } } saveSyncState(EdqsSyncStatus.STARTED); diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java index 4ef552521b..239fd9dc42 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java @@ -17,11 +17,14 @@ package org.thingsboard.server.service.edqs; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; -import org.thingsboard.server.queue.edqs.EdqsQueue; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.queue.edqs.EdqsConfig; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaSettings; import java.util.Collections; +import java.util.stream.Collectors; +import java.util.stream.IntStream; @Service @ConditionalOnExpression("'${queue.edqs.sync.enabled:true}' == 'true' && '${queue.type:null}' == 'kafka'") @@ -29,9 +32,14 @@ public class KafkaEdqsSyncService extends EdqsSyncService { private final boolean syncNeeded; - public KafkaEdqsSyncService(TbKafkaSettings kafkaSettings) { + public KafkaEdqsSyncService(TbKafkaSettings kafkaSettings, EdqsConfig edqsConfig) { TbKafkaAdmin kafkaAdmin = new TbKafkaAdmin(kafkaSettings, Collections.emptyMap()); - this.syncNeeded = kafkaAdmin.isTopicEmpty(EdqsQueue.EVENTS.getTopic()); + this.syncNeeded = kafkaAdmin.areAllTopicsEmpty(IntStream.range(0, edqsConfig.getPartitions()) + .mapToObj(partition -> TopicPartitionInfo.builder() + .topic(edqsConfig.getEventsTopic()) + .partition(partition) + .build().getFullTopicName()) + .collect(Collectors.toSet())); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 870ce3838b..e9ef8c5ace 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -122,6 +122,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import static org.thingsboard.common.util.DebugModeUtil.DEBUG_MODE_DEFAULT_DURATION_MINUTES; import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE; import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.isSigningKeyDefault; import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.validateKeyLength; @@ -199,7 +200,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { tenantProfileService.findOrCreateDefaultTenantProfile(TenantId.SYS_TENANT_ID); TenantProfileData isolatedRuleEngineTenantProfileData = new TenantProfileData(); - isolatedRuleEngineTenantProfileData.setConfiguration(new DefaultTenantProfileConfiguration()); + DefaultTenantProfileConfiguration configuration = new DefaultTenantProfileConfiguration(); + configuration.setMaxDebugModeDurationMinutes(DEBUG_MODE_DEFAULT_DURATION_MINUTES); + isolatedRuleEngineTenantProfileData.setConfiguration(configuration); TenantProfileQueueConfiguration mainQueueConfiguration = new TenantProfileQueueConfiguration(); mainQueueConfiguration.setName(DataConstants.MAIN_QUEUE_NAME); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java index ac62576714..125a06299d 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.queue; -import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; @@ -55,7 +54,7 @@ import org.thingsboard.server.service.cf.CalculatedFieldCache; import org.thingsboard.server.service.cf.CalculatedFieldStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; -import org.thingsboard.server.service.queue.processing.AbstractConsumerService; +import org.thingsboard.server.service.queue.processing.AbstractPartitionBasedConsumerService; import org.thingsboard.server.service.queue.processing.IdMsgPair; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; @@ -72,7 +71,7 @@ import java.util.stream.Collectors; @Service @TbRuleEngineComponent @Slf4j -public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerService implements TbCalculatedFieldConsumerService { +public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBasedConsumerService implements TbCalculatedFieldConsumerService { @Value("${queue.calculated_fields.poll_interval:25}") private long pollInterval; @@ -99,10 +98,8 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer this.stateService = stateService; } - @PostConstruct - public void init() { - super.init("tb-cf"); - + @Override + protected void onStartUp() { var queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME); PartitionedQueueConsumerManager> eventConsumer = PartitionedQueueConsumerManager.>create() .queueKey(queueKey) @@ -129,7 +126,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer } @Override - protected void onTbApplicationEvent(PartitionChangeEvent event) { + protected void onPartitionChangeEvent(PartitionChangeEvent event) { try { event.getNewPartitions().forEach((queueKey, partitions) -> { if (queueKey.getQueueName().equals(DataConstants.CF_QUEUE_NAME)) { @@ -193,6 +190,11 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer return ServiceType.TB_RULE_ENGINE; } + @Override + protected String getPrefix() { + return "tb-cf"; + } + @Override protected long getNotificationPollDuration() { return pollInterval; @@ -210,7 +212,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer @Override protected TbQueueConsumer> createNotificationsConsumer() { - return queueFactory.createToCalculatedFieldNotificationsMsgConsumer(); + return queueFactory.createToCalculatedFieldNotificationMsgConsumer(); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java index 9cc743e510..98f857750a 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.queue; -import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; @@ -50,7 +49,7 @@ import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.cf.CalculatedFieldCache; import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; -import org.thingsboard.server.service.queue.processing.AbstractConsumerService; +import org.thingsboard.server.service.queue.processing.AbstractPartitionBasedConsumerService; import org.thingsboard.server.service.queue.ruleengine.TbRuleEngineConsumerContext; import org.thingsboard.server.service.queue.ruleengine.TbRuleEngineQueueConsumerManager; import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService; @@ -67,7 +66,7 @@ import java.util.stream.Collectors; @Service @TbRuleEngineComponent @Slf4j -public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService implements TbRuleEngineConsumerService { +public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedConsumerService implements TbRuleEngineConsumerService { private final TbRuleEngineConsumerContext ctx; private final QueueService queueService; @@ -93,9 +92,8 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< this.queueService = queueService; } - @PostConstruct - public void init() { - super.init("tb-rule-engine"); + @Override + protected void onStartUp() { List queues = queueService.findAllQueues(); for (Queue configuration : queues) { if (partitionService.isManagedByCurrentService(configuration.getTenantId())) { @@ -106,7 +104,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< } @Override - protected void onTbApplicationEvent(PartitionChangeEvent event) { + protected void onPartitionChangeEvent(PartitionChangeEvent event) { event.getNewPartitions().forEach((queueKey, partitions) -> { if (DataConstants.CF_QUEUE_NAME.equals(queueKey.getQueueName()) || DataConstants.CF_STATES_QUEUE_NAME.equals(queueKey.getQueueName())) { return; @@ -150,6 +148,11 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< return ServiceType.TB_RULE_ENGINE; } + @Override + protected String getPrefix() { + return "tb-rule-engine"; + } + @Override protected long getNotificationPollDuration() { return ctx.getPollDuration(); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java new file mode 100644 index 0000000000..97aa81d41c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java @@ -0,0 +1,93 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.queue.processing; + +import jakarta.annotation.PostConstruct; +import org.springframework.context.ApplicationEventPublisher; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; +import org.thingsboard.server.queue.util.AfterStartUp; +import org.thingsboard.server.service.apiusage.TbApiUsageStateService; +import org.thingsboard.server.service.cf.CalculatedFieldCache; +import org.thingsboard.server.service.profile.TbAssetProfileCache; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; +import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +public abstract class AbstractPartitionBasedConsumerService extends AbstractConsumerService { + + private final Lock startupLock = new ReentrantLock(); + private volatile boolean started = false; + private PartitionChangeEvent lastPartitionChangeEvent; + + public AbstractPartitionBasedConsumerService(ActorSystemContext actorContext, + TbTenantProfileCache tenantProfileCache, + TbDeviceProfileCache deviceProfileCache, + TbAssetProfileCache assetProfileCache, + CalculatedFieldCache calculatedFieldCache, + TbApiUsageStateService apiUsageStateService, + PartitionService partitionService, + ApplicationEventPublisher eventPublisher, + JwtSettingsService jwtSettingsService) { + super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService); + } + + @PostConstruct + public void init() { + super.init(getPrefix()); + } + + @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) + @Override + public void afterStartUp() { + super.afterStartUp(); + onStartUp(); + startupLock.lock(); + try { + onPartitionChangeEvent(lastPartitionChangeEvent); + started = true; + } finally { + startupLock.unlock(); + } + } + + @Override + protected void onTbApplicationEvent(PartitionChangeEvent event) { + if (!started) { + startupLock.lock(); + try { + if (!started) { + lastPartitionChangeEvent = event; + return; + } + } finally { + startupLock.unlock(); + } + } + onPartitionChangeEvent(event); + } + + protected abstract void onStartUp(); + + protected abstract void onPartitionChangeEvent(PartitionChangeEvent event); + + protected abstract String getPrefix(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java index 9e9ca42e83..0f308d85ea 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java @@ -725,7 +725,12 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription()); ctx.setInitialDataSent(true); } else { - update = new EntityDataUpdate(ctx.getCmdId(), null, ctx.getData().getData(), ctx.getMaxEntitiesPerDataSubscription()); + // if ctx has timeseries subscription, timeseries values are cleared after each update and is empty in ctx data, + // so to avoid sending timeseries update with empty map we set it to null + List preparedData = ctx.getData().getData().stream() + .map(entityData -> new EntityData(entityData.getEntityId(), entityData.getLatest(), null)) + .toList(); + update = new EntityDataUpdate(ctx.getCmdId(), null, preparedData, ctx.getMaxEntitiesPerDataSubscription()); } ctx.sendWsMsg(update); } finally { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index c61a993d56..7ede65e375 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1645,12 +1645,12 @@ queue: calculated-field: "${TB_QUEUE_KAFKA_CF_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Calculated Field State topics calculated-field-state: "${TB_QUEUE_KAFKA_CF_STATE_TOPIC_PROPERTIES:retention.ms:-1;segment.bytes:52428800;retention.bytes:104857600000;partitions:1;min.insync.replicas:1;cleanup.policy:compact}" - # Kafka properties for EDQS events topics. Partitions number must be the same as queue.edqs.partitions - edqs-events: "${TB_QUEUE_KAFKA_EDQS_EVENTS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:-1;partitions:12;min.insync.replicas:1}" - # Kafka properties for EDQS requests topic (default: 3 minutes retention). Partitions number must be the same as queue.edqs.partitions - edqs-requests: "${TB_QUEUE_KAFKA_EDQS_REQUESTS_TOPIC_PROPERTIES:retention.ms:180000;segment.bytes:52428800;retention.bytes:1048576000;partitions:12;min.insync.replicas:1}" - # Kafka properties for EDQS state topic (infinite retention, compaction). Partitions number must be the same as queue.edqs.partitions - edqs-state: "${TB_QUEUE_KAFKA_EDQS_STATE_TOPIC_PROPERTIES:retention.ms:-1;segment.bytes:52428800;retention.bytes:-1;partitions:12;min.insync.replicas:1;cleanup.policy:compact}" + # Kafka properties for EDQS events topics + edqs-events: "${TB_QUEUE_KAFKA_EDQS_EVENTS_TOPIC_PROPERTIES:retention.ms:86400000;segment.bytes:52428800;retention.bytes:-1;partitions:1;min.insync.replicas:1}" + # Kafka properties for EDQS requests topic (default: 3 minutes retention) + edqs-requests: "${TB_QUEUE_KAFKA_EDQS_REQUESTS_TOPIC_PROPERTIES:retention.ms:180000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + # Kafka properties for EDQS state topic (infinite retention, compaction) + edqs-state: "${TB_QUEUE_KAFKA_EDQS_STATE_TOPIC_PROPERTIES:retention.ms:-1;segment.bytes:52428800;retention.bytes:-1;partitions:1;min.insync.replicas:1;cleanup.policy:compact}" consumer-stats: # Prints lag between consumer group offset and last messages offset in Kafka topics enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" @@ -1748,6 +1748,10 @@ queue: partitions: "${TB_EDQS_PARTITIONS:12}" # EDQS partitioning strategy: tenant (partition is resolved by tenant id) or none (no specific strategy, resolving by message key) partitioning_strategy: "${TB_EDQS_PARTITIONING_STRATEGY:tenant}" + # EDQS events topic + events_topic: "${TB_EDQS_EVENTS_TOPIC:edqs.events}" + # EDQS state topic + state_topic: "${TB_EDQS_STATE_TOPIC:edqs.state}" # EDQS requests topic requests_topic: "${TB_EDQS_REQUESTS_TOPIC:edqs.requests}" # EDQS responses topic diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 011399e883..90b7ea7c7b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -17,8 +17,6 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; -import org.awaitility.Awaitility; -import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -73,7 +71,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.function.BiPredicate; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java index 50c80d08c7..5264e69bd3 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java @@ -110,7 +110,7 @@ public class EdqsEntityServiceTest extends EntityServiceTest { @Override protected List findByQueryAndCheckTelemetry(EntityDataQuery query, EntityKeyType entityKeyType, String key, List expectedTelemetries) { - return await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> findEntitiesTelemetry(query, entityKeyType, key, expectedTelemetries), + return await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> loadAllData(query, expectedTelemetries.size()), loadedEntities -> loadedEntities.stream().map(entityData -> entityData.getLatest().get(entityKeyType).get(key).getValue()).toList().containsAll(expectedTelemetries)); } diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java index 1e7f5384b5..e5c4009715 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java @@ -1698,6 +1698,19 @@ public class EntityServiceTest extends AbstractControllerTest { query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); findByQueryAndCheckTelemetry(query, EntityKeyType.TIME_SERIES, "temperature", deviceHighTemperatures); + // change sort order to sort by temperature + temperatures.sort(Comparator.naturalOrder()); + List expectedSortedList = temperatures.stream().map(aDouble -> Double.toString(aDouble)).collect(Collectors.toList()); + + EntityDataSortOrder sortByTempOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.TIME_SERIES, "temperature"), EntityDataSortOrder.Direction.ASC); + EntityDataPageLink sortByTempPageLink = new EntityDataPageLink(10, 0, null, sortByTempOrder); + EntityDataQuery querySortByTemp = new EntityDataQuery(filter, sortByTempPageLink, entityFields, latestValues, null); + + List loadedEntities = loadAllData(querySortByTemp, deviceTemperatures.size()); + List entitiesTelemetry = loadedEntities.stream().map(entityData -> entityData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList(); + assertThat(entitiesTelemetry).containsExactlyElementsOf(expectedSortedList); + deviceService.deleteDevicesByTenantId(tenantId); } @@ -2377,14 +2390,14 @@ public class EntityServiceTest extends AbstractControllerTest { } protected List findByQueryAndCheckTelemetry(EntityDataQuery query, EntityKeyType entityKeyType, String key, List expectedTelemetry) { - List loadedEntities = findEntitiesTelemetry(query, entityKeyType, key, expectedTelemetry); + List loadedEntities = loadAllData(query, expectedTelemetry.size()); List entitiesTelemetry = loadedEntities.stream().map(entityData -> entityData.getLatest().get(entityKeyType).get(key).getValue()).toList(); assertThat(entitiesTelemetry).containsExactlyInAnyOrderElementsOf(expectedTelemetry); return loadedEntities; } - protected List findEntitiesTelemetry(EntityDataQuery query, EntityKeyType entityKeyType, String key, List expectedTelemetries) { - PageData data = findByQueryAndCheck(query, expectedTelemetries.size()); + protected List loadAllData(EntityDataQuery query, int expectedSize) { + PageData data = findByQueryAndCheck(query, expectedSize); List loadedEntities = new ArrayList<>(data.getData()); while (data.hasNext()) { query = query.next(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/DataPoint.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/DataPoint.java index a6f30c8004..75829dfbbf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/DataPoint.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/DataPoint.java @@ -17,7 +17,7 @@ package org.thingsboard.server.common.data.edqs; import org.thingsboard.server.common.data.kv.DataType; -public interface DataPoint { +public interface DataPoint extends Comparable { String NOT_SUPPORTED = "Not supported!"; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/CustomerData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/CustomerData.java index bf2a3f6da7..9ce300df6a 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/CustomerData.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/CustomerData.java @@ -50,10 +50,10 @@ public class CustomerData extends BaseEntityData { entitiesById.computeIfAbsent(ed.getEntityType(), et -> new ConcurrentHashMap<>()).put(ed.getId(), ed); } - public boolean remove(EntityData ed) { - var map = entitiesById.get(ed.getEntityType()); + public boolean remove(EntityType entityType, UUID entityId) { + var map = entitiesById.get(entityType); if (map != null) { - return map.remove(ed.getId()) != null; + return map.remove(entityId) != null; } else { return false; } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/AbstractDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/AbstractDataPoint.java index fd2d099281..1fdb731af6 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/AbstractDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/AbstractDataPoint.java @@ -17,6 +17,7 @@ package org.thingsboard.server.edqs.data.dp; import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.edqs.DataPoint; @RequiredArgsConstructor @@ -54,4 +55,9 @@ public abstract class AbstractDataPoint implements DataPoint { return valueToString(); } + @Override + public int compareTo(DataPoint dataPoint) { + return StringUtils.compareIgnoreCase(valueToString(), dataPoint.valueToString()); + } + } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java index 83d91d8f75..70a14917ba 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java @@ -16,6 +16,7 @@ package org.thingsboard.server.edqs.data.dp; import lombok.Getter; +import org.thingsboard.server.common.data.edqs.DataPoint; import org.thingsboard.server.common.data.kv.DataType; public class BoolDataPoint extends AbstractDataPoint { @@ -43,4 +44,8 @@ public class BoolDataPoint extends AbstractDataPoint { return Boolean.toString(value); } + @Override + public int compareTo(DataPoint dataPoint) { + return Boolean.compare(value, dataPoint.getBool()); + } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java index 21b355bc46..2ca8f2c03a 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java @@ -16,6 +16,7 @@ package org.thingsboard.server.edqs.data.dp; import lombok.Getter; +import org.thingsboard.server.common.data.edqs.DataPoint; import org.thingsboard.server.common.data.kv.DataType; public class DoubleDataPoint extends AbstractDataPoint { @@ -43,4 +44,8 @@ public class DoubleDataPoint extends AbstractDataPoint { return Double.toString(value); } + @Override + public int compareTo(DataPoint dataPoint) { + return Double.compare(value, dataPoint.getDouble()); + } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java index 7fbe90e814..92c0a972e7 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java @@ -16,6 +16,7 @@ package org.thingsboard.server.edqs.data.dp; import lombok.Getter; +import org.thingsboard.server.common.data.edqs.DataPoint; import org.thingsboard.server.common.data.kv.DataType; public class LongDataPoint extends AbstractDataPoint { @@ -47,4 +48,9 @@ public class LongDataPoint extends AbstractDataPoint { public String valueToString() { return Long.toString(value); } + + @Override + public int compareTo(DataPoint dataPoint) { + return Long.compare(value, dataPoint.getLong()); + } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index 7ddc9147df..07575220eb 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -62,7 +62,6 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.edqs.EdqsComponent; import org.thingsboard.server.queue.edqs.EdqsConfig; import org.thingsboard.server.queue.edqs.EdqsConfig.EdqsPartitioningStrategy; -import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.edqs.EdqsQueueFactory; import org.thingsboard.server.queue.util.AfterStartUp; @@ -123,8 +122,8 @@ public class EdqsProcessor implements TbQueueHandler, }; eventConsumer = PartitionedQueueConsumerManager.>create() - .queueKey(new QueueKey(ServiceType.EDQS, EdqsQueue.EVENTS.getTopic())) - .topic(EdqsQueue.EVENTS.getTopic()) + .queueKey(new QueueKey(ServiceType.EDQS, config.getEventsTopic())) + .topic(config.getEventsTopic()) .pollInterval(config.getPollInterval()) .msgPackProcessor((msgs, consumer, config) -> { for (TbProtoQueueMsg queueMsg : msgs) { @@ -133,14 +132,14 @@ public class EdqsProcessor implements TbQueueHandler, } try { ToEdqsMsg msg = queueMsg.getValue(); - process(msg, EdqsQueue.EVENTS); + process(msg, true); } catch (Exception t) { log.error("Failed to process message: {}", queueMsg, t); } } consumer.commit(); }) - .consumerCreator((config, partitionId) -> queueFactory.createEdqsMsgConsumer(EdqsQueue.EVENTS)) + .consumerCreator((config, partitionId) -> queueFactory.createEdqsEventsConsumer()) .queueAdmin(queueFactory.getEdqsQueueAdmin()) .consumerExecutor(consumersExecutor) .taskExecutor(taskExecutor) @@ -164,25 +163,27 @@ public class EdqsProcessor implements TbQueueHandler, } try { Set newPartitions = event.getNewPartitions().get(new QueueKey(ServiceType.EDQS)); - Set partitions = newPartitions.stream() - .map(tpi -> tpi.withUseInternalPartition(true)) - .collect(Collectors.toSet()); - stateService.process(withTopic(partitions, EdqsQueue.STATE.getTopic())); + stateService.process(withTopic(newPartitions, config.getStateTopic())); // eventsConsumer's partitions are updated by stateService - responseTemplate.subscribe(withTopic(partitions, config.getRequestsTopic())); // FIXME: we subscribe to partitions before we are ready. implement consumer-per-partition version for request template + responseTemplate.subscribe(withTopic(newPartitions, config.getRequestsTopic())); // TODO: we subscribe to partitions before we are ready. implement consumer-per-partition version for request template Set oldPartitions = event.getOldPartitions().get(new QueueKey(ServiceType.EDQS)); if (CollectionsUtil.isNotEmpty(oldPartitions)) { Set removedPartitions = Sets.difference(oldPartitions, newPartitions).stream() .map(tpi -> tpi.getPartition().orElse(-1)).collect(Collectors.toSet()); - if (config.getPartitioningStrategy() != EdqsPartitioningStrategy.TENANT && !removedPartitions.isEmpty()) { + if (removedPartitions.isEmpty()) { + return; + } + + if (config.getPartitioningStrategy() == EdqsPartitioningStrategy.TENANT) { + repository.clearIf(tenantId -> { + Integer partition = partitionService.resolvePartition(tenantId, null); + return removedPartitions.contains(partition); + }); + } else { log.warn("Partitions {} were removed but shouldn't be (due to NONE partitioning strategy)", removedPartitions); } - repository.clearIf(tenantId -> { - Integer partition = partitionService.resolvePartition(tenantId); - return partition != null && removedPartitions.contains(partition); - }); } } catch (Throwable t) { log.error("Failed to handle partition change event {}", event, t); @@ -233,7 +234,7 @@ public class EdqsProcessor implements TbQueueHandler, return response; } - public void process(ToEdqsMsg edqsMsg, EdqsQueue queue) { + public void process(ToEdqsMsg edqsMsg, boolean backup) { log.trace("Processing message: {}", edqsMsg); if (edqsMsg.hasEventMsg()) { EdqsEventMsg eventMsg = edqsMsg.getEventMsg(); @@ -250,7 +251,7 @@ public class EdqsProcessor implements TbQueueHandler, } else if (!ObjectType.unversionedTypes.contains(objectType)) { log.warn("[{}] {} {} doesn't have version", tenantId, objectType, key); } - if (queue != EdqsQueue.STATE) { + if (backup) { stateService.save(tenantId, objectType, key, eventType, edqsMsg); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java index be1f0481be..cc4f913d38 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java @@ -16,6 +16,7 @@ package org.thingsboard.server.edqs.processor; import lombok.Builder; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.common.errors.RecordTooLargeException; import org.thingsboard.server.common.data.ObjectType; @@ -27,60 +28,41 @@ import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.TopicService; -import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; @Slf4j +@Builder +@RequiredArgsConstructor public class EdqsProducer { - private final EdqsQueue queue; - private final EdqsPartitionService partitionService; - private final TopicService topicService; - private final TbQueueProducer> producer; - - @Builder - public EdqsProducer(EdqsQueue queue, - EdqsPartitionService partitionService, - TopicService topicService, - TbQueueProducer> producer) { - this.queue = queue; - this.partitionService = partitionService; - this.topicService = topicService; - this.producer = producer; - } + private final EdqsPartitionService partitionService; public void send(TenantId tenantId, ObjectType type, String key, ToEdqsMsg msg) { - String topic = topicService.buildTopicName(queue.getTopic()); + TopicPartitionInfo tpi = TopicPartitionInfo.builder() + .topic(producer.getDefaultTopic()) + .partition(partitionService.resolvePartition(tenantId, key)) + .build(); TbQueueCallback callback = new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { - log.trace("[{}][{}][{}] Published msg to {}: {}", tenantId, type, key, topic, msg); + log.trace("[{}][{}][{}] Published msg to {}: {}", tenantId, type, key, tpi, msg); } @Override public void onFailure(Throwable t) { if (t instanceof RecordTooLargeException) { if (!log.isDebugEnabled()) { - log.warn("[{}][{}][{}] Failed to publish msg to {}", tenantId, type, key, topic, t); // not logging the whole message + log.warn("[{}][{}][{}] Failed to publish msg to {}", tenantId, type, key, tpi, t); // not logging the whole message return; } } - log.warn("[{}][{}][{}] Failed to publish msg to {}: {}", tenantId, type, key, topic, msg, t); + log.warn("[{}][{}][{}] Failed to publish msg to {}: {}", tenantId, type, key, tpi, msg, t); } }; if (producer instanceof TbKafkaProducerTemplate> kafkaProducer) { - TopicPartitionInfo tpi = TopicPartitionInfo.builder() - .topic(topic) - .partition(partitionService.resolvePartition(tenantId)) - .useInternalPartition(true) - .build(); kafkaProducer.send(tpi, key, new TbProtoQueueMsg<>(null, msg), callback); // specifying custom key for compaction } else { - TopicPartitionInfo tpi = TopicPartitionInfo.builder() - .topic(topic) - .build(); producer.send(tpi, new TbProtoQueueMsg<>(null, msg), callback); } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/SortableEntityData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/SortableEntityData.java index 026c470ce6..18936a2696 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/SortableEntityData.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/SortableEntityData.java @@ -16,6 +16,7 @@ package org.thingsboard.server.edqs.query; import lombok.Data; +import org.thingsboard.server.common.data.edqs.DataPoint; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.edqs.data.EntityData; @@ -26,7 +27,7 @@ import java.util.UUID; public class SortableEntityData { private final EntityData entityData; - private String sortValue; + private DataPoint sortValue; public UUID getId(){ return entityData.getId(); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractQueryProcessor.java index e4cded3e3e..0c83ae4e61 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractQueryProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractQueryProcessor.java @@ -16,6 +16,7 @@ package org.thingsboard.server.edqs.query.processor; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edqs.DataPoint; import org.thingsboard.server.common.data.permission.QueryContext; import org.thingsboard.server.common.data.query.EntityFilter; import org.thingsboard.server.edqs.data.EntityData; @@ -50,7 +51,7 @@ public abstract class AbstractQueryProcessor implements protected SortableEntityData toSortData(EntityData ed) { SortableEntityData sortData = new SortableEntityData(ed); - sortData.setSortValue(getSortValue(ed, sortKey)); + sortData.setSortValue(getSortValue(ed, sortKey, ctx)); return sortData; } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractRelationQueryProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractRelationQueryProcessor.java index 8ee7338a4f..2842d57ff0 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractRelationQueryProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractRelationQueryProcessor.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.Queue; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; public abstract class AbstractRelationQueryProcessor extends AbstractQueryProcessor { @@ -89,7 +90,7 @@ public abstract class AbstractRelationQueryProcessor ext private List processTenantQuery(Set> entities) { return entities.stream() .map(this::toSortData) - .toList(); + .collect(Collectors.toList()); } private List processCustomerQuery(Set> entities) { diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java index 870574a786..b9f3589280 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java @@ -150,8 +150,9 @@ public class TenantRepo { } } else if (RelationTypeGroup.DASHBOARD.equals(entity.getTypeGroup())) { if (EntityRelation.CONTAINS_TYPE.equals(entity.getType()) && entity.getFrom().getEntityType() == EntityType.CUSTOMER) { - ((CustomerData) getEntityMap(EntityType.CUSTOMER).computeIfAbsent(entity.getFrom().getId(), CustomerData::new)) - .addOrUpdate(getEntityMap(EntityType.DASHBOARD).get(entity.getTo().getId())); + CustomerData customerData = (CustomerData) getOrCreate(entity.getFrom()); + EntityData dashboardData = getOrCreate(entity.getTo()); + customerData.addOrUpdate(dashboardData); } } } finally { @@ -170,8 +171,10 @@ public class TenantRepo { } } else if (RelationTypeGroup.DASHBOARD.equals(entityRelation.getTypeGroup())) { if (EntityRelation.CONTAINS_TYPE.equals(entityRelation.getType()) && entityRelation.getFrom().getEntityType() == EntityType.CUSTOMER) { - ((CustomerData) getEntityMap(EntityType.CUSTOMER).computeIfAbsent(entityRelation.getFrom().getId(), CustomerData::new)) - .remove(getEntityMap(EntityType.DASHBOARD).get(entityRelation.getTo().getId())); + CustomerData customerData = (CustomerData) get(entityRelation.getFrom()); + if (customerData != null) { + customerData.remove(EntityType.DASHBOARD, entityRelation.getTo().getId()); + } } } } @@ -197,13 +200,13 @@ public class TenantRepo { entityData.setCustomerId(newCustomerId); if (entityIdMismatch(oldCustomerId, newCustomerId)) { if (oldCustomerId != null) { - CustomerData old = (CustomerData) getEntityMap(EntityType.CUSTOMER).get(oldCustomerId); + CustomerData old = (CustomerData) get(EntityType.CUSTOMER, oldCustomerId); if (old != null) { - old.remove(entityData); + old.remove(entityType, entityId); } } if (newCustomerId != null) { - CustomerData newData = (CustomerData) getEntityMap(EntityType.CUSTOMER).computeIfAbsent(newCustomerId, CustomerData::new); + CustomerData newData = (CustomerData) getOrCreate(EntityType.CUSTOMER, newCustomerId); newData.addOrUpdate(entityData); } } @@ -223,11 +226,12 @@ public class TenantRepo { getEntitySet(entityType).remove(removed); } edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.fromEntityType(entityType), EdqsEventType.DELETED)); + UUID customerId = removed.getCustomerId(); if (customerId != null) { - CustomerData customerData = (CustomerData) getEntityMap(EntityType.CUSTOMER).get(customerId); + CustomerData customerData = (CustomerData) get(EntityType.CUSTOMER, customerId); if (customerData != null) { - customerData.remove(removed); + customerData.remove(entityType, entityId); } } } @@ -303,7 +307,11 @@ public class TenantRepo { } private EntityData get(EntityId entityId) { - return getEntityMap(entityId.getEntityType()).get(entityId.getId()); + return get(entityId.getEntityType(), entityId.getId()); + } + + private EntityData get(EntityType entityType, UUID entityId) { + return getEntityMap(entityType).get(entityId); } private EntityData constructEntityData(EntityType entityType, UUID id) { @@ -425,7 +433,7 @@ public class TenantRepo { EntityType entityType = entityId.getEntityType(); return switch (entityType) { case CUSTOMER, TENANT -> { - EntityFields fields = getEntityMap(entityType).get(entityId.getId()).getFields(); + EntityFields fields = get(entityId).getFields(); yield fields != null ? fields.getName() : ""; } default -> throw new RuntimeException("Unsupported entity type: " + entityType); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsPartitionService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsPartitionService.java index 94e9437650..e2fbf9a981 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsPartitionService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsPartitionService.java @@ -29,11 +29,14 @@ public class EdqsPartitionService { private final HashPartitionService hashPartitionService; private final EdqsConfig edqsConfig; - public Integer resolvePartition(TenantId tenantId) { + public Integer resolvePartition(TenantId tenantId, Object key) { if (edqsConfig.getPartitioningStrategy() == EdqsPartitioningStrategy.TENANT) { return hashPartitionService.resolvePartitionIndex(tenantId.getId(), edqsConfig.getPartitions()); } else { - return null; + if (key == null) { + throw new IllegalArgumentException("Partitioning key is missing but partitioning strategy is not TENANT"); + } + return hashPartitionService.resolvePartitionIndex(key.toString(), edqsConfig.getPartitions()); } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java index 85b8e92387..efdb1ead1c 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java @@ -36,15 +36,19 @@ import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.common.state.KafkaQueueStateService; import org.thingsboard.server.queue.common.state.QueueStateService; import org.thingsboard.server.queue.discovery.QueueKey; -import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.edqs.EdqsConfig; -import org.thingsboard.server.queue.edqs.EdqsQueue; -import org.thingsboard.server.queue.edqs.EdqsQueueFactory; import org.thingsboard.server.queue.edqs.KafkaEdqsComponent; +import org.thingsboard.server.queue.edqs.KafkaEdqsQueueFactory; +import org.thingsboard.server.queue.kafka.TbKafkaAdmin; +import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.IntStream; @Service @RequiredArgsConstructor @@ -54,8 +58,7 @@ public class KafkaEdqsStateService implements EdqsStateService { private final EdqsConfig config; private final EdqsPartitionService partitionService; - private final EdqsQueueFactory queueFactory; - private final TopicService topicService; + private final KafkaEdqsQueueFactory queueFactory; @Autowired @Lazy private EdqsProcessor edqsProcessor; @@ -71,15 +74,16 @@ public class KafkaEdqsStateService implements EdqsStateService { @Override public void init(PartitionedQueueConsumerManager> eventConsumer) { + TbKafkaAdmin queueAdmin = queueFactory.getEdqsQueueAdmin(); stateConsumer = PartitionedQueueConsumerManager.>create() - .queueKey(new QueueKey(ServiceType.EDQS, EdqsQueue.STATE.getTopic())) - .topic(EdqsQueue.STATE.getTopic()) + .queueKey(new QueueKey(ServiceType.EDQS, config.getStateTopic())) + .topic(config.getStateTopic()) .pollInterval(config.getPollInterval()) .msgPackProcessor((msgs, consumer, config) -> { for (TbProtoQueueMsg queueMsg : msgs) { try { ToEdqsMsg msg = queueMsg.getValue(); - edqsProcessor.process(msg, EdqsQueue.STATE); + edqsProcessor.process(msg, false); if (stateReadCount.incrementAndGet() % 100000 == 0) { log.info("[state] Processed {} msgs", stateReadCount.get()); } @@ -89,15 +93,15 @@ public class KafkaEdqsStateService implements EdqsStateService { } consumer.commit(); }) - .consumerCreator((config, partitionId) -> queueFactory.createEdqsMsgConsumer(EdqsQueue.STATE)) - .queueAdmin(queueFactory.getEdqsQueueAdmin()) + .consumerCreator((config, partitionId) -> queueFactory.createEdqsStateConsumer()) + .queueAdmin(queueAdmin) .consumerExecutor(eventConsumer.getConsumerExecutor()) .taskExecutor(eventConsumer.getTaskExecutor()) .scheduler(eventConsumer.getScheduler()) .uncaughtErrorHandler(edqsProcessor.getErrorHandler()) .build(); - queueStateService = new KafkaQueueStateService<>(eventConsumer, stateConsumer); + TbKafkaConsumerTemplate> eventsToBackupKafkaConsumer = queueFactory.createEdqsEventsToBackupConsumer(); eventsToBackupConsumer = QueueConsumerManager.>builder() .name("edqs-events-to-backup-consumer") .pollInterval(config.getPollInterval()) @@ -135,23 +139,47 @@ public class KafkaEdqsStateService implements EdqsStateService { } consumer.commit(); }) - .consumerCreator(() -> queueFactory.createEdqsMsgConsumer(EdqsQueue.EVENTS, "events-to-backup-consumer-group")) // shared by all instances consumer group + .consumerCreator(() -> eventsToBackupKafkaConsumer) .consumerExecutor(eventConsumer.getConsumerExecutor()) .threadPrefix("edqs-events-to-backup") .build(); stateProducer = EdqsProducer.builder() - .queue(EdqsQueue.STATE) + .producer(queueFactory.createEdqsStateProducer()) .partitionService(partitionService) - .topicService(topicService) - .producer(queueFactory.createEdqsMsgProducer(EdqsQueue.STATE)) + .build(); + + queueStateService = KafkaQueueStateService., TbProtoQueueMsg>builder() + .eventConsumer(eventConsumer) + .stateConsumer(stateConsumer) + .eventsStartOffsetsProvider(() -> { + // taking start offsets for events topics from the events-to-backup consumer group, + // since eventConsumer doesn't use consumer group management and thus offset tracking + // (because we need to be able to consume the same topic-partition by multiple instances) + Map offsets = new HashMap<>(); + try { + queueAdmin.getConsumerGroupOffsets(eventsToBackupKafkaConsumer.getGroupId()) + .forEach((topicPartition, offsetAndMetadata) -> { + offsets.put(topicPartition.topic(), offsetAndMetadata.offset()); + }); + } catch (Exception e) { + log.error("Failed to get consumer group offsets for {}", eventsToBackupKafkaConsumer.getGroupId(), e); + } + return offsets; + }) .build(); } @Override public void process(Set partitions) { if (queueStateService.getPartitions().isEmpty()) { - eventsToBackupConsumer.subscribe(); + Set allPartitions = IntStream.range(0, config.getPartitions()) + .mapToObj(partition -> TopicPartitionInfo.builder() + .topic(config.getEventsTopic()) + .partition(partition) + .build()) + .collect(Collectors.toSet()); + eventsToBackupConsumer.subscribe(allPartitions); eventsToBackupConsumer.launch(); } queueStateService.update(new QueueKey(ServiceType.EDQS), partitions); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java index 383115ddf1..cde21edfaf 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java @@ -29,7 +29,6 @@ import org.thingsboard.server.edqs.util.EdqsRocksDb; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; -import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.edqs.InMemoryEdqsComponent; import java.util.Set; @@ -61,14 +60,14 @@ public class LocalEdqsStateService implements EdqsStateService { try { ToEdqsMsg edqsMsg = ToEdqsMsg.parseFrom(value); log.trace("[{}] Restored msg from RocksDB: {}", key, edqsMsg); - processor.process(edqsMsg, EdqsQueue.STATE); + processor.process(edqsMsg, false); } catch (Exception e) { log.error("[{}] Failed to restore value", key, e); } }); log.info("Restore completed"); } - eventConsumer.update(withTopic(partitions, EdqsQueue.EVENTS.getTopic())); + eventConsumer.update(withTopic(partitions, eventConsumer.getTopic())); this.partitions = partitions; } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java index 3bf12752a0..71b58d759a 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java @@ -67,10 +67,10 @@ import static org.thingsboard.server.common.data.query.ComplexFilterPredicate.Co @Slf4j public class RepositoryUtils { - public static final Comparator SORT_ASC = Comparator.comparing((SortableEntityData sed) -> Optional.ofNullable(sed.getSortValue()).orElse(""), String.CASE_INSENSITIVE_ORDER) + public static final Comparator SORT_ASC = Comparator.comparing(SortableEntityData::getSortValue, Comparator.nullsFirst(Comparator.naturalOrder())) .thenComparing(sp -> sp.getId().toString()); - public static final Comparator SORT_DESC = Comparator.comparing((SortableEntityData sed) -> Optional.ofNullable(sed.getSortValue()).orElse(""), String.CASE_INSENSITIVE_ORDER) + public static final Comparator SORT_DESC = Comparator.comparing(SortableEntityData::getSortValue, Comparator.nullsFirst(Comparator.naturalOrder())) .thenComparing(sp -> sp.getId().toString()).reversed(); public static EntityType resolveEntityType(EntityFilter entityFilter) { @@ -348,24 +348,11 @@ public class RepositoryUtils { } } - public static String getSortValue(EntityData entity, DataKey sortKey) { + public static DataPoint getSortValue(EntityData entity, DataKey sortKey, QueryContext queryContext) { if (sortKey == null) { return null; } - switch (sortKey.type()) { - case ENTITY_FIELD -> { - return entity.getField(sortKey.key()); - } - case ATTRIBUTE, CLIENT_ATTRIBUTE, SHARED_ATTRIBUTE, SERVER_ATTRIBUTE -> { - var dp = entity.getAttr(sortKey.keyId(), sortKey.type()); - return dp != null ? dp.valueToString() : ""; - } - case TIME_SERIES -> { - var dp = entity.getTs(sortKey.keyId()); - return dp != null ? dp.valueToString() : ""; - } - default -> throw new IllegalStateException("toSortKey is not implemented for type: " + sortKey.type()); - } + return entity.getDataPoint(sortKey, queryContext); } public static boolean checkFilters(EdqsQuery query, EntityData entity) { diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java index f09826e9b6..b18debaf49 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java @@ -90,9 +90,7 @@ public class TopicPartitionInfo { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TopicPartitionInfo that = (TopicPartitionInfo) o; - return topic.equals(that.topic) && - Objects.equals(tenantId, that.tenantId) && - Objects.equals(partition, that.partition) && + return Objects.equals(partition, that.partition) && fullTopicName.equals(that.fullTopicName); } diff --git a/common/message/src/test/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfoTest.java b/common/message/src/test/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfoTest.java index ec1e106dd8..f222a5a094 100644 --- a/common/message/src/test/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfoTest.java +++ b/common/message/src/test/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfoTest.java @@ -18,12 +18,16 @@ package org.thingsboard.server.common.msg.queue; import org.junit.jupiter.api.Test; import org.thingsboard.server.common.data.id.TenantId; +import java.util.UUID; + import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; public class TopicPartitionInfoTest { + private final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + @Test public void givenTopicPartitionInfo_whenEquals_thenTrue() { @@ -52,13 +56,13 @@ public class TopicPartitionInfoTest { assertThat(TopicPartitionInfo.builder() .topic("tb_core") - .tenantId(TenantId.SYS_TENANT_ID) + .tenantId(tenantId) .partition(4) .myPartition(true) //will ignored on equals .build() , is(TopicPartitionInfo.builder() .topic("tb_core") - .tenantId(TenantId.SYS_TENANT_ID) + .tenantId(tenantId) .partition(4) .myPartition(true) //will ignored on equals .build())); @@ -109,7 +113,7 @@ public class TopicPartitionInfoTest { assertThat(TopicPartitionInfo.builder() .topic("tb_core") - .tenantId(TenantId.SYS_TENANT_ID) + .tenantId(tenantId) .partition(4) .myPartition(true) //will ignored on equals .build() @@ -117,7 +121,7 @@ public class TopicPartitionInfoTest { assertThat(TopicPartitionInfo.builder() .topic("tb_core") - .tenantId(TenantId.SYS_TENANT_ID) + .tenantId(tenantId) .partition(4) .myPartition(false) //will ignored on equals .build() diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplate.java index 4efb297491..1beb505595 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplate.java @@ -263,7 +263,6 @@ public class DefaultTbQueueRequestTemplate { @@ -296,7 +298,7 @@ public class MainQueueConsumerManager removedPartitions) { @@ -304,13 +306,19 @@ public class MainQueueConsumerManager Optional.ofNullable(consumers.remove(tpi)).ifPresent(TbQueueConsumerTask::awaitCompletion)); } - protected void addPartitions(Set partitions, Consumer onStop) { + protected void addPartitions(Set partitions, Consumer onStop, Function startOffsetProvider) { partitions.forEach(tpi -> { Integer partitionId = tpi.getPartition().orElse(-1); String key = queueKey + "-" + partitionId; Runnable callback = onStop != null ? () -> onStop.accept(tpi) : null; - TbQueueConsumerTask consumer = new TbQueueConsumerTask<>(key, () -> consumerCreator.apply(config, partitionId), callback); + TbQueueConsumerTask consumer = new TbQueueConsumerTask<>(key, () -> { + TbQueueConsumer queueConsumer = consumerCreator.apply(config, partitionId); + if (startOffsetProvider != null && queueConsumer instanceof TbKafkaConsumerTemplate kafkaConsumer) { + kafkaConsumer.setStartOffsetProvider(startOffsetProvider); + } + return queueConsumer; + }, callback); consumers.put(tpi, consumer); consumer.subscribe(Set.of(tpi)); launchConsumer(consumer); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java index f25a98adf4..0de1e53753 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java @@ -33,6 +33,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledExecutorService; import java.util.function.BiFunction; import java.util.function.Consumer; +import java.util.function.Function; @Slf4j public class PartitionedQueueConsumerManager extends MainQueueConsumerManager { @@ -57,7 +58,7 @@ public class PartitionedQueueConsumerManager extends MainQ protected void processTask(TbQueueConsumerManagerTask task) { if (task instanceof AddPartitionsTask addPartitionsTask) { log.info("[{}] Added partitions: {}", queueKey, addPartitionsTask.partitions()); - consumerWrapper.addPartitions(addPartitionsTask.partitions(), addPartitionsTask.onStop()); + consumerWrapper.addPartitions(addPartitionsTask.partitions(), addPartitionsTask.onStop(), addPartitionsTask.startOffsetProvider()); } else if (task instanceof RemovePartitionsTask removePartitionsTask) { log.info("[{}] Removed partitions: {}", queueKey, removePartitionsTask.partitions()); consumerWrapper.removePartitions(removePartitionsTask.partitions()); @@ -76,11 +77,11 @@ public class PartitionedQueueConsumerManager extends MainQ } public void addPartitions(Set partitions) { - addPartitions(partitions, null); + addPartitions(partitions, null, null); } - public void addPartitions(Set partitions, Consumer onStop) { - addTask(new AddPartitionsTask(partitions, onStop)); + public void addPartitions(Set partitions, Consumer onStop, Function startOffsetProvider) { + addTask(new AddPartitionsTask(partitions, onStop, startOffsetProvider)); } public void removePartitions(Set partitions) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java index e0dd9b808b..a287a391af 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Function; public interface TbQueueConsumerManagerTask { @@ -46,7 +47,9 @@ public interface TbQueueConsumerManagerTask { } } - record AddPartitionsTask(Set partitions, Consumer onStop) implements TbQueueConsumerManagerTask { + record AddPartitionsTask(Set partitions, + Consumer onStop, + Function startOffsetProvider) implements TbQueueConsumerManagerTask { @Override public QueueTaskType getType() { return QueueTaskType.ADD_PARTITIONS; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java index 9adc6bb996..bf02afe86c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java @@ -15,13 +15,16 @@ */ package org.thingsboard.server.queue.common.state; +import lombok.Builder; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.TbQueueMsg; import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; import org.thingsboard.server.queue.discovery.QueueKey; +import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTopic; @@ -29,14 +32,21 @@ import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTop public class KafkaQueueStateService extends QueueStateService { private final PartitionedQueueConsumerManager stateConsumer; + private final Supplier> eventsStartOffsetsProvider; - public KafkaQueueStateService(PartitionedQueueConsumerManager eventConsumer, PartitionedQueueConsumerManager stateConsumer) { + @Builder + public KafkaQueueStateService(PartitionedQueueConsumerManager eventConsumer, + PartitionedQueueConsumerManager stateConsumer, + Supplier> eventsStartOffsetsProvider) { super(eventConsumer); this.stateConsumer = stateConsumer; + this.eventsStartOffsetsProvider = eventsStartOffsetsProvider; } @Override protected void addPartitions(QueueKey queueKey, Set partitions) { + Map eventsStartOffsets = eventsStartOffsetsProvider != null ? eventsStartOffsetsProvider.get() : null; // remembering the offsets before subscribing to states + Set statePartitions = withTopic(partitions, stateConsumer.getTopic()); partitionsInProgress.addAll(statePartitions); stateConsumer.addPartitions(statePartitions, statePartition -> { @@ -51,12 +61,12 @@ public class KafkaQueueStateService TopicPartitionInfo eventPartition = statePartition.withTopic(eventConsumer.getTopic()); if (this.partitions.get(queueKey).contains(eventPartition)) { - eventConsumer.addPartitions(Set.of(eventPartition)); + eventConsumer.addPartitions(Set.of(eventPartition), null, eventsStartOffsets != null ? eventsStartOffsets::get : null); } } finally { readLock.unlock(); } - }); + }, null); } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 3a76d50825..7186bf7055 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -39,6 +39,7 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.discovery.event.ServiceListChangedEvent; import org.thingsboard.server.queue.util.AfterStartUp; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -559,7 +560,15 @@ public class HashPartitionService implements PartitionService { @Override public int resolvePartitionIndex(UUID entityId, int partitions) { - int hash = hash(entityId); + return resolvePartitionIndex(hash(entityId), partitions); + } + + @Override + public int resolvePartitionIndex(String key, int partitions) { + return resolvePartitionIndex(hash(key), partitions); + } + + private int resolvePartitionIndex(int hash, int partitions) { return Math.abs(hash % partitions); } @@ -725,6 +734,12 @@ public class HashPartitionService implements PartitionService { .hash().asInt(); } + private int hash(String key) { + return hashFunction.newHasher() + .putString(key, StandardCharsets.UTF_8) + .hash().asInt(); + } + public static HashFunction forName(String name) { return switch (name) { case "murmur3_32" -> Hashing.murmur3_32(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java index 7abd68e25f..5dda413f17 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java @@ -79,4 +79,6 @@ public interface PartitionService { int resolvePartitionIndex(UUID entityId, int partitions); + int resolvePartitionIndex(String key, int partitions); + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java index 5992083d85..2a0fad0643 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java @@ -103,6 +103,9 @@ public class TopicService { } public String buildTopicName(String topic) { + if (topic == null) { + return null; + } return prefix.isBlank() ? topic : prefix + "." + topic; } @@ -113,9 +116,9 @@ public class TopicService { public String buildConsumerGroupId(String servicePrefix, TenantId tenantId, String queueName, Integer partitionId) { return this.buildTopicName( servicePrefix + queueName - + (tenantId.isSysTenantId() ? "" : ("-isolated-" + tenantId)) - + "-consumer" - + suffix(partitionId)); + + (tenantId.isSysTenantId() ? "" : ("-isolated-" + tenantId)) + + "-consumer" + + suffix(partitionId)); } String suffix(Integer partitionId) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java index f7a4d2abf6..cf9f27ee39 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java @@ -315,7 +315,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi ScheduledFuture task = delayedTasks.remove(serviceId); if (task != null) { if (task.cancel(false)) { - log.debug("[{}] Recalculate partitions ignored. Service was restarted in time [{}].", + log.info("[{}] Recalculate partitions ignored. Service was restarted in time [{}].", serviceId, serviceTypesList); } else { log.debug("[{}] Going to recalculate partitions. Service was not restarted in time [{}]!", diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java index e4e1e81815..401b451f59 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java @@ -30,6 +30,10 @@ public class EdqsConfig { @Value("#{'${queue.edqs.partitioning_strategy:tenant}'.toUpperCase()}") private EdqsPartitioningStrategy partitioningStrategy; + @Value("${queue.edqs.events_topic:edqs.events}") + private String eventsTopic; + @Value("${queue.edqs.state_topic:edqs.state}") + private String stateTopic; @Value("${queue.edqs.requests_topic:edqs.requests}") private String requestsTopic; @Value("${queue.edqs.responses_topic:edqs.responses}") diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueue.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueue.java deleted file mode 100644 index d859b50994..0000000000 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueue.java +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright © 2016-2025 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.queue.edqs; - -import lombok.Getter; - -@Getter -public enum EdqsQueue { - - EVENTS("edqs.events", false, false), - STATE("edqs.state", true, true); - - private final String topic; - private final boolean readFromBeginning; - private final boolean stopWhenRead; - - EdqsQueue(String topic, boolean readFromBeginning, boolean stopWhenRead) { - this.topic = topic; - this.readFromBeginning = readFromBeginning; - this.stopWhenRead = stopWhenRead; - } - -} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueueFactory.java index b5541c740b..5c0d68779a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueueFactory.java @@ -25,11 +25,13 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; public interface EdqsQueueFactory { - TbQueueConsumer> createEdqsMsgConsumer(EdqsQueue queue); + TbQueueConsumer> createEdqsEventsConsumer(); - TbQueueConsumer> createEdqsMsgConsumer(EdqsQueue queue, String group); + TbQueueConsumer> createEdqsEventsToBackupConsumer(); - TbQueueProducer> createEdqsMsgProducer(EdqsQueue queue); + TbQueueConsumer> createEdqsStateConsumer(); + + TbQueueProducer> createEdqsStateProducer(); TbQueueResponseTemplate, TbProtoQueueMsg> createEdqsResponseTemplate(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java index 0801399c14..8c670e66c0 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java @@ -43,24 +43,23 @@ public class InMemoryEdqsQueueFactory implements EdqsQueueFactory { private final TbQueueAdmin queueAdmin; @Override - public TbQueueConsumer> createEdqsMsgConsumer(EdqsQueue queue) { - if (queue == EdqsQueue.STATE) { - throw new UnsupportedOperationException(); - } - return new InMemoryTbQueueConsumer<>(storage, queue.getTopic()); + public TbQueueConsumer> createEdqsEventsConsumer() { + return new InMemoryTbQueueConsumer<>(storage, edqsConfig.getEventsTopic()); } @Override - public TbQueueConsumer> createEdqsMsgConsumer(EdqsQueue queue, String group) { - return createEdqsMsgConsumer(queue); + public TbQueueConsumer> createEdqsEventsToBackupConsumer() { + throw new UnsupportedOperationException(); } @Override - public TbQueueProducer> createEdqsMsgProducer(EdqsQueue queue) { - if (queue == EdqsQueue.STATE) { - throw new UnsupportedOperationException(); - } - return new InMemoryTbQueueProducer<>(storage, queue.getTopic()); + public TbQueueConsumer> createEdqsStateConsumer() { + throw new UnsupportedOperationException(); + } + + @Override + public TbQueueProducer> createEdqsStateProducer() { + throw new UnsupportedOperationException(); } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java index e985696040..ab88943b10 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java @@ -19,11 +19,8 @@ import org.springframework.stereotype.Component; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsType; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; -import org.thingsboard.server.queue.TbQueueAdmin; -import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueResponseTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueResponseTemplate; @@ -71,54 +68,68 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory { } @Override - public TbQueueConsumer> createEdqsMsgConsumer(EdqsQueue queue) { - String consumerGroup = "edqs-" + queue.name().toLowerCase() + "-consumer-group-" + serviceInfoProvider.getServiceId(); - return createEdqsMsgConsumer(queue, consumerGroup); + public TbKafkaConsumerTemplate> createEdqsEventsConsumer() { + return createEdqsMsgConsumer(edqsConfig.getEventsTopic(), + "edqs-events-" + consumerCounter.getAndIncrement() + "-consumer-" + serviceInfoProvider.getServiceId(), + null, // not using consumer group management, offsets from the edqs-events-to-backup-consumer-group are used (see KafkaEdqsStateService) + false, edqsEventsAdmin); } @Override - public TbQueueConsumer> createEdqsMsgConsumer(EdqsQueue queue, String group) { + public TbKafkaConsumerTemplate> createEdqsEventsToBackupConsumer() { + return createEdqsMsgConsumer(edqsConfig.getEventsTopic(), + "edqs-events-to-backup-consumer-" + serviceInfoProvider.getServiceId(), + "edqs-events-to-backup-consumer-group", + false, edqsEventsAdmin); + } + + @Override + public TbKafkaConsumerTemplate> createEdqsStateConsumer() { + return createEdqsMsgConsumer(edqsConfig.getStateTopic(), + "edqs-state-" + consumerCounter.getAndIncrement() + "-consumer-" + serviceInfoProvider.getServiceId(), + null, // not using consumer group management + true, edqsStateAdmin); + } + + public TbKafkaConsumerTemplate> createEdqsMsgConsumer(String topic, String clientId, String group, boolean readFullAndStop, TbKafkaAdmin admin) { return TbKafkaConsumerTemplate.>builder() .settings(kafkaSettings) - .topic(topicService.buildTopicName(queue.getTopic())) - .readFromBeginning(queue.isReadFromBeginning()) - .stopWhenRead(queue.isStopWhenRead()) - .clientId("edqs-" + queue.name().toLowerCase() + "-" + consumerCounter.getAndIncrement() + "-consumer-" + serviceInfoProvider.getServiceId()) + .topic(topicService.buildTopicName(topic)) + .readFromBeginning(readFullAndStop) + .stopWhenRead(readFullAndStop) + .clientId(clientId) .groupId(topicService.buildTopicName(group)) .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdqsMsg.parseFrom(msg.getData()), msg.getHeaders())) - .admin(queue == EdqsQueue.STATE ? edqsStateAdmin : edqsEventsAdmin) + .admin(admin) .statsService(consumerStatsService) .build(); } @Override - public TbQueueProducer> createEdqsMsgProducer(EdqsQueue queue) { + public TbQueueProducer> createEdqsStateProducer() { return TbKafkaProducerTemplate.>builder() - .clientId("edqs-" + queue.name().toLowerCase() + "-producer-" + serviceInfoProvider.getServiceId()) + .clientId("edqs-state-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName(edqsConfig.getStateTopic())) .settings(kafkaSettings) - .admin(queue == EdqsQueue.STATE ? edqsStateAdmin : edqsEventsAdmin) + .admin(edqsStateAdmin) .build(); } @Override public TbQueueResponseTemplate, TbProtoQueueMsg> createEdqsResponseTemplate() { - String requestsConsumerGroup = "edqs-requests-consumer-group-" + edqsConfig.getLabel(); - var requestConsumer = TbKafkaConsumerTemplate.>builder() - .settings(kafkaSettings) - .topic(topicService.buildTopicName(edqsConfig.getRequestsTopic())) - .clientId("edqs-requests-consumer-" + serviceInfoProvider.getServiceId()) - .groupId(topicService.buildTopicName(requestsConsumerGroup)) - .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToEdqsMsg.parseFrom(msg.getData()), msg.getHeaders())) - .admin(edqsRequestsAdmin) - .statsService(consumerStatsService); + var requestConsumer = createEdqsMsgConsumer(edqsConfig.getRequestsTopic(), + "edqs-requests-consumer-" + serviceInfoProvider.getServiceId(), + "edqs-requests-consumer-group", + false, edqsRequestsAdmin); var responseProducer = TbKafkaProducerTemplate.>builder() .settings(kafkaSettings) .clientId("edqs-response-producer-" + serviceInfoProvider.getServiceId()) .defaultTopic(topicService.buildTopicName(edqsConfig.getResponsesTopic())) - .admin(edqsRequestsAdmin); + .admin(edqsRequestsAdmin) + .build(); return DefaultTbQueueResponseTemplate., TbProtoQueueMsg>builder() - .requestTemplate(requestConsumer.build()) - .responseTemplate(responseProducer.build()) + .requestTemplate(requestConsumer) + .responseTemplate(responseProducer) .maxPendingRequests(edqsConfig.getMaxPendingRequests()) .requestTimeout(edqsConfig.getMaxRequestTimeout()) .pollInterval(edqsConfig.getPollInterval()) @@ -128,7 +139,7 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory { } @Override - public TbQueueAdmin getEdqsQueueAdmin() { + public TbKafkaAdmin getEdqsQueueAdmin() { return edqsEventsAdmin; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java index 4ae744be67..37f881b49c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.queue.kafka; +import lombok.Getter; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.admin.CreateTopicsResult; import org.apache.kafka.clients.admin.ListOffsetsResult; @@ -45,6 +47,7 @@ public class TbKafkaAdmin implements TbQueueAdmin { private final TbKafkaSettings settings; private final Map topicConfigs; + @Getter private final int numPartitions; private volatile Set topics; @@ -157,8 +160,7 @@ public class TbKafkaAdmin implements TbQueueAdmin { if (partitionId == null) { return; } - Map oldOffsets = - settings.getAdminClient().listConsumerGroupOffsets(fatGroupId).partitionsToOffsetAndMetadata().get(10, TimeUnit.SECONDS); + Map oldOffsets = getConsumerGroupOffsets(fatGroupId); if (oldOffsets.isEmpty()) { return; } @@ -169,8 +171,7 @@ public class TbKafkaAdmin implements TbQueueAdmin { continue; } var om = consumerOffset.getValue(); - Map newOffsets = - settings.getAdminClient().listConsumerGroupOffsets(newGroupId).partitionsToOffsetAndMetadata().get(10, TimeUnit.SECONDS); + Map newOffsets = getConsumerGroupOffsets(newGroupId); var existingOffset = newOffsets.get(tp); if (existingOffset == null) { @@ -187,32 +188,52 @@ public class TbKafkaAdmin implements TbQueueAdmin { } } + @SneakyThrows + public Map getConsumerGroupOffsets(String groupId) { + return settings.getAdminClient().listConsumerGroupOffsets(groupId).partitionsToOffsetAndMetadata().get(10, TimeUnit.SECONDS); + } + public boolean isTopicEmpty(String topic) { + return areAllTopicsEmpty(Set.of(topic)); + } + + public boolean areAllTopicsEmpty(Set topics) { try { - if (!getTopics().contains(topic)) { + List existingTopics = getTopics().stream().filter(topics::contains).toList(); + if (existingTopics.isEmpty()) { return true; } - TopicDescription topicDescription = settings.getAdminClient().describeTopics(Collections.singletonList(topic)).topicNameValues().get(topic).get(); - List partitions = topicDescription.partitions().stream().map(partitionInfo -> new TopicPartition(topic, partitionInfo.partition())).toList(); - Map beginningOffsets = settings.getAdminClient().listOffsets(partitions.stream() + List allPartitions = settings.getAdminClient().describeTopics(existingTopics).topicNameValues().entrySet().stream() + .flatMap(entry -> { + String topic = entry.getKey(); + TopicDescription topicDescription; + try { + topicDescription = entry.getValue().get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + return topicDescription.partitions().stream().map(partitionInfo -> new TopicPartition(topic, partitionInfo.partition())); + }) + .toList(); + + Map beginningOffsets = settings.getAdminClient().listOffsets(allPartitions.stream() .collect(Collectors.toMap(partition -> partition, partition -> OffsetSpec.earliest()))).all().get(); - - Map endOffsets = settings.getAdminClient().listOffsets(partitions.stream() + Map endOffsets = settings.getAdminClient().listOffsets(allPartitions.stream() .collect(Collectors.toMap(partition -> partition, partition -> OffsetSpec.latest()))).all().get(); - for (TopicPartition partition : partitions) { + for (TopicPartition partition : allPartitions) { long beginningOffset = beginningOffsets.get(partition).offset(); long endOffset = endOffsets.get(partition).offset(); if (beginningOffset != endOffset) { - log.debug("Partition [{}] of topic [{}] is not empty. Returning false.", partition.partition(), topic); + log.debug("Partition [{}] of topic [{}] is not empty. Returning false.", partition.partition(), partition.topic()); return false; } } return true; } catch (InterruptedException | ExecutionException e) { - log.error("Failed to check if topic [{}] is empty.", topic, e); + log.error("Failed to check if topics [{}] empty.", topics, e); return false; } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java index 4bd3bf0fe6..3abed8475a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java @@ -16,6 +16,8 @@ package org.thingsboard.server.queue.kafka; import lombok.Builder; +import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRebalanceListener; @@ -39,7 +41,9 @@ import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.IntStream; /** * Created by ashvayka on 24.09.18. @@ -47,13 +51,16 @@ import java.util.stream.Collectors; @Slf4j public class TbKafkaConsumerTemplate extends AbstractTbQueueConsumerTemplate, T> { - private final TbQueueAdmin admin; + private final TbKafkaAdmin admin; private final KafkaConsumer consumer; private final TbKafkaDecoder decoder; private final TbKafkaConsumerStatsService statsService; + @Getter private final String groupId; + @Setter + private Function startOffsetProvider; private final boolean readFromBeginning; // reset offset to beginning private final boolean stopWhenRead; // stop consuming when reached end offset remembered on start private int readCount; @@ -78,7 +85,7 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue statsService.registerClientGroup(groupId); } - this.admin = admin; + this.admin = (TbKafkaAdmin) admin; this.consumer = new KafkaConsumer<>(props); this.decoder = decoder; this.readFromBeginning = readFromBeginning; @@ -105,14 +112,19 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue List toSubscribe = new ArrayList<>(); topics.forEach((topic, kafkaPartitions) -> { if (kafkaPartitions == null) { - toSubscribe.add(topic); - } else { - List topicPartitions = kafkaPartitions.stream() - .map(partition -> new TopicPartition(topic, partition)) - .toList(); - consumer.assign(topicPartitions); - onPartitionsAssigned(topicPartitions); + if (groupId != null) { + toSubscribe.add(topic); + return; + } else { // if no consumer group management - manually assigning all topic partitions + kafkaPartitions = IntStream.range(0, admin.getNumPartitions()).boxed().toList(); + } } + + List topicPartitions = kafkaPartitions.stream() + .map(partition -> new TopicPartition(topic, partition)) + .toList(); + consumer.assign(topicPartitions); + onPartitionsAssigned(topicPartitions); }); if (!toSubscribe.isEmpty()) { if (readFromBeginning || stopWhenRead) { @@ -179,9 +191,21 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue private void onPartitionsAssigned(Collection partitions) { if (readFromBeginning) { + log.debug("Seeking to beginning for {}", partitions); consumer.seekToBeginning(partitions); + } else if (startOffsetProvider != null) { + partitions.forEach(topicPartition -> { + Long offset = startOffsetProvider.apply(topicPartition.topic()); + if (offset != null) { + log.debug("Seeking to offset {} for {}", offset, topicPartition); + consumer.seek(topicPartition, offset); + } else { + log.info("No start offset provided for {}", topicPartition); + } + }); } if (stopWhenRead) { + log.debug("Getting end offsets for {}", partitions); endOffsets = consumer.endOffsets(partitions).entrySet().stream() .filter(entry -> entry.getValue() > 0) .collect(Collectors.toMap(entry -> entry.getKey().partition(), Map.Entry::getValue)); @@ -195,7 +219,9 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue @Override protected void doCommit() { - consumer.commitSync(); + if (groupId != null) { + consumer.commitSync(); + } } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/EdqsClientQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/EdqsClientQueueFactory.java index 95be49f82b..f2309f7224 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/EdqsClientQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/EdqsClientQueueFactory.java @@ -20,11 +20,10 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.edqs.EdqsQueue; public interface EdqsClientQueueFactory { - TbQueueProducer> createEdqsMsgProducer(EdqsQueue queue); + TbQueueProducer> createEdqsEventsProducer(); TbQueueRequestTemplate, TbProtoQueueMsg> createEdqsRequestTemplate(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java index fac2cf5d5e..89d83af826 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java @@ -37,7 +37,6 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.edqs.EdqsConfig; -import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.memory.InMemoryStorage; import org.thingsboard.server.queue.memory.InMemoryTbQueueConsumer; import org.thingsboard.server.queue.memory.InMemoryTbQueueProducer; @@ -149,7 +148,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE } @Override - public TbQueueConsumer> createToCalculatedFieldNotificationsMsgConsumer() { + public TbQueueConsumer> createToCalculatedFieldNotificationMsgConsumer() { return new InMemoryTbQueueConsumer<>(storage, topicService.getCalculatedFieldNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); } @@ -239,8 +238,8 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE } @Override - public TbQueueProducer> createEdqsMsgProducer(EdqsQueue queue) { - return new InMemoryTbQueueProducer<>(storage, queue.getTopic()); + public TbQueueProducer> createEdqsEventsProducer() { + return new InMemoryTbQueueProducer<>(storage, edqsConfig.getEventsTopic()); } @Override 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 f07bd9dcbb..dcebe085b3 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 @@ -54,7 +54,6 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.edqs.EdqsConfig; -import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; @@ -542,7 +541,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi } @Override - public TbQueueConsumer> createToCalculatedFieldNotificationsMsgConsumer() { + public TbQueueConsumer> createToCalculatedFieldNotificationMsgConsumer() { TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(topicService.getCalculatedFieldNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); @@ -590,9 +589,10 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi } @Override - public TbQueueProducer> createEdqsMsgProducer(EdqsQueue queue) { + public TbQueueProducer> createEdqsEventsProducer() { return TbKafkaProducerTemplate.>builder() - .clientId("edqs-producer-" + queue.name().toLowerCase() + "-" + serviceInfoProvider.getServiceId()) + .clientId("edqs-events-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName(edqsConfig.getEventsTopic())) .settings(kafkaSettings) .admin(edqsEventsAdmin) .build(); 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 3c6d144a0c..e9c42b0022 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 @@ -52,7 +52,6 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.edqs.EdqsConfig; -import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; @@ -480,9 +479,10 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createEdqsMsgProducer(EdqsQueue queue) { + public TbQueueProducer> createEdqsEventsProducer() { return TbKafkaProducerTemplate.>builder() - .clientId("edqs-producer-" + queue.name().toLowerCase() + "-" + serviceInfoProvider.getServiceId()) + .clientId("edqs-events-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName(edqsConfig.getEventsTopic())) .settings(kafkaSettings) .admin(edqsEventsAdmin) .build(); 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 d0e6c2f123..d43ef5c9ac 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 @@ -48,7 +48,7 @@ import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; -import org.thingsboard.server.queue.edqs.EdqsQueue; +import org.thingsboard.server.queue.edqs.EdqsConfig; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; @@ -79,6 +79,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueEdgeSettings edgeSettings; private final TbQueueCalculatedFieldSettings calculatedFieldSettings; + private final EdqsConfig edqsConfig; private final TbQueueAdmin coreAdmin; private final TbKafkaAdmin ruleEngineAdmin; @@ -101,7 +102,9 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbKafkaConsumerStatsService consumerStatsService, TbQueueTransportNotificationSettings transportNotificationSettings, - TbQueueEdgeSettings edgeSettings, TbQueueCalculatedFieldSettings calculatedFieldSettings, + TbQueueEdgeSettings edgeSettings, + TbQueueCalculatedFieldSettings calculatedFieldSettings, + EdqsConfig edqsConfig, TbKafkaTopicConfigs kafkaTopicConfigs) { this.topicService = topicService; this.kafkaSettings = kafkaSettings; @@ -113,6 +116,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { this.transportNotificationSettings = transportNotificationSettings; this.edgeSettings = edgeSettings; this.calculatedFieldSettings = calculatedFieldSettings; + this.edqsConfig = edqsConfig; this.coreAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getRuleEngineConfigs()); @@ -337,7 +341,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { } @Override - public TbQueueConsumer> createToCalculatedFieldNotificationsMsgConsumer() { + public TbQueueConsumer> createToCalculatedFieldNotificationMsgConsumer() { TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(topicService.getCalculatedFieldNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); @@ -349,6 +353,16 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { return consumerBuilder.build(); } + @Override + public TbQueueProducer> createToCalculatedFieldNotificationMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + requestBuilder.settings(kafkaSettings); + requestBuilder.clientId("tb-calculated-field-notifications-producer-" + serviceInfoProvider.getServiceId()); + requestBuilder.defaultTopic(topicService.getCalculatedFieldNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + requestBuilder.admin(notificationAdmin); + return requestBuilder.build(); + } + @Override public TbQueueConsumer> createCalculatedFieldStateConsumer() { return TbKafkaConsumerTemplate.>builder() @@ -375,9 +389,10 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { } @Override - public TbQueueProducer> createEdqsMsgProducer(EdqsQueue queue) { + public TbQueueProducer> createEdqsEventsProducer() { return TbKafkaProducerTemplate.>builder() - .clientId("edqs-producer-" + queue.name().toLowerCase() + "-" + serviceInfoProvider.getServiceId()) + .clientId("edqs-events-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName(edqsConfig.getEventsTopic())) .settings(kafkaSettings) .admin(edqsEventsAdmin) .build(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java index dcadf02d02..8e1952fc14 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java @@ -50,6 +50,7 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { private TbQueueProducer> toEdgeNotifications; private TbQueueProducer> toEdgeEvents; private TbQueueProducer> toCalculatedFields; + private TbQueueProducer> toCalculatedFieldNotifications; public TbRuleEngineProducerProvider(TbRuleEngineQueueFactory tbQueueProvider) { this.tbQueueProvider = tbQueueProvider; @@ -68,6 +69,7 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { this.toEdgeNotifications = tbQueueProvider.createEdgeNotificationsMsgProducer(); this.toEdgeEvents = tbQueueProvider.createEdgeEventMsgProducer(); this.toCalculatedFields = tbQueueProvider.createToCalculatedFieldMsgProducer(); + this.toCalculatedFieldNotifications = tbQueueProvider.createToCalculatedFieldNotificationMsgProducer(); } @Override @@ -132,7 +134,7 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { @Override public TbQueueProducer> getCalculatedFieldsNotificationsMsgProducer() { - throw new RuntimeException("Not Implemented! Should not be used by Rule Engine Service!"); + return toCalculatedFieldNotifications; } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java index 03f662d8bb..329e3e346a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java @@ -127,7 +127,9 @@ public interface TbRuleEngineQueueFactory extends TbUsageStatsClientQueueFactory TbQueueProducer> createToCalculatedFieldMsgProducer(); - TbQueueConsumer> createToCalculatedFieldNotificationsMsgConsumer(); + TbQueueConsumer> createToCalculatedFieldNotificationMsgConsumer(); + + TbQueueProducer> createToCalculatedFieldNotificationMsgProducer(); TbQueueConsumer> createCalculatedFieldStateConsumer(); diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 95fff48883..72792c1093 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -15,6 +15,7 @@ */ package org.thingsboard.script.api.tbel; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.primitives.Bytes; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; @@ -23,6 +24,10 @@ import org.mvel2.ParserConfiguration; import org.mvel2.execution.ExecutionArrayList; import org.mvel2.execution.ExecutionHashMap; import org.mvel2.util.MethodStub; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.geo.Coordinates; +import org.thingsboard.common.util.geo.GeoUtil; +import org.thingsboard.common.util.geo.RangeUnit; import org.thingsboard.server.common.data.StringUtils; import java.io.IOException; @@ -371,6 +376,10 @@ public class TbUtils { byte[].class, int.class))); parserConfig.addImport("parseBinaryArrayToInt", new MethodStub(TbUtils.class.getMethod("parseBinaryArrayToInt", byte[].class, int.class, int.class))); + parserConfig.addImport("isInsidePolygon", new MethodStub(TbUtils.class.getMethod("isInsidePolygon", + double.class, double.class, String.class))); + parserConfig.addImport("isInsideCircle", new MethodStub(TbUtils.class.getMethod("isInsideCircle", + double.class, double.class, String.class))); } public static String btoa(String input) { @@ -1437,6 +1446,22 @@ public class TbUtils { return result; } + public static boolean isInsidePolygon(double latitude, double longitude, String perimeter) { + return GeoUtil.contains(perimeter, new Coordinates(latitude, longitude)); + } + + public static boolean isInsideCircle(double latitude, double longitude, String perimeter) { + JsonNode perimeterJson = JacksonUtil.toJsonNode(perimeter); + double centerLatitude = Double.parseDouble(perimeterJson.get("latitude").asText()); + double centerLongitude = Double.parseDouble(perimeterJson.get("longitude").asText()); + double range = Double.parseDouble(perimeterJson.get("radius").asText()); + RangeUnit rangeUnit = perimeterJson.has("radiusUnit") ? RangeUnit.valueOf(perimeterJson.get("radiusUnit").asText()) : RangeUnit.METER; + + Coordinates entityCoordinates = new Coordinates(latitude, longitude); + Coordinates perimeterCoordinates = new Coordinates(centerLatitude, centerLongitude); + return range > GeoUtil.distance(entityCoordinates, perimeterCoordinates, rangeUnit); + } + private static byte isValidIntegerToByte(Integer val) { if (val > 255 || val < -128) { throw new NumberFormatException("The value '" + val + "' could not be correctly converted to a byte. " + diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index e3860ed89a..1063a8e8de 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -1150,6 +1150,26 @@ public class TbUtilsTest { Assertions.assertTrue(TbUtils.isNaN(Double.NaN)); } + @Test + public void isInsidePolygon() { + // outside the polygon + String perimeter = "[[[50.75581142688204,29.097910166341073],[50.16785158177623,29.35066098977171],[50.164329922384674,29.773743889862114],[50.16785158177623,30.801230932938843],[50.459245308833495,30.92760634465418],[50.486522489629564,30.68548421850448],[50.703612031034005,30.872660513473573]],[[50.606017492632766,29.36165015600782],[50.54317104075835,29.762754723626013],[50.41021974600505,29.455058069014804]]]"; + Assertions.assertFalse(TbUtils.isInsidePolygon(50.50869555168039, 30.80123093293884, perimeter)); + // inside the polygon + Assertions.assertTrue(TbUtils.isInsidePolygon(50.50520628167696, 30.339685951022016, perimeter)); + // inside the hole + Assertions.assertFalse(TbUtils.isInsidePolygon(50.52265651287081, 29.488025567723156, perimeter)); + } + + @Test + public void isInsideCircle() { + // outside the circle + String perimeter = "{\"latitude\":50.32254778825905,\"longitude\":28.207787701215757,\"radius\":47477.33130420423}"; + Assertions.assertFalse(TbUtils.isInsideCircle(50.81490715736681, 28.05943395702824, perimeter)); + // inside the circle + Assertions.assertTrue(TbUtils.isInsideCircle(50.599397971892444, 28.086906872618542, perimeter)); + } + private static List toList(byte[] data) { List result = new ArrayList<>(data.length); for (Byte b : data) { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index a7c4cb8c6c..5aea93528a 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -762,7 +762,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement ctx.writeAndFlush(createSubAckMessage(mqttMsg.variableHeader().messageId(), Collections.singletonList(MqttReasonCodes.SubAck.NOT_AUTHORIZED.byteValue() & 0xFF))); return; } - log.trace("[{}] Processing subscription [{}]!", sessionId, mqttMsg.variableHeader().messageId()); + //TODO consume the rate limit + log.trace("[{}][{}] Processing subscription [{}]!", deviceSessionCtx.getTenantId(), sessionId, mqttMsg.variableHeader().messageId()); List grantedQoSList = new ArrayList<>(); boolean activityReported = false; for (MqttTopicSubscription subscription : mqttMsg.payload().topicSubscriptions()) { @@ -772,7 +773,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement if (MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC.equals(topic)) { registerSubQoS(topic, grantedQoSList, reqQoS); } else { - log.debug("[{}] Failed to subscribe to [{}][{}]", sessionId, topic, reqQoS); + log.debug("[{}][{}] Failed to subscribe because this session is provision only [{}][{}]", deviceSessionCtx.getTenantId(), sessionId, topic, reqQoS); grantedQoSList.add(ReturnCodeResolver.getSubscriptionReturnCode(deviceSessionCtx.getMqttVersion(), MqttReasonCodes.SubAck.TOPIC_FILTER_INVALID)); } activityReported = true; @@ -847,13 +848,14 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement registerSubQoS(topic, grantedQoSList, reqQoS); break; default: - log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topic, reqQoS); + //TODO increment an error counter if any exists + log.warn("[{}][{}] Failed to subscribe because topic is not supported [{}][{}]", deviceSessionCtx.getTenantId(), sessionId, topic, reqQoS); grantedQoSList.add(ReturnCodeResolver.getSubscriptionReturnCode(deviceSessionCtx.getMqttVersion(), MqttReasonCodes.SubAck.TOPIC_FILTER_INVALID)); break; } } } catch (Exception e) { - log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topic, reqQoS, e); + log.warn("[{}][{}] Failed to subscribe to [{}][{}]", deviceSessionCtx.getTenantId(), sessionId, topic, reqQoS, e); grantedQoSList.add(ReturnCodeResolver.getSubscriptionReturnCode(deviceSessionCtx.getMqttVersion(), MqttReasonCodes.SubAck.IMPLEMENTATION_SPECIFIC_ERROR)); } } diff --git a/common/util/pom.xml b/common/util/pom.xml index 430a6e7df9..82768cdb68 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -103,6 +103,14 @@ com.fasterxml.jackson.datatype jackson-datatype-jdk8 + + org.locationtech.spatial4j + spatial4j + + + org.locationtech.jts + jts-core + diff --git a/common/util/src/main/java/org/thingsboard/common/util/DebugModeUtil.java b/common/util/src/main/java/org/thingsboard/common/util/DebugModeUtil.java index c4b062a0d8..1b035f61f1 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/DebugModeUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/DebugModeUtil.java @@ -22,7 +22,7 @@ import java.util.Set; public final class DebugModeUtil { - private static final int DEBUG_MODE_DEFAULT_DURATION_MINUTES = 15; + public static final int DEBUG_MODE_DEFAULT_DURATION_MINUTES = 15; private DebugModeUtil() { } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/Coordinates.java b/common/util/src/main/java/org/thingsboard/common/util/geo/Coordinates.java similarity index 94% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/Coordinates.java rename to common/util/src/main/java/org/thingsboard/common/util/geo/Coordinates.java index c3b91e39bb..dc1998bff1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/Coordinates.java +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/Coordinates.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.geo; +package org.thingsboard.common.util.geo; import lombok.Data; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java b/common/util/src/main/java/org/thingsboard/common/util/geo/GeoUtil.java similarity index 99% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java rename to common/util/src/main/java/org/thingsboard/common/util/geo/GeoUtil.java index f92a345362..11a6799348 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/GeoUtil.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.geo; +package org.thingsboard.common.util.geo; import com.google.gson.JsonArray; import com.google.gson.JsonElement; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/Perimeter.java b/common/util/src/main/java/org/thingsboard/common/util/geo/Perimeter.java similarity index 95% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/Perimeter.java rename to common/util/src/main/java/org/thingsboard/common/util/geo/Perimeter.java index 3474683c6c..238fa19af5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/Perimeter.java +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/Perimeter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.geo; +package org.thingsboard.common.util.geo; import lombok.Data; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/PerimeterType.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterType.java similarity index 94% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/PerimeterType.java rename to common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterType.java index 2df6cf796c..c3b334abd7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/PerimeterType.java +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterType.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.geo; +package org.thingsboard.common.util.geo; public enum PerimeterType { CIRCLE, POLYGON diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/RangeUnit.java b/common/util/src/main/java/org/thingsboard/common/util/geo/RangeUnit.java similarity index 95% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/RangeUnit.java rename to common/util/src/main/java/org/thingsboard/common/util/geo/RangeUnit.java index 090d251b33..56883ca43f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/RangeUnit.java +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/RangeUnit.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.geo; +package org.thingsboard.common.util.geo; public enum RangeUnit { METER(1000.0), KILOMETER(1.0), FOOT(3280.84), MILE(0.62137), NAUTICAL_MILE(0.539957); diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java index 187de20667..c9c7af1a89 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java @@ -39,6 +39,7 @@ public class CalculatedFieldDataValidator extends DataValidator protected void validateCreate(TenantId tenantId, CalculatedField calculatedField) { validateNumberOfCFsPerEntity(tenantId, calculatedField.getEntityId()); validateNumberOfArgumentsPerCF(tenantId, calculatedField); + validateArgumentNames(calculatedField); } @Override @@ -48,6 +49,7 @@ public class CalculatedFieldDataValidator extends DataValidator throw new DataValidationException("Can't update non existing calculated field!"); } validateNumberOfArgumentsPerCF(tenantId, calculatedField); + validateArgumentNames(calculatedField); return old; } @@ -71,4 +73,10 @@ public class CalculatedFieldDataValidator extends DataValidator } } + private void validateArgumentNames(CalculatedField calculatedField) { + if (calculatedField.getConfiguration().getArguments().containsKey("ctx")) { + throw new DataValidationException("Argument name 'ctx' is reserved and cannot be used."); + } + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java index 4f7239cfb3..1c5d554350 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java @@ -119,7 +119,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq @Override public ListenableFuture findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { var aggParams = query.getAggParameters(); - if (Aggregation.NONE.equals(aggParams.getAggregation())) { + if (Aggregation.NONE.equals(aggParams.getAggregation()) || aggParams.getInterval() < 1) { return Futures.immediateFuture(findAllAsyncWithLimit(entityId, query)); } else { List>> futures = new ArrayList<>(); @@ -144,7 +144,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq } } - private ReadTsKvQueryResult findAllAsyncWithLimit(EntityId entityId, ReadTsKvQuery query) { + ReadTsKvQueryResult findAllAsyncWithLimit(EntityId entityId, ReadTsKvQuery query) { Integer keyId = keyDictionaryDao.getOrSaveKeyId(query.getKey()); List tsKvEntities = tsKvRepository.findAllWithLimit( entityId.getId(), diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java index 04a57c3fea..e165d6cfe6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java @@ -44,6 +44,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +import static org.thingsboard.common.util.DebugModeUtil.DEBUG_MODE_DEFAULT_DURATION_MINUTES; import static org.thingsboard.server.dao.service.Validator.validateId; @Service("TenantProfileDaoService") @@ -160,7 +161,9 @@ public class TenantProfileServiceImpl extends AbstractCachedEntityService maxTsIntervals || intervalCounts < 0) { throw new IncorrectParameterException("Incorrect TsKvQuery. Number of intervals is to high - " + intervalCounts + ". " + diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java index a070de4d82..6a38159804 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java @@ -45,6 +45,7 @@ import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.objects.TelemetryEntityView; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.AbstractServiceTest; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -757,6 +758,21 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { assertThat(fullList).containsOnlyOnceElementsOf(timeseries); } + @Test + public void testFindAllByQueriesWithAggregationAndZeroInterval() throws Exception { + testFindAllByQueriesWithAggregationAndInvalidInterval(0); + } + + @Test + public void testFindAllByQueriesWithAggregationAndNegativeInterval() throws Exception { + testFindAllByQueriesWithAggregationAndInvalidInterval(-1); + } + + private void testFindAllByQueriesWithAggregationAndInvalidInterval(long interval) { + BaseReadTsKvQuery query = new BaseReadTsKvQuery(STRING_KEY, TS, TS, interval, 1000, Aggregation.SUM, "DESC"); + Assert.assertThrows(IncorrectParameterException.class, () -> findAndVerifyQueryId(deviceId, query)); + } + private TsKvEntry save(DeviceId deviceId, long ts, long value) throws Exception { TsKvEntry entry = new BasicTsKvEntry(ts, new LongDataEntry(LONG_KEY, value)); tsService.save(tenantId, deviceId, entry).get(MAX_TIMEOUT, TimeUnit.SECONDS); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDaoTest.java index f9ea7d2a41..eb34e6dc4c 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDaoTest.java @@ -51,6 +51,7 @@ public class AbstractChunkedAggregationTimeseriesDaoTest { Optional optionalListenableFuture = Optional.of(mock(TsKvEntry.class)); willReturn(Futures.immediateFuture(optionalListenableFuture)).given(tsDao).findAndAggregateAsync(any(), anyString(), anyLong(), anyLong(), anyLong(), any()); willReturn(Futures.immediateFuture(mock(ReadTsKvQueryResult.class))).given(tsDao).getReadTsKvQueryResultFuture(any(), any()); + willReturn(mock(ReadTsKvQueryResult.class)).given(tsDao).findAllAsyncWithLimit(any(), any()); } @Test @@ -146,6 +147,24 @@ public class AbstractChunkedAggregationTimeseriesDaoTest { } } + @Test + public void givenZeroInterval_whenAggregateCount_thenFindAllWithoutAggregation() { + givenInterval_whenAggregateCount_thenFindAllWithoutAggregation(0); + } + + @Test + public void givenNegativeInterval_whenAggregateCount_thenFindAllWithoutAggregation() { + givenInterval_whenAggregateCount_thenFindAllWithoutAggregation(-1); + } + + public void givenInterval_whenAggregateCount_thenFindAllWithoutAggregation(int interval) { + ReadTsKvQuery query = new BaseReadTsKvQuery(TEMP, 1, 3000, interval, LIMIT, COUNT, DESC); + willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query); + tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query); + verify(tsDao, times(1)).findAllAsyncWithLimit(any(), any()); + verify(tsDao, times(0)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any()); + } + long getTsForReadTsKvQuery(long startTs, long endTs) { return startTs + (endTs - startTs) / 2L; } diff --git a/docker/docker-compose.edqs.volumes.yml b/docker/docker-compose.edqs.volumes.yml index 89b4b3a59c..3a45542b99 100644 --- a/docker/docker-compose.edqs.volumes.yml +++ b/docker/docker-compose.edqs.volumes.yml @@ -17,10 +17,10 @@ version: '3.0' services: - tb-edqs-1: + tb-edqs1: volumes: - tb-edqs-log-volume:/var/log/tb-edqs - tb-edqs-2: + tb-edqs2: volumes: - tb-edqs-log-volume:/var/log/tb-edqs diff --git a/docker/docker-compose.edqs.yml b/docker/docker-compose.edqs.yml index 6dd9606ee6..21bace143a 100644 --- a/docker/docker-compose.edqs.yml +++ b/docker/docker-compose.edqs.yml @@ -29,9 +29,12 @@ services: tb-rule-engine2: env_file: - tb-rule-engine-edqs.env - tb-edqs-1: + tb-edqs1: restart: always image: "${DOCKER_REPO}/${EDQS_DOCKER_NAME}:${TB_VERSION}" + environment: + TB_SERVICE_ID: tb-edqs1 + JAVA_OPTS: "${JAVA_OPTS}" env_file: - tb-edqs.env volumes: @@ -42,9 +45,12 @@ services: depends_on: - zookeeper - kafka - tb-edqs-2: + tb-edqs2: restart: always image: "${DOCKER_REPO}/${EDQS_DOCKER_NAME}:${TB_VERSION}" + environment: + TB_SERVICE_ID: tb-edqs2 + JAVA_OPTS: "${JAVA_OPTS}" env_file: - tb-edqs.env volumes: diff --git a/docker/tb-edqs/conf/logback.xml b/docker/tb-edqs/conf/logback.xml index 14b5b0f04b..c8e97e48e4 100644 --- a/docker/tb-edqs/conf/logback.xml +++ b/docker/tb-edqs/conf/logback.xml @@ -24,7 +24,7 @@ /var/log/tb-edqs/${TB_SERVICE_ID}/tb-edqs.log - /var/log/tb-edqs/tb-edqs.%d{yyyy-MM-dd}.%i.log + /var/log/tb-edqs/${TB_SERVICE_ID}/tb-edqs.%d{yyyy-MM-dd}.%i.log 100MB 30 3GB @@ -41,7 +41,6 @@ - diff --git a/edqs/src/main/resources/edqs.yml b/edqs/src/main/resources/edqs.yml index f7d0eda841..353d76391b 100644 --- a/edqs/src/main/resources/edqs.yml +++ b/edqs/src/main/resources/edqs.yml @@ -57,6 +57,10 @@ queue: partitions: "${TB_EDQS_PARTITIONS:12}" # EDQS partitioning strategy: tenant (partitions are resolved and distributed by tenant id) or none (partitions are resolved by message key; each instance has all the partitions) partitioning_strategy: "${TB_EDQS_PARTITIONING_STRATEGY:tenant}" + # EDQS events topic + events_topic: "${TB_EDQS_EVENTS_TOPIC:edqs.events}" + # EDQS state topic + state_topic: "${TB_EDQS_STATE_TOPIC:edqs.state}" # EDQS requests topic requests_topic: "${TB_EDQS_REQUESTS_TOPIC:edqs.requests}" # EDQS responses topic @@ -148,12 +152,12 @@ queue: # - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms # value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) topic-properties: - # Kafka properties for EDQS events topics. Partitions number must be the same as queue.edqs.partitions - edqs-events: "${TB_QUEUE_KAFKA_EDQS_EVENTS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:-1;partitions:12;min.insync.replicas:1}" - # Kafka properties for EDQS requests topic (default: 3 minutes retention). Partitions number must be the same as queue.edqs.partitions - edqs-requests: "${TB_QUEUE_KAFKA_EDQS_REQUESTS_TOPIC_PROPERTIES:retention.ms:180000;segment.bytes:52428800;retention.bytes:1048576000;partitions:12;min.insync.replicas:1}" - # Kafka properties for EDQS state topic (infinite retention, compaction). Partitions number must be the same as queue.edqs.partitions - edqs-state: "${TB_QUEUE_KAFKA_EDQS_STATE_TOPIC_PROPERTIES:retention.ms:-1;segment.bytes:52428800;retention.bytes:-1;partitions:12;min.insync.replicas:1;cleanup.policy:compact}" + # Kafka properties for EDQS events topics + edqs-events: "${TB_QUEUE_KAFKA_EDQS_EVENTS_TOPIC_PROPERTIES:retention.ms:86400000;segment.bytes:52428800;retention.bytes:-1;partitions:1;min.insync.replicas:1}" + # Kafka properties for EDQS requests topic (default: 3 minutes retention) + edqs-requests: "${TB_QUEUE_KAFKA_EDQS_REQUESTS_TOPIC_PROPERTIES:retention.ms:180000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + # Kafka properties for EDQS state topic (infinite retention, compaction) + edqs-state: "${TB_QUEUE_KAFKA_EDQS_STATE_TOPIC_PROPERTIES:retention.ms:-1;segment.bytes:52428800;retention.bytes:-1;partitions:1;min.insync.replicas:1;cleanup.policy:compact}" consumer-stats: # Prints lag between consumer group offset and last messages offset in Kafka topics enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java index a190b7b791..b42e9138e4 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java @@ -20,6 +20,7 @@ public class Latencies { public static final String WS_CONNECT = "wsConnect"; public static final String WS_SUBSCRIBE = "wsSubscribe"; public static final String LOG_IN = "logIn"; + public static final String EDQS_QUERY = "edqsQuery"; public static String request(String key) { return String.format("%sRequest", key); diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java index 342ee121ef..9c3ee5b786 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java @@ -18,5 +18,6 @@ package org.thingsboard.monitoring.data; public class MonitoredServiceKey { public static final String GENERAL = "Monitoring"; + public static final String EDQS = "*EDQS*"; } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java index 9157e9f31c..7219e4f9aa 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java @@ -15,10 +15,13 @@ */ package org.thingsboard.monitoring.service; +import com.google.common.collect.Sets; import jakarta.annotation.PostConstruct; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.thingsboard.monitoring.client.TbClient; import org.thingsboard.monitoring.client.WsClient; @@ -27,13 +30,26 @@ import org.thingsboard.monitoring.config.MonitoringConfig; import org.thingsboard.monitoring.config.MonitoringTarget; import org.thingsboard.monitoring.data.Latencies; import org.thingsboard.monitoring.data.MonitoredServiceKey; +import org.thingsboard.monitoring.data.ServiceFailureException; import org.thingsboard.monitoring.service.transport.TransportHealthChecker; import org.thingsboard.monitoring.util.TbStopWatch; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityTypeFilter; +import org.thingsboard.server.common.data.query.TsValue; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -41,6 +57,7 @@ import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import java.util.stream.Stream; @Slf4j public abstract class BaseMonitoringService, T extends MonitoringTarget> { @@ -61,6 +78,9 @@ public abstract class BaseMonitoringService, T ext @Autowired protected ApplicationContext applicationContext; + @Value("${monitoring.edqs.enabled:false}") + private boolean edqsMonitoringEnabled; + @PostConstruct private void init() { if (configs == null || configs.isEmpty()) { @@ -108,6 +128,21 @@ public abstract class BaseMonitoringService, T ext check(healthChecker, wsClient); } } + + if (edqsMonitoringEnabled) { + try { + stopWatch.start(); + checkEdqs(); + reporter.reportLatency(Latencies.EDQS_QUERY, stopWatch.getTime()); + + reporter.serviceIsOk(MonitoredServiceKey.EDQS); + } catch (ServiceFailureException e) { + reporter.serviceFailure(MonitoredServiceKey.EDQS, e); + } catch (Exception e) { + reporter.serviceFailure(MonitoredServiceKey.GENERAL, e); + } + } + reporter.reportLatencies(tbClient); log.debug("Finished {}", getName()); } catch (Throwable error) { @@ -149,6 +184,39 @@ public abstract class BaseMonitoringService, T ext } } + private void checkEdqs() { + EntityTypeFilter entityTypeFilter = new EntityTypeFilter(); + entityTypeFilter.setEntityType(EntityType.DEVICE); + EntityDataPageLink pageLink = new EntityDataPageLink(100, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"))); + EntityDataQuery entityDataQuery = new EntityDataQuery(entityTypeFilter, pageLink, + List.of(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "type")), + List.of(new EntityKey(EntityKeyType.TIME_SERIES, "testData")), + Collections.emptyList()); + + PageData result = tbClient.findEntityDataByQuery(entityDataQuery); + Set devices = result.getData().stream() + .map(entityData -> entityData.getEntityId().getId()) + .collect(Collectors.toSet()); + Set missing = Sets.difference(new HashSet<>(this.devices), devices); + if (!missing.isEmpty()) { + throw new ServiceFailureException("Missing devices in the response: " + missing); + } + + result.getData().stream() + .filter(entityData -> this.devices.contains(entityData.getEntityId().getId())) + .forEach(entityData -> { + Map values = new HashMap<>(entityData.getLatest().get(EntityKeyType.ENTITY_FIELD)); + values.putAll(entityData.getLatest().get(EntityKeyType.TIME_SERIES)); + + Stream.of("name", "type", "testData").forEach(key -> { + TsValue value = values.get(key); + if (value == null || StringUtils.isBlank(value.getValue())) { + throw new ServiceFailureException("Missing " + key + " for device " + entityData.getEntityId()); + } + }); + }); + } + @SneakyThrows private Set getAssociatedUrls(String baseUrl) { URI url = new URI(baseUrl); diff --git a/monitoring/src/main/resources/tb-monitoring.yml b/monitoring/src/main/resources/tb-monitoring.yml index ae0d265ed6..6cc2e79cb4 100644 --- a/monitoring/src/main/resources/tb-monitoring.yml +++ b/monitoring/src/main/resources/tb-monitoring.yml @@ -105,6 +105,9 @@ monitoring: # To add more targets, use following environment variables: # monitoring.transports.lwm2m.targets[1].base_url, monitoring.transports.lwm2m.targets[2].base_url, etc. + edqs: + enabled: "${EDQS_MONITORING_ENABLED:false}" + notifications: message_prefix: '${NOTIFICATION_MESSAGE_PREFIX:}' slack: diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java index 65def6a964..f2e721bdcb 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java @@ -188,8 +188,8 @@ public class ContainerTestSuite { .waitingFor("tb-vc-executor1", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) .waitingFor("tb-vc-executor2", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) .waitingFor("tb-js-executor", Wait.forLogMessage(TB_JS_EXECUTOR_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) - .waitingFor("tb-edqs-1", Wait.forLogMessage(TB_EDQS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) - .waitingFor("tb-edqs-2", Wait.forLogMessage(TB_EDQS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)); + .waitingFor("tb-edqs1", Wait.forLogMessage(TB_EDQS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) + .waitingFor("tb-edqs2", Wait.forLogMessage(TB_EDQS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)); testContainer.start(); setActive(true); } catch (Exception e) { diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java index 63faed41ab..2593e18a55 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; @@ -73,7 +74,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { " var airDensity = pressure / (287.05 * temperatureK);\n" + "\n" + " return {\n" + - " \"airDensity\": airDensity\n" + + " \"airDensity\": toFixed(airDensity, 2)\n" + " };"; private TenantId tenantId; @@ -280,6 +281,34 @@ public class CalculatedFieldTest extends AbstractContainerTest { testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); } + @Test + public void testEntityIdIsProfileAndRefEntityIsCommon() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + CalculatedField savedCalculatedField = createScriptCalculatedField(deviceProfileId); + + await().alias("create CF -> perform initial calculation for device by profile").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode airDensity = testRestClient.getLatestTelemetry(device.getId()); + assertThat(airDensity).isNotNull(); + assertThat(airDensity.get("airDensity").get(0).get("value").asText()).isEqualTo("1.05"); + }); + + testRestClient.postTelemetryAttribute(asset.getId(), DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"altitude\":1531}")); + + await().alias("create CF -> update telemetry for common entity").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode airDensity = testRestClient.getLatestTelemetry(device.getId()); + assertThat(airDensity).isNotNull(); + assertThat(airDensity.get("airDensity").get(0).get("value").asText()).isEqualTo("0.99"); + }); + + testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); + } + private CalculatedField createSimpleCalculatedField() { return createSimpleCalculatedField(device.getId()); } @@ -323,19 +352,21 @@ public class CalculatedFieldTest extends AbstractContainerTest { calculatedField.setName("Air density" + RandomStringUtils.randomAlphabetic(5)); calculatedField.setDebugSettings(DebugSettings.all()); - SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + ScriptCalculatedFieldConfiguration config = new ScriptCalculatedFieldConfiguration(); Argument argument1 = new Argument(); argument1.setRefEntityId(asset.getId()); ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("altitude", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE); argument1.setRefEntityKey(refEntityKey1); - config.setArguments(Map.of("altitude", argument1)); Argument argument2 = new Argument(); ReferencedEntityKey refEntityKey2 = new ReferencedEntityKey("temperatureInF", ArgumentType.TS_ROLLING, null); + argument2.setTimeWindow(30000L); + argument2.setLimit(5); argument2.setRefEntityKey(refEntityKey2); - config.setArguments(Map.of("temperature", argument2)); - config.setExpression("return {\"airDensity\": 5};"); + config.setArguments(Map.of("altitude", argument1, "temperature", argument2)); + + config.setExpression(exampleScript); Output output = new Output(); output.setType(OutputType.TIME_SERIES); diff --git a/pom.xml b/pom.xml index 72195b1e4f..b25c355934 100755 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,7 @@ 4.0.2 2.4.0-b180830.0359 4.0.5 + 10.1.39 3.2.12 3.2.12 3.2.12 @@ -88,7 +89,7 @@ 1.18.32 1.2.5 1.2.5 - 4.1.115.Final + 4.1.119.Final 2.0.65.Final 1.1.18 1.7.1 @@ -1147,6 +1148,21 @@ jaxb-runtime ${jaxb-runtime.version} + + org.apache.tomcat.embed + tomcat-embed-core + ${tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-el + ${tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-websocket + ${tomcat.version} + org.springframework.boot spring-boot-starter diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index eb568f9cc7..7ee5ab1496 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -120,14 +120,6 @@ org.bouncycastle bcpkix-jdk18on - - org.locationtech.spatial4j - spatial4j - - - org.locationtech.jts - jts-core - com.sun.mail jakarta.mail diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java index 8025baeec7..ff6b3acb88 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java @@ -21,6 +21,11 @@ import com.google.gson.JsonParser; import org.locationtech.spatial4j.context.jts.JtsSpatialContext; import org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.geo.Coordinates; +import org.thingsboard.common.util.geo.GeoUtil; +import org.thingsboard.common.util.geo.Perimeter; +import org.thingsboard.common.util.geo.PerimeterType; +import org.thingsboard.common.util.geo.RangeUnit; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -74,7 +79,7 @@ public abstract class AbstractGeofencingNode void; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 1cf44966fc..c5b256ea89 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -37,6 +37,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { catchError, filter, switchMap, tap } from 'rxjs/operators'; import { + ArgumentEntityType, ArgumentType, CalculatedField, CalculatedFieldEventArguments, @@ -60,7 +61,7 @@ import { DatePipe } from '@angular/common'; export class CalculatedFieldsTableConfig extends EntityTableConfig { readonly calculatedFieldsDebugPerTenantLimitsConfiguration = - getCurrentAuthState(this.store)['calculatedFieldsDebugPerTenantLimitsConfiguration'] || '1:1'; + getCurrentAuthState(this.store)['calculatedFieldsDebugPerTenantLimitsConfiguration']; readonly maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE; readonly tenantId = getCurrentAuthUser(this.store).tenantId; additionalDebugActionConfig = { @@ -159,7 +160,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.getCalculatedFieldDialog(calculatedField, 'action.add')), + switchMap(calculatedField => this.getCalculatedFieldDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)), filter(Boolean), switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)), filter(Boolean), @@ -261,6 +262,18 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.updateData()); } + private updateImportedCalculatedField(calculatedField: CalculatedField): CalculatedField { + calculatedField.configuration.arguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { + const arg = calculatedField.configuration.arguments[key]; + acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant + ? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } } + : arg; + return acc; + }, {}); + + return calculatedField; + } + private onDebugConfigChanged(id: string, debugSettings: EntityDebugSettings): void { this.calculatedFieldsService.getCalculatedFieldById(id).pipe( switchMap(field => this.calculatedFieldsService.saveCalculatedField({ ...field, debugSettings })), diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html index abb34cb502..53d8a9d7b6 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html @@ -58,7 +58,7 @@
- @if (argument.refEntityId?.id) { + @if (argument.refEntityId?.id && argument.refEntityId?.entityType !== ArgumentEntityType.Tenant) { {{ entityNameMap.get(argument.refEntityId.id) ?? '' }} @@ -87,17 +87,17 @@ - +
} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts index 8aa61eb1a4..2a64c90f20 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts @@ -36,12 +36,13 @@ import { EntityId } from '@shared/models/id/entity-id'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { EntityFilter } from '@shared/models/query/query.models'; import { AliasFilterType } from '@shared/models/alias.models'; -import { merge } from 'rxjs'; +import { BehaviorSubject, merge } from 'rxjs'; import { MINUTE } from '@shared/models/time/time.models'; import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { AppState } from '@core/core.state'; import { Store } from '@ngrx/store'; import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; @Component({ selector: 'tb-calculated-field-argument-panel', @@ -51,18 +52,16 @@ import { EntityAutocompleteComponent } from '@shared/components/entity/entity-au export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewInit { @Input() buttonTitle: string; - @Input() index: number; @Input() argument: CalculatedFieldArgumentValue; @Input() entityId: EntityId; @Input() tenantId: string; @Input() entityName: string; - @Input() entityHasError: boolean; @Input() calculatedFieldType: CalculatedFieldType; @Input() usedArgumentNames: string[]; @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent; - argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>(); + argumentsDataApplied = output(); readonly maxDataPointsPerRollingArg = getCurrentAuthState(this.store).maxDataPointsPerRollingArg; readonly defaultLimit = Math.floor(this.maxDataPointsPerRollingArg / 10); @@ -85,6 +84,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI argumentTypes: ArgumentType[]; entityFilter: EntityFilter; + entityNameSubject = new BehaviorSubject(null); readonly argumentEntityTypes = Object.values(ArgumentEntityType) as ArgumentEntityType[]; readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations; @@ -106,7 +106,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI private store: Store ) { this.observeEntityFilterChanges(); - this.observeEntityTypeChanges() + this.observeEntityTypeChanges(); this.observeEntityKeyChanges(); this.observeUpdatePosition(); } @@ -141,7 +141,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } ngAfterViewInit(): void { - if (this.entityHasError) { + if (this.argument.refEntityId?.id === NULL_UUID) { this.entityAutocomplete.selectEntityFormGroup.get('entity').markAsTouched(); } } @@ -152,11 +152,14 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI if (refEntityId.entityType === ArgumentEntityType.Tenant) { refEntityId.id = this.tenantId; } + if (refEntityId.entityType !== ArgumentEntityType.Current && refEntityId.entityType !== ArgumentEntityType.Tenant) { + value.entityName = this.entityNameSubject.value; + } if (value.defaultValue) { value.defaultValue = value.defaultValue.trim(); } value.refEntityKey.key = value.refEntityKey.key.trim(); - this.argumentsDataApplied.emit({ value, index: this.index }); + this.argumentsDataApplied.emit(value); } cancel(): void { @@ -212,12 +215,16 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } private observeEntityTypeChanges(): void { - this.argumentFormGroup.get('refEntityId').get('entityType').valueChanges + this.refEntityIdFormGroup.get('entityType').valueChanges .pipe(distinctUntilChanged(), takeUntilDestroyed()) .subscribe(type => { this.argumentFormGroup.get('refEntityId').get('id').setValue(''); + const isEntityWithId = type !== ArgumentEntityType.Tenant && type !== ArgumentEntityType.Current; this.argumentFormGroup.get('refEntityId') - .get('id')[type === ArgumentEntityType.Tenant || type === ArgumentEntityType.Current ? 'disable' : 'enable'](); + .get('id')[isEntityWithId ? 'enable' : 'disable'](); + if (!isEntityWithId) { + this.entityNameSubject.next(null); + } if (!this.enableAttributeScopeSelection) { this.refEntityKeyFormGroup.get('scope').setValue(AttributeScope.SERVER_SCOPE); } diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts index 85a90aef63..90beed2f7e 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts @@ -153,5 +153,6 @@ export class EntityDebugSettingsButtonComponent implements ControlValueAccessor } else { this.debugSettingsFormGroup.enable({emitEvent: false}); } + this.cd.markForCheck(); } } diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html index 4a9bc57d8d..5863a295d6 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html @@ -53,7 +53,7 @@ color="primary" type="button" (click)="onCancel(); additionalActionConfig.action()"> - {{ additionalActionConfig.title | translate }} + {{ additionalActionConfig.title }} }
diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts index 873d8f0f3f..cb982b9650 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts @@ -37,14 +37,18 @@ export class EntityDebugSettingsService { if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const debugStrategyPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, EntityDebugSettingsPanelComponent, 'bottom', true, null, - { + const debugStrategyPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: EntityDebugSettingsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: 'bottom', + context: { ...panelConfig.debugSettings, ...panelConfig.debugConfig, }, - {}, - {}, {}, true); + isModal: true, + }); debugStrategyPopover.tbComponentRef.instance.onSettingsApplied.subscribe(settings => { panelConfig.onSettingsAppliedFn(settings); debugStrategyPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html index 706076e3ff..d05097bd5b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html @@ -48,7 +48,7 @@ - {{ 'device-profile.lwm2m.no-objects-matching' | translate:{object: truncate.transform(searchText, true, 6, '...')} }} + {{ 'device-profile.lwm2m.no-objects-matching' | translate:{object: (searchText | truncate: true: 6: '...')} }} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts index 7ea253cd9d..01e79fcadf 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts @@ -33,8 +33,8 @@ import { DeviceProfileService } from '@core/http/device-profile.service'; import { Direction } from '@shared/models/page/sort-order'; import { isDefined, isDefinedAndNotNull, isObject, isString } from '@core/utils'; import { PageLink } from '@shared/models/page/page-link'; -import { TruncatePipe } from '@shared/pipe/truncate.pipe'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; @Component({ selector: 'tb-profile-lwm2m-object-list', @@ -79,13 +79,12 @@ export class Lwm2mObjectListComponent implements ControlValueAccessor, OnInit, V @Output() removeList = new EventEmitter(); - @ViewChild('objectInput') objectInput: ElementRef; + @ViewChild('objectInput', {static: true}) objectInput: ElementRef; + @ViewChild('objectInput', {static: true, read: MatAutocompleteTrigger}) matAutocompleteTrigger: MatAutocompleteTrigger; - private propagateChange = (v: any) => { - } + private propagateChange: (value: any) => void = () => {}; - constructor(public truncate: TruncatePipe, - private deviceProfileService: DeviceProfileService, + constructor(private deviceProfileService: DeviceProfileService, private fb: UntypedFormBuilder) { this.lwm2mListFormGroup = this.fb.group({ objectsList: [this.objectsList], @@ -113,7 +112,7 @@ export class Lwm2mObjectListComponent implements ControlValueAccessor, OnInit, V this.propagateChange = fn; } - registerOnTouched(fn: any): void { + registerOnTouched(_fn: any): void { } ngOnInit() { @@ -140,6 +139,9 @@ export class Lwm2mObjectListComponent implements ControlValueAccessor, OnInit, V if (isDefined(this.objectInput)) { this.clear('', false); } + if (this.matAutocompleteTrigger.panelOpen) { + this.matAutocompleteTrigger.closePanel(); + } } else { this.lwm2mListFormGroup.enable({emitEvent: false}); } diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts index f06abaf3a8..903712aac8 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts @@ -232,16 +232,23 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const removeOtherEntitiesConfirmPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, RemoveOtherEntitiesConfirmComponent, 'bottom', true, null, - { + const removeOtherEntitiesConfirmPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: RemoveOtherEntitiesConfirmComponent, + hostView: this.viewContainerRef, + preferredPlacement: 'bottom', + context: { onClose: (result: boolean | null) => { removeOtherEntitiesConfirmPopover.hide(); if (result) { entityTypeControl.get('config').get('removeOtherEntities').patchValue(true, {emitEvent: true}); } } - }, {}, {}, {}, false); + }, + showCloseButton: false, + isModal: true + }); } } } diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.ts index 88d4079916..19dc945d65 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.ts @@ -304,9 +304,13 @@ export class EntityVersionDiffComponent extends PageComponent implements OnInit, if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const restoreVersionPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, EntityVersionRestoreComponent, 'leftTop', true, null, - { + const restoreVersionPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: EntityVersionRestoreComponent, + hostView: this.viewContainerRef, + preferredPlacement: 'leftTop', + context: { versionName: this.versionName, versionId: this.versionId, externalEntityId: this.externalEntityId, @@ -317,7 +321,10 @@ export class EntityVersionDiffComponent extends PageComponent implements OnInit, this.versionRestored.emit(); } } - }, {}, {}, {}, false); + }, + showCloseButton: false, + isModal: true + }); restoreVersionPopover.tbComponentRef.instance.popoverComponent = restoreVersionPopover; } } diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.ts index 5ccadcd4cb..fe9af56e4b 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.ts @@ -211,9 +211,13 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const createVersionPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, EntityVersionCreateComponent, 'leftTop', true, null, - { + const createVersionPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: EntityVersionCreateComponent, + hostView: this.viewContainerRef, + preferredPlacement: 'leftTop', + context: { branch: this.branch, entityId: this.entityId, entityName: this.entityName, @@ -229,8 +233,11 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni } } }, - {maxHeight: '100vh', height: '100%', padding: '10px'}, - {width: '400px', minWidth: '100%', maxWidth: '100%'}, {}, false); + showCloseButton: false, + overlayStyle: {maxHeight: '100vh', height: '100%', padding: '10px'}, + popoverStyle: {width: '400px', minWidth: '100%', maxWidth: '100%'}, + isModal: true + }); createVersionPopover.tbComponentRef.instance.popoverComponent = createVersionPopover; } } @@ -243,9 +250,13 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const complexCreateVersionPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, ComplexVersionCreateComponent, 'leftTop', true, null, - { + const complexCreateVersionPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: ComplexVersionCreateComponent, + hostView: this.viewContainerRef, + preferredPlacement: 'leftTop', + context: { branch: this.branch, onClose: (result: VersionCreationResult | null, branch: string | null) => { complexCreateVersionPopover.hide(); @@ -258,8 +269,10 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni } } }, - {maxHeight: '90vh', height: '100%', padding: '10px'}, - {}, {}, false); + showCloseButton: false, + overlayStyle: {maxHeight: '90vh', height: '100%', padding: '10px'}, + isModal: true + }); complexCreateVersionPopover.tbComponentRef.instance.popoverComponent = complexCreateVersionPopover; } } @@ -272,14 +285,21 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const diffVersionPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, EntityVersionDiffComponent, 'leftTop', true, null, - { + const diffVersionPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: EntityVersionDiffComponent, + hostView: this.viewContainerRef, + preferredPlacement: 'leftTop', + context: { versionName: entityVersion.name, versionId: entityVersion.id, entityId: this.entityId, externalEntityId: this.externalEntityIdValue - }, {}, {}, {}, false); + }, + showCloseButton: false, + isModal: true + }); diffVersionPopover.tbComponentRef.instance.popoverComponent = diffVersionPopover; diffVersionPopover.tbComponentRef.instance.versionRestored.subscribe(() => { this.versionRestored.emit(); @@ -295,9 +315,13 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const restoreVersionPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, EntityVersionRestoreComponent, 'leftTop', true, null, - { + const restoreVersionPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: EntityVersionRestoreComponent, + hostView: this.viewContainerRef, + preferredPlacement: 'leftTop', + context: { versionName: entityVersion.name, versionId: entityVersion.id, externalEntityId: this.externalEntityIdValue, @@ -308,8 +332,11 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni } } }, - {maxHeight: '100vh', height: '100%', padding: '10px'}, - {width: '400px', minWidth: '100%', maxWidth: '100%'}, {}, false); + showCloseButton: false, + overlayStyle: {maxHeight: '100vh', height: '100%', padding: '10px'}, + popoverStyle: {width: '400px', minWidth: '100%', maxWidth: '100%'}, + isModal: true + }); restoreVersionPopover.tbComponentRef.instance.popoverComponent = restoreVersionPopover; } } @@ -322,17 +349,23 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const restoreEntitiesVersionPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, ComplexVersionLoadComponent, 'leftTop', true, null, - { + const restoreEntitiesVersionPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: ComplexVersionLoadComponent, + hostView: this.viewContainerRef, + preferredPlacement: 'leftTop', + context: { versionName: entityVersion.name, versionId: entityVersion.id, onClose: (result: VersionLoadResult | null) => { restoreEntitiesVersionPopover.hide(); } }, - {maxHeight: '80vh', height: '100%', padding: '10px'}, - {}, {}, false); + showCloseButton: false, + overlayStyle: {maxHeight: '80vh', height: '100%', padding: '10px'}, + isModal: true + }); restoreEntitiesVersionPopover.tbComponentRef.instance.popoverComponent = restoreEntitiesVersionPopover; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html index 3d88f08c8d..2376e52a64 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html @@ -33,6 +33,7 @@ [callbacks]="data.callbacks" [widgetType] = "data.widgetType" [actionSources]="actionSources" + [defaultIconColor]="data.defaultIconColor" [additionalWidgetActionTypes]="data.additionalWidgetActionTypes" formControlName="actions"> diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts index bc3de16c69..067eb74166 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts @@ -32,6 +32,7 @@ export interface ManageWidgetActionsDialogData { actionsData: WidgetActionsData; callbacks: WidgetActionCallbacks; widgetType: widgetType; + defaultIconColor?: string; additionalWidgetActionTypes?: WidgetActionType[]; } diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts index 621bfd523f..c108d673d3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts @@ -21,11 +21,11 @@ import { widgetActionTypeTranslationMap } from '@app/shared/models/widget.models'; import { CollectionViewer, DataSource } from '@angular/cdk/collections'; -import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs'; +import { BehaviorSubject, Observable, of, ReplaySubject, shareReplay } from 'rxjs'; import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { TranslateService } from '@ngx-translate/core'; import { PageLink } from '@shared/models/page/page-link'; -import { catchError, map, publishReplay, refCount } from 'rxjs/operators'; +import { catchError, map } from 'rxjs/operators'; import { UtilsService } from '@core/services/utils.service'; import { deepClone } from '@core/utils'; @@ -68,11 +68,11 @@ export class WidgetActionsDatasource implements DataSource> { + connect(_collectionViewer: CollectionViewer): Observable> { return this.actionsSubject.asObservable(); } - disconnect(collectionViewer: CollectionViewer): void { + disconnect(_collectionViewer: CollectionViewer): void { this.actionsSubject.complete(); this.pageDataSubject.complete(); } @@ -115,8 +115,7 @@ export class WidgetActionsDatasource implements DataSource}; + private viewsInited = false; + private dirtyValue = false; private widgetResize$: ResizeObserver; private destroyed = false; @@ -101,15 +100,14 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni private propagateChange = (_: any) => {}; - constructor(protected store: Store, - private translate: TranslateService, + constructor(private translate: TranslateService, private utils: UtilsService, private dialog: MatDialog, private dialogs: DialogService, private cd: ChangeDetectorRef, private elementRef: ElementRef, private zone: NgZone) { - super(store); + super(); const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC }; this.pageLink = new PageLink(10, 0, null, sortOrder); this.dataSource = new WidgetActionsDatasource(this.translate, this.utils); @@ -137,7 +135,6 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni } ngAfterViewInit() { - fromEvent(this.searchInputField.nativeElement, 'keyup') .pipe( debounceTime(150), @@ -162,10 +159,9 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni this.dirtyValue = false; this.updateData(true); } - } - updateData(reload: boolean = false) { + private updateData(reload: boolean = false) { this.pageLink.page = this.paginator.pageIndex; this.pageLink.pageSize = this.paginator.pageSize; this.pageLink.sortOrder.property = this.sort.active; @@ -198,7 +194,7 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni this.openWidgetActionDialog($event, action); } - openWidgetActionDialog($event: Event, action: WidgetActionDescriptorInfo = null) { + private openWidgetActionDialog($event: Event, action: WidgetActionDescriptorInfo = null) { if ($event) { $event.stopPropagation(); } @@ -208,15 +204,15 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni prevActionSourceId = action.actionSourceId; } const availableActionSources: {[actionSourceId: string]: WidgetActionSource} = {}; - for (const id of Object.keys(this.innerValue.actionSources)) { - const actionSource = this.innerValue.actionSources[id]; + for (const id of Object.keys(this.actionSources)) { + const actionSource = this.actionSources[id]; if (actionSource.multiple) { availableActionSources[id] = actionSource; } else { if (!isAdd && action.actionSourceId === id) { availableActionSources[id] = actionSource; } else { - const existing = this.innerValue.actionsMap[id]; + const existing = this.actionsMap[id]; if (!existing || !existing.length) { availableActionSources[id] = actionSource; } @@ -225,7 +221,7 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni } const actionsData: WidgetActionsData = { - actionsMap: this.innerValue.actionsMap, + actionsMap: this.actionsMap, actionSources: availableActionSources }; @@ -239,6 +235,7 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni actionsData, action: deepClone(action), widgetType: this.widgetType, + defaultIconColor: this.defaultIconColor, additionalWidgetActionTypes: this.additionalWidgetActionTypes } }).afterClosed().subscribe( @@ -277,7 +274,7 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni } private getOrCreateTargetActions(actionSourceId: string): Array { - const actionsMap = this.innerValue.actionsMap; + const actionsMap = this.actionsMap; let targetActions = actionsMap[actionSourceId]; if (!targetActions) { targetActions = []; @@ -323,7 +320,7 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni this.updateData(); } - resetSortAndFilter(update: boolean = true) { + private resetSortAndFilter(update: boolean = true) { this.pageLink.textSearch = null; this.paginator.pageIndex = 0; const sortable = this.sort.sortables.get('actionSourceName'); @@ -338,7 +335,7 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni this.propagateChange = fn; } - registerOnTouched(fn: any): void { + registerOnTouched(_fn: any): void { } setDisabledState(isDisabled: boolean): void { @@ -346,13 +343,14 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni } writeValue(actions?: {[actionSourceId: string]: Array}): void { - this.innerValue = { - actionsMap: actions || {}, - actionSources: this.actionSources || {} - }; + this.actionsMap = actions ?? {}; setTimeout(() => { if (!this.destroyed) { - this.dataSource.setActions(this.innerValue); + const actionData: WidgetActionsData = { + actionsMap: this.actionsMap, + actionSources: this.actionSources + }; + this.dataSource.setActions(actionData); if (this.viewsInited) { this.resetSortAndFilter(true); } else { @@ -364,6 +362,6 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni private onActionsUpdated() { this.updateData(true); - this.propagateChange(this.innerValue.actionsMap); + this.propagateChange(this.actionsMap); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts index b77b5c860f..ea49a8b442 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts @@ -61,6 +61,7 @@ export interface WidgetActionDialogData { actionsData: WidgetActionsData; action?: WidgetActionDescriptorInfo; widgetType: widgetType; + defaultIconColor?: string; additionalWidgetActionTypes?: WidgetActionType[]; } @@ -78,6 +79,8 @@ export class WidgetActionDialogComponent extends DialogComponent -
widget-config.decimals-suffix
+
widget-config.digits-suffix
{ timewindowStylePanelPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts index 93b2a6b6e5..1cbc41c368 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts @@ -17,7 +17,7 @@ import * as CanvasGauges from 'canvas-gauges'; import { FontSettings, getFontFamily } from '@home/components/widget/lib/settings.models'; import { WidgetContext } from '@home/models/widget-component.models'; -import { isDefined } from '@core/utils'; +import { isDefined, isDefinedAndNotNull } from '@core/utils'; import tinycolor from 'tinycolor2'; import Highlight = CanvasGauges.Highlight; import BaseGauge = CanvasGauges.BaseGauge; @@ -107,7 +107,9 @@ export abstract class TbBaseGauge { } resize() { - this.gauge.update({width: this.ctx.width, height: this.ctx.height} as GenericOptions); + if (this.ctx.width > 0 && this.ctx.height > 0) { + this.gauge.update({width: this.ctx.width, height: this.ctx.height} as GenericOptions); + } } destroy() { @@ -262,8 +264,7 @@ function getValueDec(ctx: WidgetContext, settings: AnalogueGaugeSettings): numbe if (dataKey && isDefined(dataKey.decimals)) { return dataKey.decimals; } else { - return (isDefined(settings.valueDec) && settings.valueDec !== null) - ? settings.valueDec : ctx.decimals; + return isDefinedAndNotNull(ctx.decimals) ? ctx.decimals : 0; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html index 8833b60cfe..0e0170c603 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html @@ -17,7 +17,14 @@ -->
- + @if (widgetComponent.dashboardWidget.showWidgetTitlePanel) { +
+ + +
+ } @else { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.scss index 3c584b7fb8..76cae3cf24 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.scss @@ -104,4 +104,11 @@ } } } + + .tb-widget-title-row .tb-widget-actions { + margin: 0; + } + .tb-widget-actions.tb-widget-actions-absolute { + z-index: 2; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts index 51be4a70bf..746daf43b3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts @@ -40,6 +40,7 @@ import { } from '@home/components/widget/lib/chart/bar-chart-with-labels-widget.models'; import { TbTimeSeriesChart } from '@home/components/widget/lib/chart/time-series-chart'; import { DataKey } from '@shared/models/widget.models'; +import { WidgetComponent } from '@home/components/widget/widget.component'; @Component({ selector: 'tb-bar-chart-with-labels-widget', @@ -57,9 +58,6 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft @Input() ctx: WidgetContext; - @Input() - widgetTitlePanel: TemplateRef; - showLegend: boolean; legendClass: string; @@ -73,7 +71,8 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft private timeSeriesChart: TbTimeSeriesChart; - constructor(private imagePipe: ImagePipe, + constructor(public widgetComponent: WidgetComponent, + private imagePipe: ImagePipe, private sanitizer: DomSanitizer, private renderer: Renderer2, private cd: ChangeDetectorRef) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart-widget.component.html index 7f7fb47389..8e90ab8fb6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart-widget.component.html @@ -19,6 +19,5 @@ #latestChart [ctx]="ctx" [settings]="settings" - [widgetTitlePanel]="widgetTitlePanel" [callbacks]="callbacks" > diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html index 578f7fdf4b..20cfdf4623 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html @@ -17,7 +17,14 @@ -->
- + @if (widgetComponent.dashboardWidget.showWidgetTitlePanel) { +
+ + +
+ } @else { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss index 3566820c8d..2eb0ceb940 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss @@ -106,4 +106,11 @@ } } } + + .tb-widget-title-row .tb-widget-actions { + margin: 0; + } + .tb-widget-actions.tb-widget-actions-absolute { + z-index: 2; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts index 651240342c..85905dfd3e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts @@ -68,9 +68,6 @@ export class LatestChartComponent implements OnInit, OnDestroy, AfterViewInit { @Input() ctx: WidgetContext; - @Input() - widgetTitlePanel: TemplateRef; - @Input() callbacks: LatestChartComponentCallbacks; @@ -98,9 +95,9 @@ export class LatestChartComponent implements OnInit, OnDestroy, AfterViewInit { private latestChart: TbLatestChart; - constructor(private imagePipe: ImagePipe, + constructor(public widgetComponent: WidgetComponent, + private imagePipe: ImagePipe, private sanitizer: DomSanitizer, - private widgetComponent: WidgetComponent, private renderer: Renderer2, private translate: TranslateService, private cd: ChangeDetectorRef) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts index 13e9dd7d5e..fad0d84632 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts @@ -223,40 +223,42 @@ export abstract class TbLatestChart { } protected updateSeriesData(initial = false) { - this.total = 0; - this.totalText = 'N/A'; - let hasValue = false; - for (const dataItem of this.dataItems) { - if (dataItem.enabled && dataItem.hasValue) { - hasValue = true; - this.total += dataItem.value; - } - if (this.settings.showLegend) { - const legendItem = this.legendItems.find(item => item.dataKey === dataItem.dataKey); - if (dataItem.hasValue) { - legendItem.hasValue = true; - legendItem.value = formatValue(dataItem.value, this.decimals, this.units, false); - } else { - legendItem.hasValue = false; - legendItem.value = '--'; + if (!this.latestChart.isDisposed()) { + this.total = 0; + this.totalText = 'N/A'; + let hasValue = false; + for (const dataItem of this.dataItems) { + if (dataItem.enabled && dataItem.hasValue) { + hasValue = true; + this.total += dataItem.value; + } + if (this.settings.showLegend) { + const legendItem = this.legendItems.find(item => item.dataKey === dataItem.dataKey); + if (dataItem.hasValue) { + legendItem.hasValue = true; + legendItem.value = formatValue(dataItem.value, this.decimals, this.units, false); + } else { + legendItem.hasValue = false; + legendItem.value = '--'; + } } } - } - if (this.settings.showTotal || this.settings.showLegend) { - if (hasValue) { - this.totalText = formatValue(this.total, this.decimals, this.units, false); - if (this.settings.showLegend && !this.settings.showTotal) { - this.legendItems[this.legendItems.length - 1].hasValue = true; - this.legendItems[this.legendItems.length - 1].value = this.totalText; + if (this.settings.showTotal || this.settings.showLegend) { + if (hasValue) { + this.totalText = formatValue(this.total, this.decimals, this.units, false); + if (this.settings.showLegend && !this.settings.showTotal) { + this.legendItems[this.legendItems.length - 1].hasValue = true; + this.legendItems[this.legendItems.length - 1].value = this.totalText; + } + } else if (this.settings.showLegend && !this.settings.showTotal) { + this.legendItems[this.legendItems.length - 1].hasValue = false; + this.legendItems[this.legendItems.length - 1].value = '--'; } - } else if (this.settings.showLegend && !this.settings.showTotal) { - this.legendItems[this.legendItems.length - 1].hasValue = false; - this.legendItems[this.legendItems.length - 1].value = '--'; } + this.doUpdateSeriesData(); + this.latestChart.setOption(this.latestChartOption); + this.afterUpdateSeriesData(initial); } - this.doUpdateSeriesData(); - this.latestChart.setOption(this.latestChartOption); - this.afterUpdateSeriesData(initial); } private drawChart() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html index 8880503eb5..39112c484b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html @@ -17,7 +17,14 @@ -->
- + @if (widgetComponent.dashboardWidget.showWidgetTitlePanel) { +
+ + +
+ } @else { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.scss index 6f4b21a78d..ff339362a1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.scss @@ -104,4 +104,10 @@ } } } + .tb-widget-title-row .tb-widget-actions { + margin: 0; + } + .tb-widget-actions.tb-widget-actions-absolute { + z-index: 2; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts index eacbb883a4..0201ec7d5f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts @@ -48,6 +48,7 @@ import { Observable } from 'rxjs'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { DomSanitizer } from '@angular/platform-browser'; import { TbTimeSeriesChart } from '@home/components/widget/lib/chart/time-series-chart'; +import { WidgetComponent } from '@home/components/widget/widget.component'; @Component({ selector: 'tb-range-chart-widget', @@ -65,9 +66,6 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn @Input() ctx: WidgetContext; - @Input() - widgetTitlePanel: TemplateRef; - showLegend: boolean; legendClass: string; @@ -86,7 +84,8 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn private timeSeriesChart: TbTimeSeriesChart; - constructor(private imagePipe: ImagePipe, + constructor(public widgetComponent: WidgetComponent, + private imagePipe: ImagePipe, private sanitizer: DomSanitizer, private renderer: Renderer2, private cd: ChangeDetectorRef) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html index 1ba501674e..1efcf30781 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html @@ -17,9 +17,14 @@ -->
- - - + @if (widgetComponent.dashboardWidget.showWidgetTitlePanel) { +
+ + +
+ } @else { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.scss index f500d3009b..37067ef935 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.scss @@ -156,4 +156,10 @@ $maxLegendHeight: 35%; } } } + .tb-widget-title-row .tb-widget-actions { + margin: 0; + } + .tb-widget-actions.tb-widget-actions-absolute { + z-index: 2; + } } 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 2e9f297b4a..2fd12b1b98 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 @@ -61,9 +61,6 @@ export class TimeSeriesChartWidgetComponent implements OnInit, OnDestroy, AfterV @Input() ctx: WidgetContext; - @Input() - widgetTitlePanel: TemplateRef; - horizontalLegendPosition = false; showLegend: boolean; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index ad121a1eb0..7bea76d325 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -690,12 +690,14 @@ export class TbTimeSeriesChart { } private updateSeriesData(updateScale = false): void { - this.updateSeries(); - if (updateScale && this.updateYAxisScale(this.yAxisList)) { - this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); + if (!this.timeSeriesChart.isDisposed()) { + this.updateSeries(); + if (updateScale && this.updateYAxisScale(this.yAxisList)) { + this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); + } + this.timeSeriesChart.setOption(this.timeSeriesChartOptions); + this.updateAxes(); } - this.timeSeriesChart.setOption(this.timeSeriesChartOptions); - this.updateAxes(); } private updateSeries(): void { @@ -791,7 +793,7 @@ export class TbTimeSeriesChart { } } else { if (!axis.option.name) { - axis.option.name = axis.settings.label; + axis.option.name = this.ctx.utilsService.customTranslation(axis.settings.label, axis.settings.label); result.changed = true; } const nameGap = size; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss index 43ff26793a..a1959c74e8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss @@ -38,9 +38,6 @@ .tb-get-started-container { flex: 1; overflow: auto; - mat-stepper { - transform: scale(1); //fixed blur content - } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts index 9e25ddb486..2d3fc6b2fe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts @@ -22,7 +22,7 @@ import { TbMapDatasource } from '@shared/models/widget/maps/map.models'; import L from 'leaflet'; -import { FormattedData } from '@shared/models/widget.models'; +import { DataKey, FormattedData } from '@shared/models/widget.models'; import { TbShapesDataLayer } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer'; import { TbMap } from '@home/components/widget/lib/maps/map'; import { Observable } from 'rxjs'; @@ -213,9 +213,8 @@ export class TbCirclesDataLayer extends TbShapesDataLayer): Partial { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/latest-map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/latest-map-data-layer.ts index 3a537ba8e7..fb79611d71 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/latest-map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/latest-map-data-layer.ts @@ -117,6 +117,7 @@ export abstract class TbLatestDataLayerItem { - this.datasource = mapDataSourceSettingsToDatasource(this.settings); - this.datasource.dataKeys = this.settings.additionalDataKeys ? [...this.settings.additionalDataKeys] : []; - const colorRangeKeys = this.allColorSettings().filter(settings => settings.type === DataLayerColorType.range && settings.rangeKey) - .map(settings => settings.rangeKey); - this.datasource.dataKeys.push(...colorRangeKeys); - this.mapDataId = this.datasource.mapDataIds[0]; - this.datasource = this.setupDatasource(this.datasource); + this.dataSources = []; + const datasource = mapDataSourceSettingsToDatasource(this.settings); + this.mapDataId = datasource.mapDataIds[0]; + this.dataSources.push(datasource); + if (this.settings.additionalDataSources?.length && datasource.type !== DatasourceType.function) { + this.dataSources.push(...this.settings.additionalDataSources.map(ds => mapDataSourceSettingsToDatasource(ds, this.mapDataId))); + } + const dataKeys = this.calculateDataKeys(); + const latestDataKeys = this.calculateLatestDataKeys(); + this.dataSources.forEach(ds => ds.dataKeys.push(...dataKeys)); + if (latestDataKeys?.length) { + this.dataSources.forEach(ds => ds.latestDataKeys.push(...latestDataKeys)); + } return forkJoin( [ this.dataLayerLabelProcessor ? this.dataLayerLabelProcessor.setup() : of(null), @@ -241,8 +247,8 @@ export abstract class TbMapDataLayer settings.type === DataLayerColorType.range && settings.rangeKey) + .map(settings => settings.rangeKey); + dataKeys.push(...colorRangeKeys); + dataKeys.push(...this.getDataKeys()); + return dataKeys; + } + + protected calculateLatestDataKeys(): DataKey[] { + return []; + } + + protected getDataKeys(): DataKey[] { + return []; } protected allColorSettings(): DataLayerColorSettings[] { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts index 99cb5a9b7d..093cc265bb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts @@ -37,7 +37,7 @@ import { TbMapDatasource } from '@shared/models/widget/maps/map.models'; import L, { FeatureGroup } from 'leaflet'; -import { FormattedData } from '@shared/models/widget.models'; +import { DataKey, FormattedData } from '@shared/models/widget.models'; import { forkJoin, Observable, of } from 'rxjs'; import { CompiledTbFunction } from '@shared/models/js-function.models'; import { @@ -480,10 +480,14 @@ class TbMarkerDataLayerItem extends TbLatestDataLayerItem { + (this.marker.dragging as any)._draggable = { _moved: true, off: (_args: any) => { return { disable: () => {}} } }; + (this.marker.dragging as any)._enabled = true; this.moving = true; }); this.marker.on('pm:dragend', () => { this.saveMarkerLocation(); + delete (this.marker.dragging as any)._draggable; + delete (this.marker.dragging as any)._enabled; this.moving = false; }); } @@ -624,9 +628,8 @@ export class TbMarkersDataLayer extends TbLatestMapDataLayer): Partial { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts index e598685786..ae2f62853e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts @@ -351,10 +351,10 @@ class TbTripDataItem extends TbDataLayerItem 1) { + result[timeStamp[timeStamp.length - 1]].rotationAngle = result[timeStamp[timeStamp.length - 2]].rotationAngle; + } + } else if (this.settings.rotateMarker && timeStamp.length > 1) { + const startPoint = this.dataLayer.dataProcessor.extractLocation(result[timeStamp[timeStamp.length - 2]], dsData); + const endPoint = this.dataLayer.dataProcessor.extractLocation(result[timeStamp[timeStamp.length - 1]], dsData); + result[timeStamp[timeStamp.length - 1]].rotationAngle += findRotationAngle(startPoint, endPoint); } return result; } @@ -528,22 +535,33 @@ export class TbTripsDataLayer extends TbMapDataLayer settings.type === DataLayerColorType.range && settings.rangeKey) - .map(settings => settings.rangeKey); + .map(settings => settings.rangeKey); if (this.settings.additionalDataKeys?.length) { additionalKeys.push(...this.settings.additionalDataKeys); } if (additionalKeys.length) { const tsKeys = additionalKeys.filter(key => key.type === DataKeyType.timeseries); + dataKeys.push(...tsKeys); + } + return dataKeys; + } + + protected calculateLatestDataKeys(): DataKey[] { + const additionalKeys = this.allColorSettings().filter(settings => settings.type === DataLayerColorType.range && settings.rangeKey) + .map(settings => settings.rangeKey); + if (this.settings.additionalDataKeys?.length) { + additionalKeys.push(...this.settings.additionalDataKeys); + } + if (additionalKeys.length) { const latestKeys = additionalKeys.filter(key => key.type !== DataKeyType.timeseries); - datasource.dataKeys.push(...tsKeys); if (latestKeys.length) { - datasource.latestDataKeys = latestKeys; + return latestKeys; } } - return datasource; + return []; } protected allColorSettings(): DataLayerColorSettings[] { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html index b45869addf..e64ee71170 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html @@ -17,9 +17,14 @@ -->
- - - + @if (widgetComponent.dashboardWidget.showWidgetTitlePanel) { +
+ + +
+ } @else { + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.scss index 88157426f3..eab92a664f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.scss @@ -34,4 +34,10 @@ div.tb-widget-title { padding: 0; } + .tb-widget-title-row .tb-widget-actions { + margin: 0; + } + .tb-widget-actions.tb-widget-actions-absolute { + z-index: 2; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts index 013a6c88a9..4ef3c3cd45 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts @@ -21,7 +21,6 @@ import { Input, OnDestroy, OnInit, - TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core'; @@ -55,9 +54,6 @@ export class MapWidgetComponent implements OnInit, OnDestroy { @Input() ctx: WidgetContext; - @Input() - widgetTitlePanel: TemplateRef; - backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; padding: string; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts index 6672941045..f1bb414878 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts @@ -133,6 +133,7 @@ export abstract class TbMap { private currentEditButton: L.TB.ToolbarButton; private dragMode = true; + private createMapItemActionId: string; private get isPlacingItem(): boolean { return !!this.currentEditButton; @@ -176,6 +177,7 @@ export abstract class TbMap { private setupControls(): Observable { if (this.settings.scales?.length) { L.control.scale({ + position: 'bottomright', metric: this.settings.scales.includes(MapScale.metric), imperial: this.settings.scales.includes(MapScale.imperial) }).addTo(this.map); @@ -306,7 +308,8 @@ export abstract class TbMap { () => { let datasources: TbMapDatasource[]; for (const layerType of mapDataLayerTypes) { - const typeDatasources = this.latestDataLayers.filter(dl => dl.dataLayerType() === layerType).map(dl => dl.getDatasource()); + const typeDatasources = this.latestDataLayers.filter(dl => dl.dataLayerType() === layerType) + .map(dl => dl.getDataSources()).flat(); if (!datasources) { datasources = typeDatasources; } else { @@ -347,7 +350,7 @@ export abstract class TbMap { ); } if (this.tripDataLayers.length) { - const tripDatasources = this.tripDataLayers.map(dl => dl.getDatasource()); + const tripDatasources = this.tripDataLayers.map(dl => dl.getDataSources()).flat(); const tripDataLayersSubscriptionOptions: WidgetSubscriptionOptions = { datasources: tripDatasources, hasDataPageLink: true, @@ -685,9 +688,14 @@ export abstract class TbMap { } private createItem(actionData: PlaceMapItemActionData, prepareDrawMode: () => void) { - if (this.isPlacingItem) { + const actionId = 'id' in actionData.action ? actionData.action.id : 'map-button'; + if (this.createMapItemActionId === actionId) { + this.finishCreatedItem(); return; } + if (isDefined(this.createMapItemActionId)) { + this.finishCreatedItem(); + } this.updatePlaceItemState(actionData.additionalParams?.button, true); this.map.once('pm:create', (e) => { @@ -700,7 +708,7 @@ export abstract class TbMap { // @ts-ignore e.layer._pmTempLayer = true; e.layer.remove(); - this.finishAdd(); + this.finishCreatedItem(); }); prepareDrawMode(); @@ -713,10 +721,12 @@ export abstract class TbMap { iconClass: 'tb-close', title: this.ctx.translate.instant('action.cancel'), showText: true, - click: this.finishAdd + click: this.finishCreatedItem } ], false); + this.createMapItemActionId = actionId; + const convertLayerToCoordinates = (type: MapItemType, layer: L.Layer): {x: number; y: number} | TbPolygonRawCoordinates | TbCircleData => { switch (type) { case MapItemType.marker: @@ -748,6 +758,11 @@ export abstract class TbMap { } } + private finishCreatedItem = () => { + delete this.createMapItemActionId; + this.finishAdd(); + } + private finishAdd = () => { if (this.currentPopover) { this.currentPopover.hide(); @@ -912,7 +927,7 @@ export abstract class TbMap { private calculateCurrentTime(minTime: number, maxTime: number): number { if (minTime !== this.minTime || maxTime !== this.maxTime) { - if (this.minTime >= this.currentTime || isUndefined(this.currentTime)) { + if (this.minTime >= this.currentTime || isUndefined(this.currentTime) || this.currentTime === Infinity) { return this.minTime; } else if (this.maxTime <= this.currentTime) { return this.maxTime; @@ -937,7 +952,13 @@ export abstract class TbMap { bounds = new L.LatLngBounds(null, null); dataLayersBounds.forEach(b => bounds.extend(b)); const mapBounds = this.map.getBounds(); - if (bounds.isValid() && (!this.bounds || !this.bounds.isValid() || (!this.bounds.equals(bounds) || force) && this.settings.fitMapBounds && !mapBounds.contains(bounds))) { + if (bounds.isValid() && + ( + (!this.bounds || !this.bounds.isValid() || (!this.bounds.equals(bounds) || force) && this.settings.fitMapBounds) + && !mapBounds.contains(bounds) + ) + ) + { this.bounds = bounds; if (!this.ignoreUpdateBounds && !this.isPlacingItem) { this.fitBounds(bounds); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts index 7044ffa5fe..f9d0fa6d07 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts @@ -202,7 +202,10 @@ export class MapTimelinePanelComponent implements OnInit, OnDestroy { public moveNext() { if (this.index < this.maxTimeIndex) { if (this.settings.snapToRealLocation) { - const anchorIndex = this.findIndex(this.currentTime, this.anchors) + 1; + let anchorIndex = this.findIndex(this.currentTime, this.anchors) + 1; + if (anchorIndex >= this.anchors.length) { + anchorIndex = this.anchors.length - 1; + } this.index = Math.floor((this.anchors[anchorIndex] - this.minValue) / this.settings.timeStep); } else { this.index++; @@ -214,7 +217,10 @@ export class MapTimelinePanelComponent implements OnInit, OnDestroy { public movePrev() { if (this.index > this.minTimeIndex) { if (this.settings.snapToRealLocation) { - const anchorIndex = this.findIndex(this.currentTime, this.anchors) - 1; + let anchorIndex = this.findIndex(this.currentTime, this.anchors) - 1; + if (anchorIndex < 0) { + anchorIndex = 0; + } this.index = Math.floor((this.anchors[anchorIndex] - this.minValue) / this.settings.timeStep); } else { this.index--; 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 61f5073953..10d5f9b1a0 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 @@ -149,7 +149,7 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { if (this.markdownText !== markdownText) { this.markdownText = this.utils.customTranslation(markdownText, markdownText); } - this.cd.markForCheck(); + this.cd.detectChanges(); } markdownClick($event: MouseEvent) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html index ba613ddbfe..af2c92b733 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html @@ -95,7 +95,7 @@ {{ 'widgets.table.display-pagination' | translate }}
-
{{ 'widgets.table.page-step-settings' | translate }}
+
{{ 'widgets.table.page-step-settings' | translate }}
widgets.table.page-step-increment
@@ -113,7 +113,7 @@ warning - +
widgets.table.page-step-count
-
{{ 'widgets.table.page-step-settings' | translate }}
+
{{ 'widgets.table.page-step-settings' | translate }}
widgets.table.page-step-increment
@@ -80,7 +80,7 @@ warning - +
widgets.table.page-step-count
@@ -44,6 +45,7 @@
@@ -58,7 +60,8 @@ [validationArgs]="[]" [editorCompleter]="customPrettyActionEditorCompleter" functionTitle="{{ 'widget-action.custom-pretty-function' | translate }}" - helpId="widget/action/custom_pretty_action_fn"> + [helpId]="helpId" + [helpPopupStyle]="{width: '900px'}">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.ts index 2e177eb918..77f835a668 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.ts @@ -14,28 +14,22 @@ /// limitations under the License. /// -// eslint-disable-next-line @typescript-eslint/triple-slash-reference -/// - import { AfterViewInit, Component, ElementRef, forwardRef, Input, - OnDestroy, - OnInit, QueryList, ViewChildren, ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { PageComponent } from '@shared/components/page.component'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; import { combineLatest } from 'rxjs'; -import { CustomActionDescriptor } from '@shared/models/widget.models'; -import { CustomPrettyActionEditorCompleter } from '@home/components/widget/lib/settings/common/action/custom-action.models'; +import { CustomActionDescriptor, WidgetActionType } from '@shared/models/widget.models'; +import { + CustomPrettyActionEditorCompleter +} from '@home/components/widget/lib/settings/common/action/custom-action.models'; @Component({ selector: 'tb-custom-action-pretty-editor', @@ -50,7 +44,7 @@ import { CustomPrettyActionEditorCompleter } from '@home/components/widget/lib/s ], encapsulation: ViewEncapsulation.None }) -export class CustomActionPrettyEditorComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor { +export class CustomActionPrettyEditorComponent implements AfterViewInit, ControlValueAccessor { @Input() disabled: boolean; @@ -58,6 +52,17 @@ export class CustomActionPrettyEditorComponent extends PageComponent implements fullscreen = false; + helpId= 'widget/action/custom_pretty_action_fn'; + + @Input() + set widgetActionType(type: WidgetActionType) { + if (type === WidgetActionType.placeMapItem) { + this.helpId = 'widget/action/place_map_item/place_map_item_action'; + } else { + this.helpId = 'widget/action/custom_pretty_action_fn'; + } + } + @ViewChildren('leftPanel') leftPanelElmRef: QueryList>; @@ -68,15 +73,11 @@ export class CustomActionPrettyEditorComponent extends PageComponent implements private propagateChange = (_: any) => {}; - constructor(protected store: Store) { - super(store); - } - - ngOnInit(): void { + constructor() { } ngAfterViewInit(): void { - combineLatest(this.leftPanelElmRef.changes, this.rightPanelElmRef.changes).subscribe(() => { + combineLatest([this.leftPanelElmRef.changes, this.rightPanelElmRef.changes]).subscribe(() => { if (this.leftPanelElmRef.length && this.rightPanelElmRef.length) { this.initSplitLayout(this.leftPanelElmRef.first.nativeElement, this.rightPanelElmRef.first.nativeElement); @@ -92,14 +93,11 @@ export class CustomActionPrettyEditorComponent extends PageComponent implements }); } - ngOnDestroy(): void { - } - registerOnChange(fn: any): void { this.propagateChange = fn; } - registerOnTouched(fn: any): void { + registerOnTouched(_fn: any): void { } setDisabledState(isDisabled: boolean): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.html index 9f351345c1..00c5f86dd3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.html @@ -101,7 +101,8 @@ [validationArgs]="[]" [editorCompleter]="customPrettyActionEditorCompleter" functionTitle="{{ 'widget-action.custom-pretty-function' | translate }}" - helpId="widget/action/custom_pretty_action_fn"> + [helpId]="helpId" + [helpPopupStyle]="{width: '900px'}"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.ts index 5c65e23b58..367f90d160 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.ts @@ -27,16 +27,15 @@ import { ViewChild, ViewEncapsulation } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; import { PageComponent } from '@shared/components/page.component'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; import { CustomActionDescriptor } from '@shared/models/widget.models'; import { Ace } from 'ace-builds'; import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; -import { CustomPrettyActionEditorCompleter } from '@home/components/widget/lib/settings/common/action/custom-action.models'; +import { + CustomPrettyActionEditorCompleter +} from '@home/components/widget/lib/settings/common/action/custom-action.models'; import { Observable } from 'rxjs/internal/Observable'; -import { forkJoin, from } from 'rxjs'; +import { forkJoin } from 'rxjs'; import { map, tap } from 'rxjs/operators'; import { getAce } from '@shared/models/ace/ace.models'; import { beautifyCss, beautifyHtml } from '@shared/models/beautify.models'; @@ -55,6 +54,9 @@ export class CustomActionPrettyResourcesTabsComponent extends PageComponent impl @Input() hasCustomFunction: boolean; + @Input() + helpId: string; + @Output() actionUpdated: EventEmitter = new EventEmitter(); @@ -76,10 +78,8 @@ export class CustomActionPrettyResourcesTabsComponent extends PageComponent impl customPrettyActionEditorCompleter = CustomPrettyActionEditorCompleter; - constructor(protected store: Store, - private translate: TranslateService, - private raf: RafService) { - super(store); + constructor(private raf: RafService) { + super(); } ngOnInit(): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action.models.ts index 1b63e97ad6..40f1a6c819 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action.models.ts @@ -22,6 +22,8 @@ import { deepClone, isDefined, isUndefined } from '@core/utils'; import customSampleJs from './custom-sample-js.raw'; import customSampleCss from './custom-sample-css.raw'; import customSampleHtml from './custom-sample-html.raw'; +import placeMapItemSampleHtml from './place-map-item-sample-html.raw'; +import placeMapItemSampleJs from './place-map-item-sample-js.raw'; const customActionCompletions: TbEditorCompletions = { ...{ @@ -96,5 +98,15 @@ export const toCustomAction = (action: WidgetAction): CustomActionDescriptor => return result; }; +export const toPlaceMapItemAction = (action: WidgetAction): CustomActionDescriptor => { + const result: CustomActionDescriptor = { + customHtml: action?.customHtml ?? placeMapItemSampleHtml, + customCss: action?.customCss ?? '', + customFunction: action?.customFunction ?? placeMapItemSampleJs + }; + result.customResources = isDefined(action?.customResources) ? deepClone(action.customResources) : []; + return result; +}; + export const CustomActionEditorCompleter = new TbEditorCompleter(customActionCompletions); export const CustomPrettyActionEditorCompleter = new TbEditorCompleter(customPrettyActionCompletions); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings.component.ts index 00cea43192..7bdb17c06a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings.component.ts @@ -129,23 +129,25 @@ export class GetValueActionSettingsComponent implements OnInit, ControlValueAcce if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const ctx: any = { - getValueSettings: this.modelValue, - panelTitle: this.panelTitle, - valueType: this.valueType, - trueLabel: this.trueLabel, - falseLabel: this.falseLabel, - stateLabel: this.stateLabel, - aliasController: this.aliasController, - targetDevice: this.targetDevice, - widgetType: this.widgetType - }; - const getValueSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, GetValueActionSettingsPanelComponent, - ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null, - ctx, - {}, - {}, {}, true); + const getValueSettingsPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: GetValueActionSettingsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], + context: { + getValueSettings: this.modelValue, + panelTitle: this.panelTitle, + valueType: this.valueType, + trueLabel: this.trueLabel, + falseLabel: this.falseLabel, + stateLabel: this.stateLabel, + aliasController: this.aliasController, + targetDevice: this.targetDevice, + widgetType: this.widgetType + }, + isModal: true + }); getValueSettingsPanelPopover.tbComponentRef.instance.popover = getValueSettingsPanelPopover; getValueSettingsPanelPopover.tbComponentRef.instance.getValueSettingsApplied.subscribe((getValueSettings) => { getValueSettingsPanelPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-html.raw b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-html.raw new file mode 100644 index 0000000000..bb36a39986 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-html.raw @@ -0,0 +1,82 @@ + + + + +
+ +

Add entity

+ + +
+ + +
+
+
+ + Entity Name + + + Entity name is required. + + + + Entity Label + + +
+
+ + + +
+
+
+ + Address + + + + Owner + + +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-js.raw b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-js.raw new file mode 100644 index 0000000000..cb8c23faee --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-js.raw @@ -0,0 +1,89 @@ +/*========================================================================*/ +/*========================= Add entity example =========================*/ +/*========================================================================*/ + +let $injector = widgetContext.$scope.$injector; +let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); +let assetService = $injector.get(widgetContext.servicesMap.get('assetService')); +let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); +let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService')); + +openAddEntityDialog(); + +function openAddEntityDialog() { + customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe(); +} + +function AddEntityDialogController(instance) { + let vm = instance; + + vm.allowedEntityTypes = ['ASSET', 'DEVICE']; + + vm.addEntityFormGroup = vm.fb.group({ + entityName: ['', [vm.validators.required]], + entityType: ['DEVICE'], + entityLabel: [null], + type: ['', [vm.validators.required]], + attributes: vm.fb.group({ + address: [null], + owner: [null] + }) + }); + + vm.cancel = function() { + vm.dialogRef.close(null); + }; + + vm.save = function() { + vm.addEntityFormGroup.markAsPristine(); + saveEntityObservable().pipe( + widgetContext.rxjs.switchMap((entity) => saveAttributes(entity.id)) + ).subscribe(() => { + widgetContext.updateAliases(); + vm.dialogRef.close(null); + }); + }; + + function saveEntityObservable() { + const formValues = vm.addEntityFormGroup.value; + let entity = { + name: formValues.entityName, + type: formValues.type, + label: formValues.entityLabel + }; + if (formValues.entityType == 'ASSET') { + return assetService.saveAsset(entity); + } else if (formValues.entityType == 'DEVICE') { + return deviceService.saveDevice(entity); + } + } + + function saveAttributes(entityId) { + let attributes = vm.addEntityFormGroup.get('attributes').value; + let attributesArray = getMapItemLocationAttributes(); + for (let key in attributes) { + if(attributes[key] !== null) { + attributesArray.push({key: key, value: attributes[key]}); + } + } + if (attributesArray.length > 0) { + return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray); + } + return widgetContext.rxjs.of([]); + } + + function getMapItemLocationAttributes() { + const attributes = []; + const mapItemType = $event.shape; + if (mapItemType === 'Marker') { + const mapType = widgetContext.mapInstance.type(); + attributes.push({key: mapType === 'image' ? 'xPos' : 'latitude', value: additionalParams.coordinates.x}); + attributes.push({key: mapType === 'image' ? 'yPos' : 'longitude', value: additionalParams.coordinates.y}); + } else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon') { + attributes.push({key: 'perimeter', value: additionalParams.coordinates}); + } else if (mapItemType === 'Circle') { + attributes.push({key: 'circle', value: additionalParams.coordinates}); + } + return attributes; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings.component.ts index 08345bb148..cf90db33ed 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings.component.ts @@ -115,20 +115,22 @@ export class SetValueActionSettingsComponent implements OnInit, ControlValueAcce if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const ctx: any = { - setValueSettings: this.modelValue, - panelTitle: this.panelTitle, - valueType: this.valueType, - aliasController: this.aliasController, - targetDevice: this.targetDevice, - widgetType: this.widgetType - }; - const setValueSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, SetValueActionSettingsPanelComponent, - ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null, - ctx, - {}, - {}, {}, true); + const setValueSettingsPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: SetValueActionSettingsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], + context: { + setValueSettings: this.modelValue, + panelTitle: this.panelTitle, + valueType: this.valueType, + aliasController: this.aliasController, + targetDevice: this.targetDevice, + widgetType: this.widgetType + }, + isModal: true + }); setValueSettingsPanelPopover.tbComponentRef.instance.popover = setValueSettingsPanelPopover; setValueSettingsPanelPopover.tbComponentRef.instance.setValueSettingsApplied.subscribe((setValueSettings) => { setValueSettingsPanelPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts index 7731df6407..de29366846 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts @@ -114,19 +114,21 @@ export class WidgetActionSettingsComponent implements OnInit, ControlValueAccess if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const ctx: any = { - widgetAction: this.modelValue, - panelTitle: this.panelTitle, - widgetType: this.widgetType, - callbacks: this.callbacks, - additionalWidgetActionTypes: this.additionalWidgetActionTypes - }; - const widgetActionSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, WidgetActionSettingsPanelComponent, - ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null, - ctx, - {}, - {}, {}, true); + const widgetActionSettingsPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: WidgetActionSettingsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], + context: { + widgetAction: this.modelValue, + panelTitle: this.panelTitle, + widgetType: this.widgetType, + callbacks: this.callbacks, + additionalWidgetActionTypes: this.additionalWidgetActionTypes + }, + isModal: true + }); widgetActionSettingsPanelPopover.tbComponentRef.instance.widgetActionApplied.subscribe((widgetAction) => { widgetActionSettingsPanelPopover.hide(); this.modelValue = widgetAction; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html index ca2405b8b4..5c735010f1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html @@ -37,7 +37,7 @@
widget-config.action
- + {{ widgetActionTypeTranslations.get(widgetActionType[actionType]) | translate }} @@ -256,6 +256,7 @@ [editorCompleter]="customActionEditorCompleter" withModules helpId="widget/action/custom_action_fn" + [helpPopupStyle]="{width: '900px'}" > @@ -274,7 +275,8 @@ || widgetActionFormGroup.get('type').value === widgetActionType.placeMapItem ? widgetActionFormGroup.get('type').value : ''"> + [widgetActionType]="widgetActionFormGroup.get('type').value" + formControlName="customAction"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts index 932c0e52c3..698645bacf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts @@ -26,7 +26,7 @@ import { ValidatorFn, Validators } from '@angular/forms'; -import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { Component, computed, ElementRef, forwardRef, input, Input, OnInit, ViewChild } from '@angular/core'; import { MapItemType, mapItemTypeTranslationMap, @@ -48,7 +48,8 @@ import { TranslateService } from '@ngx-translate/core'; import { PopoverPlacement, PopoverPlacements } from '@shared/components/popover.models'; import { CustomActionEditorCompleter, - toCustomAction + toCustomAction, + toPlaceMapItemAction } from '@home/components/widget/lib/settings/common/action/custom-action.models'; import { coerceBoolean } from '@shared/decorators/coercion'; @@ -101,19 +102,16 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali @Input() actionNames: string[]; - @Input() - set additionalWidgetActionTypes(value: WidgetActionType[]) { - if (this.widgetActionFormGroup && !widgetActionTypes.includes(this.widgetActionFormGroup.get('type').value)) { - this.widgetActionFormGroup.get('type').setValue(WidgetActionType.doNothing); - } - if (value?.length) { - this.widgetActionTypes = widgetActionTypes.concat(value); - } else { - this.widgetActionTypes = widgetActionTypes; + additionalWidgetActionTypes = input(null); + + actionTypes = computed(() => { + const predefinedActionTypes = widgetActionTypes; + if (this.additionalWidgetActionTypes()?.length) { + return predefinedActionTypes.concat(this.additionalWidgetActionTypes()); } - } + return predefinedActionTypes; + }); - widgetActionTypes = widgetActionTypes; widgetActionTypeTranslations = widgetActionTypeTranslationMap; widgetActionType = WidgetActionType; @@ -190,9 +188,6 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali ).subscribe(() => { this.widgetActionUpdated(); }); - if (this.additionalWidgetActionTypes) { - this.widgetActionTypes = this.widgetActionTypes.concat(this.additionalWidgetActionTypes); - } } writeValue(widgetAction?: WidgetAction): void { @@ -336,7 +331,7 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali ); this.actionTypeFormGroup.addControl( 'customAction', - this.fb.control(toCustomAction(action), [Validators.required]) + this.fb.control(toPlaceMapItemAction(action), [Validators.required]) ); break; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings.component.ts index ae4158e3ed..f46bce6dd6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings.component.ts @@ -78,15 +78,18 @@ export class AutoDateFormatSettingsComponent implements OnInit, ControlValueAcce if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const ctx: any = { - autoDateFormatSettings: deepClone(this.modelValue), - defaultValues: this.defaultValues - }; - const autoDateFormatSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, AutoDateFormatSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, - ctx, - {}, - {}, {}, true); + const autoDateFormatSettingsPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: AutoDateFormatSettingsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], + context: { + autoDateFormatSettings: deepClone(this.modelValue), + defaultValues: this.defaultValues + }, + isModal: true + }); autoDateFormatSettingsPanelPopover.tbComponentRef.instance.popover = autoDateFormatSettingsPanelPopover; autoDateFormatSettingsPanelPopover.tbComponentRef.instance.autoDateFormatSettingsApplied.subscribe((autoDateFormatSettings) => { autoDateFormatSettingsPanelPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts index c25bd1c8fb..a5ed2cf96a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts @@ -37,9 +37,10 @@ import { TbPopoverService } from '@shared/components/popover.service'; import { BackgroundSettingsPanelComponent } from '@home/components/widget/lib/settings/common/background-settings-panel.component'; -import { Observable, of } from 'rxjs'; +import { Observable, of, pipe } from 'rxjs'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { DomSanitizer } from '@angular/platform-browser'; +import { tap } from 'rxjs/operators'; @Component({ selector: 'tb-background-settings', @@ -106,14 +107,17 @@ export class BackgroundSettingsComponent implements OnInit, ControlValueAccessor if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const ctx: any = { - backgroundSettings: this.modelValue - }; - const backgroundSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, BackgroundSettingsPanelComponent, ['left'], true, null, - ctx, - {}, - {}, {}, true); + const backgroundSettingsPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: BackgroundSettingsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: 'left', + context: { + backgroundSettings: this.modelValue + }, + isModal: true + }); backgroundSettingsPanelPopover.tbComponentRef.instance.popover = backgroundSettingsPanelPopover; backgroundSettingsPanelPopover.tbComponentRef.instance.backgroundSettingsApplied.subscribe((backgroundSettings) => { backgroundSettingsPanelPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.ts index 5e4e049055..1cfc7a77cf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.ts @@ -124,19 +124,21 @@ export class WidgetButtonCustomStyleComponent implements OnInit, OnChanges, Cont if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const ctx: any = { - appearance: this.appearance, - borderRadius: this.borderRadius, - autoScale: this.autoScale, - state: this.state, - customStyle: this.modelValue - }; - const widgetButtonCustomStylePanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, WidgetButtonCustomStylePanelComponent, - ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null, - ctx, - {}, - {}, {}, true); + const widgetButtonCustomStylePanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: WidgetButtonCustomStylePanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], + context: { + appearance: this.appearance, + borderRadius: this.borderRadius, + autoScale: this.autoScale, + state: this.state, + customStyle: this.modelValue + }, + isModal: true + }); widgetButtonCustomStylePanelPopover.tbComponentRef.instance.popover = widgetButtonCustomStylePanelPopover; widgetButtonCustomStylePanelPopover.tbComponentRef.instance.customStyleApplied.subscribe((customStyle) => { widgetButtonCustomStylePanelPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.ts index 3e31420747..1e0f19ea58 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.ts @@ -127,20 +127,22 @@ export class WidgetButtonToggleCustomStyleComponent implements OnInit, OnChanges if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const ctx: any = { - appearance: this.appearance, - borderRadius: this.borderRadius, - autoScale: this.autoScale, - state: this.state, - value: this.value, - customStyle: this.modelValue - }; - const widgetButtonCustomStylePanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, WidgetButtonToggleCustomStylePanelComponent, - ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null, - ctx, - {}, - {}, {}, true); + const widgetButtonCustomStylePanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: WidgetButtonToggleCustomStylePanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], + context: { + appearance: this.appearance, + borderRadius: this.borderRadius, + autoScale: this.autoScale, + state: this.state, + value: this.value, + customStyle: this.modelValue + }, + isModal: true + }); widgetButtonCustomStylePanelPopover.tbComponentRef.instance.popover = widgetButtonCustomStylePanelPopover; widgetButtonCustomStylePanelPopover.tbComponentRef.instance.customStyleApplied.subscribe((customStyle) => { widgetButtonCustomStylePanelPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.ts index 25ce5238ef..016f213c0b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.ts @@ -85,17 +85,20 @@ export class TimeSeriesChartAxisSettingsButtonComponent implements OnInit, Contr if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const ctx: any = { - axisSettings: this.modelValue, - axisType: this.axisType, - panelTitle: this.panelTitle, - advanced: this.advanced - }; - const axisSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, TimeSeriesChartAxisSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, - ctx, - {}, - {}, {}, true); + const axisSettingsPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: TimeSeriesChartAxisSettingsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], + context: { + axisSettings: this.modelValue, + axisType: this.axisType, + panelTitle: this.panelTitle, + advanced: this.advanced + }, + isModal: true + }); axisSettingsPanelPopover.tbComponentRef.instance.popover = axisSettingsPanelPopover; axisSettingsPanelPopover.tbComponentRef.instance.axisSettingsApplied.subscribe((axisSettings) => { axisSettingsPanelPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings.component.ts index abb40594d6..d39e4b48dd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings.component.ts @@ -107,11 +107,21 @@ export class TimeSeriesChartThresholdSettingsComponent implements OnInit, Contro hideYAxis: this.hideYAxis, yAxisIds: this.yAxisIds }; - const thresholdSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, TimeSeriesChartThresholdSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, - ctx, - {}, - {}, {}, true); + const thresholdSettingsPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: TimeSeriesChartThresholdSettingsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], + context: { + thresholdSettings: deepClone(this.modelValue), + panelTitle: this.title, + widgetConfig: this.widgetConfig, + hideYAxis: this.hideYAxis, + yAxisIds: this.yAxisIds + }, + isModal: true + }); thresholdSettingsPanelPopover.tbComponentRef.instance.popover = thresholdSettingsPanelPopover; thresholdSettingsPanelPopover.tbComponentRef.instance.thresholdSettingsApplied.subscribe((thresholdSettings) => { thresholdSettingsPanelPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts index 425e436102..dec7148b30 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts @@ -151,17 +151,20 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const ctx: any = { - axisType: 'yAxis', - panelTitle: this.translate.instant('widgets.time-series-chart.axis.y-axis-settings'), - axisSettings: deepClone(this.modelValue), - advanced: this.advanced - }; - const yAxisSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, TimeSeriesChartAxisSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, - ctx, - {}, - {}, {}, true); + const yAxisSettingsPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: TimeSeriesChartAxisSettingsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], + context: { + axisType: 'yAxis', + panelTitle: this.translate.instant('widgets.time-series-chart.axis.y-axis-settings'), + axisSettings: deepClone(this.modelValue), + advanced: this.advanced + }, + isModal: true + }); yAxisSettingsPanelPopover.tbComponentRef.instance.popover = yAxisSettingsPanelPopover; yAxisSettingsPanelPopover.tbComponentRef.instance.axisSettingsApplied.subscribe((yAxisSettings) => { yAxisSettingsPanelPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-settings.component.ts index 10f574a9d4..23b1efbce9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-settings.component.ts @@ -121,15 +121,18 @@ export class ColorRangeSettingsComponent implements OnInit, ControlValueAccessor if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const ctx: any = { - colorRangeSettings: this.modelValue, - settingsComponents: this.colorSettingsComponentService.getOtherColorSettingsComponents(this) - }; - const colorRangeSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, ColorRangePanelComponent, 'left', true, null, - ctx, - {}, - {}, {}, true); + const colorRangeSettingsPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: ColorRangePanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: 'left', + context: { + colorRangeSettings: this.modelValue, + settingsComponents: this.colorSettingsComponentService.getOtherColorSettingsComponents(this) + }, + isModal: true + }); colorRangeSettingsPanelPopover.tbComponentRef.instance.popover = colorRangeSettingsPanelPopover; colorRangeSettingsPanelPopover.tbComponentRef.instance.colorRangeApplied.subscribe((colorRangeSettings: Array) => { colorRangeSettingsPanelPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts index 484d46fa38..05c069cbf3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts @@ -159,22 +159,25 @@ export class ColorSettingsComponent implements OnInit, ControlValueAccessor, OnD if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const ctx: any = { - colorSettings: this.modelValue, - settingsComponents: this.colorSettingsComponentService.getOtherColorSettingsComponents(this), - aliasController: this.aliasController, - dataKeyCallbacks: this.dataKeyCallbacks, - datasource: this.datasource, - rangeAdvancedMode: this.rangeAdvancedMode, - gradientAdvancedMode: this.gradientAdvancedMode, - minValue: this.minValue, - maxValue: this.maxValue - }; - const colorSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, ColorSettingsPanelComponent, 'left', true, null, - ctx, - {}, - {}, {}, true); + const colorSettingsPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: ColorSettingsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: 'left', + context: { + colorSettings: this.modelValue, + settingsComponents: this.colorSettingsComponentService.getOtherColorSettingsComponents(this), + aliasController: this.aliasController, + dataKeyCallbacks: this.dataKeyCallbacks, + datasource: this.datasource, + rangeAdvancedMode: this.rangeAdvancedMode, + gradientAdvancedMode: this.gradientAdvancedMode, + minValue: this.minValue, + maxValue: this.maxValue + }, + isModal: true + }); colorSettingsPanelPopover.tbComponentRef.instance.popover = colorSettingsPanelPopover; colorSettingsPanelPopover.tbComponentRef.instance.colorSettingsApplied.subscribe((colorSettings) => { colorSettingsPanelPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts index 3a626b528b..847e869fe6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts @@ -162,14 +162,16 @@ export class DateFormatSelectComponent implements OnInit, ControlValueAccessor { if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const ctx: any = { - dateFormat: deepClone(this.modelValue) - }; - const dateFormatSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, DateFormatSettingsPanelComponent, 'top', true, null, - ctx, - {}, - {}, {}, true); + const dateFormatSettingsPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: DateFormatSettingsPanelComponent, + hostView: this.viewContainerRef, + context: { + dateFormat: deepClone(this.modelValue) + }, + isModal: true + }); dateFormatSettingsPanelPopover.tbComponentRef.instance.popover = dateFormatSettingsPanelPopover; dateFormatSettingsPanelPopover.tbComponentRef.instance.dateFormatApplied.subscribe((dateFormat) => { dateFormatSettingsPanelPopover.hide(); @@ -187,15 +189,18 @@ export class DateFormatSelectComponent implements OnInit, ControlValueAccessor { if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const ctx: any = { - autoDateFormatSettings: mergeDeep({} as AutoDateFormatSettings, - defaultAutoDateFormatSettings, this.modelValue.autoDateFormatSettings) - }; - const autoDateFormatSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, AutoDateFormatSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, - ctx, - {}, - {}, {}, true); + const autoDateFormatSettingsPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: AutoDateFormatSettingsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], + context: { + autoDateFormatSettings: mergeDeep({} as AutoDateFormatSettings, + defaultAutoDateFormatSettings, this.modelValue.autoDateFormatSettings) + }, + isModal: true + }); autoDateFormatSettingsPanelPopover.tbComponentRef.instance.popover = autoDateFormatSettingsPanelPopover; autoDateFormatSettingsPanelPopover.tbComponentRef.instance.autoDateFormatSettingsApplied.subscribe((autoDateFormatSettings) => { autoDateFormatSettingsPanelPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts index 96526da502..58d879ba79 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts @@ -168,17 +168,20 @@ export class DynamicFormPropertyRowComponent implements ControlValueAccessor, On if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const ctx: any = { - isAdd: add, - disabled: this.disabled, - booleanPropertyIds: this.booleanPropertyIds, - property: deepClone(this.modelValue) - }; - const dynamicFormPropertyPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, DynamicFormPropertyPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, - ctx, - {}, - {}, {}, true); + const dynamicFormPropertyPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: DynamicFormPropertyPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], + context: { + isAdd: add, + disabled: this.disabled, + booleanPropertyIds: this.booleanPropertyIds, + property: deepClone(this.modelValue) + }, + isModal: true + }); dynamicFormPropertyPanelPopover.tbComponentRef.instance.popover = dynamicFormPropertyPanelPopover; dynamicFormPropertyPanelPopover.tbComponentRef.instance.propertySettingsApplied.subscribe((property) => { dynamicFormPropertyPanelPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html index 5e67638092..ff1bcb06a4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html @@ -15,9 +15,14 @@ limitations under the License. --> - - {{ 'filter.filter' | translate }} - + {{ 'filter.filter' | translate }} + - - + + warning + +
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.scss new file mode 100644 index 0000000000..3d5db8d73a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.scss @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2025 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 '../../../../../../../../../scss/constants'; + +.tb-form-table-row.tb-additional-map-data-source-row { + + .tb-source-field { + flex: 1 1 50%; + display: flex; + gap: 12px; + .tb-ds-type-field, .tb-label-field, .tb-device-field, .tb-entity-alias-field { + flex: 1; + } + } + + .tb-data-keys-field { + flex: 1 1 50%; + min-width: 0; + } + + .tb-remove-button { + width: 40px; + min-width: 40px; + } + + @media #{$mat-lt-lg} { + .tb-source-field { + flex-direction: column; + flex: 1 1 30%; + } + .tb-data-keys-field { + flex: 1 1 70%; + } + @media #{$mat-lt-md} { + .tb-source-field { + flex: 1 1 50%; + } + .tb-data-keys-field { + flex: 1 1 50%; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.ts new file mode 100644 index 0000000000..60adb60cb7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.ts @@ -0,0 +1,192 @@ +/// +/// Copyright © 2016-2025 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, + DestroyRef, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { AdditionalMapDataSourceSettings, updateDataKeyToNewDsType } from '@shared/models/widget/maps/map.models'; +import { DataKey, DatasourceType, datasourceTypeTranslationMap, widgetType } from '@shared/models/widget.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { genNextLabelForDataKeys } from '@core/utils'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; + +@Component({ + selector: 'tb-additional-map-data-source-row', + templateUrl: './additional-map-data-source-row.component.html', + styleUrls: ['./additional-map-data-source-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AdditionalMapDataSourceRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class AdditionalMapDataSourceRowComponent implements ControlValueAccessor, OnInit { + + DatasourceType = DatasourceType; + DataKeyType = DataKeyType; + + EntityType = EntityType; + + widgetType = widgetType; + + datasourceTypes: Array = []; + datasourceTypesTranslations = datasourceTypeTranslationMap; + + @Input() + disabled: boolean; + + @Input() + context: MapSettingsContext; + + @Output() + dataSourceRemoved = new EventEmitter(); + + dataSourceFormGroup: UntypedFormGroup; + + generateAdditionalDataKey = this.generateDataKey.bind(this); + + modelValue: AdditionalMapDataSourceSettings; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef, + private destroyRef: DestroyRef) { + } + + ngOnInit() { + if (this.context.functionsOnly) { + this.datasourceTypes = [DatasourceType.function]; + } else { + this.datasourceTypes = [DatasourceType.function, DatasourceType.device, DatasourceType.entity]; + } + this.dataSourceFormGroup = this.fb.group({ + dsType: [null, [Validators.required]], + dsLabel: [null, []], + dsDeviceId: [null, [Validators.required]], + dsEntityAliasId: [null, [Validators.required]], + dataKeys: [null, [Validators.required]] + }); + this.dataSourceFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => this.updateModel() + ); + this.dataSourceFormGroup.get('dsType').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + (newDsType: DatasourceType) => this.onDsTypeChanged(newDsType) + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.dataSourceFormGroup.disable({emitEvent: false}); + } else { + this.dataSourceFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: AdditionalMapDataSourceSettings): void { + this.modelValue = value; + this.dataSourceFormGroup.patchValue( + { + dsType: value?.dsType, + dsLabel: value?.dsLabel, + dsDeviceId: value?.dsDeviceId, + dsEntityAliasId: value?.dsEntityAliasId, + dataKeys: value?.dataKeys + }, {emitEvent: false} + ); + this.updateValidators(); + this.cd.markForCheck(); + } + + private generateDataKey(key: DataKey): DataKey { + const dataKey = this.context.callbacks.generateDataKey(key.name, key.type, null, false, null); + const dataKeys: DataKey[] = this.dataSourceFormGroup.get('dataKeys').value || []; + dataKey.label = genNextLabelForDataKeys(dataKey.label, dataKeys); + return dataKey; + } + + private onDsTypeChanged(newDsType: DatasourceType) { + let updateModel = false; + const dataKeys: DataKey[] = this.dataSourceFormGroup.get('dataKeys').value; + if (dataKeys?.length) { + for (const key of dataKeys) { + updateModel = updateDataKeyToNewDsType(key, newDsType) || updateModel; + } + if (updateModel) { + this.dataSourceFormGroup.get('dataKeys').patchValue(dataKeys, {emitEvent: false}); + } + } + this.updateValidators(); + if (updateModel) { + this.updateModel(); + } + } + + private updateValidators() { + const dsType: DatasourceType = this.dataSourceFormGroup.get('dsType').value; + if (dsType === DatasourceType.function) { + this.dataSourceFormGroup.get('dsLabel').enable({emitEvent: false}); + this.dataSourceFormGroup.get('dsDeviceId').disable({emitEvent: false}); + this.dataSourceFormGroup.get('dsEntityAliasId').disable({emitEvent: false}); + } else if (dsType === DatasourceType.device) { + this.dataSourceFormGroup.get('dsLabel').disable({emitEvent: false}); + this.dataSourceFormGroup.get('dsDeviceId').enable({emitEvent: false}); + this.dataSourceFormGroup.get('dsEntityAliasId').disable({emitEvent: false}); + } else { + this.dataSourceFormGroup.get('dsLabel').disable({emitEvent: false}); + this.dataSourceFormGroup.get('dsDeviceId').disable({emitEvent: false}); + this.dataSourceFormGroup.get('dsEntityAliasId').enable({emitEvent: false}); + } + } + + private updateModel() { + this.modelValue = {...this.modelValue, ...this.dataSourceFormGroup.value}; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-sources.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-sources.component.html new file mode 100644 index 0000000000..7492dd90ff --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-sources.component.html @@ -0,0 +1,43 @@ + +
+
+
+
widgets.maps.data-layer.source
+
widgets.maps.data-layer.data-keys
+
+
+
+
+ + +
+
+
+
+ +
+
+ + {{ 'widgets.maps.data-layer.no-datasources' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-sources.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-sources.component.scss new file mode 100644 index 0000000000..de4e639051 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-sources.component.scss @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2025 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 '../../../../../../../../../scss/constants'; + +.tb-additional-map-data-sources { + .tb-form-table-header-cell { + &.tb-source-header { + flex: 1 1 50%; + } + &.tb-data-keys-header { + flex: 1 1 50%; + } + &.tb-actions-header { + width: 40px; + min-width: 40px; + } + @media #{$mat-lt-lg} { + &.tb-source-header { + flex: 1 1 30%; + } + &.tb-data-keys-header { + flex: 1 1 70%; + } + @media #{$mat-lt-md} { + &.tb-source-header { + flex: 1 1 50%; + } + &.tb-data-keys-header { + flex: 1 1 50%; + } + } + } + } + + .tb-form-table-body { + tb-additional-map-data-source-row { + overflow: hidden; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-sources.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-sources.component.ts new file mode 100644 index 0000000000..5cb2ade353 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-sources.component.ts @@ -0,0 +1,147 @@ +/// +/// Copyright © 2016-2025 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, DestroyRef, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { mergeDeep } from '@core/utils'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + AdditionalMapDataSourceSettings, + additionalMapDataSourceValid, + additionalMapDataSourceValidator, + defaultAdditionalMapDataSourceSettings +} from '@shared/models/widget/maps/map.models'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; + +@Component({ + selector: 'tb-additional-map-data-sources', + templateUrl: './additional-map-data-sources.component.html', + styleUrls: ['./additional-map-data-sources.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AdditionalMapDataSourcesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AdditionalMapDataSourcesComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class AdditionalMapDataSourcesComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + disabled: boolean; + + @Input() + context: MapSettingsContext; + + dataSourcesFormGroup: UntypedFormGroup; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private destroyRef: DestroyRef) { + } + + ngOnInit() { + this.dataSourcesFormGroup = this.fb.group({ + dataSources: [this.fb.array([]), []] + }); + this.dataSourcesFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => { + let dataSources: AdditionalMapDataSourceSettings[] = this.dataSourcesFormGroup.get('dataSources').value; + if (dataSources) { + dataSources = dataSources.filter(dataSource => additionalMapDataSourceValid(dataSource)); + } + this.propagateChange(dataSources); + } + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.dataSourcesFormGroup.disable({emitEvent: false}); + } else { + this.dataSourcesFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: AdditionalMapDataSourceSettings[] | undefined): void { + const dataSources: AdditionalMapDataSourceSettings[] = value || []; + this.dataSourcesFormGroup.setControl('dataSources', this.prepareDataSourcesFormArray(dataSources), {emitEvent: false}); + } + + public validate(c: UntypedFormControl) { + const valid = this.dataSourcesFormGroup.valid; + return valid ? null : { + dataSources: { + valid: false, + }, + }; + } + + dataSourcesFormArray(): UntypedFormArray { + return this.dataSourcesFormGroup.get('dataSources') as UntypedFormArray; + } + + trackByDataSource(index: number, dataSourceControl: AbstractControl): any { + return dataSourceControl; + } + + removeDataSource(index: number) { + (this.dataSourcesFormGroup.get('dataSources') as UntypedFormArray).removeAt(index); + } + + addDataSource() { + const dataSource = mergeDeep({} as AdditionalMapDataSourceSettings, + defaultAdditionalMapDataSourceSettings(this.context.functionsOnly)); + const dataSourcesArray = this.dataSourcesFormGroup.get('dataSources') as UntypedFormArray; + const dataSourceControl = this.fb.control(dataSource, [additionalMapDataSourceValidator]); + dataSourcesArray.push(dataSourceControl); + } + + private prepareDataSourcesFormArray(dataSources: AdditionalMapDataSourceSettings[]): UntypedFormArray { + const dataSourcesControls: Array = []; + dataSources.forEach((dataSource) => { + dataSourcesControls.push(this.fb.control(dataSource, [additionalMapDataSourceValidator])); + }); + return this.fb.array(dataSourcesControls); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.html index 51cbc572a5..3501559523 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.html @@ -39,7 +39,7 @@
- + [helpId]="helpId" + [helpPopupStyle]="{width: '900px'}">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.scss index 47ab07639b..4d5a14283d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.scss @@ -43,6 +43,7 @@ display: flex; flex-direction: column; min-height: 0; + overflow: auto; } .tb-data-layer-color-settings-panel-buttons { height: 40px; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts index 5f56bd85c3..ea5da11890 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts @@ -97,19 +97,22 @@ export class DataLayerColorSettingsComponent implements ControlValueAccessor { if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const ctx: any = { - colorSettings: this.modelValue, - context: this.context, - dsType: this.dsType, - dsEntityAliasId: this.dsEntityAliasId, - dsDeviceId: this.dsDeviceId, - helpId: this.helpId - }; - const colorSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, DataLayerColorSettingsPanelComponent, 'left', true, null, - ctx, - {}, - {}, {}, true); + const colorSettingsPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: DataLayerColorSettingsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: 'left', + context: { + colorSettings: this.modelValue, + context: this.context, + dsType: this.dsType, + dsEntityAliasId: this.dsEntityAliasId, + dsDeviceId: this.dsDeviceId, + helpId: this.helpId + }, + isModal: true + }); colorSettingsPanelPopover.tbComponentRef.instance.popover = colorSettingsPanelPopover; colorSettingsPanelPopover.tbComponentRef.instance.colorSettingsApplied.subscribe((colorSettings) => { colorSettingsPanelPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html index 93ec63c128..451d3f7282 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html @@ -46,7 +46,8 @@ [globalVariables]="functionScopeVariables" [functionArgs]="['data', 'dsData']" functionTitle="{{ (patternType === 'label' ? 'widgets.maps.data-layer.label-function' : 'widgets.maps.data-layer.tooltip-function') | translate }}" - [helpId]="helpId"> + [helpId]="helpId" + [helpPopupStyle]="{width: '900px'}">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index 803cdd01f1..d124757183 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -65,6 +65,18 @@ [callbacks]="context.callbacks">
+ + + + widgets.maps.data-layer.more-datasources + + + + + + +
{{ 'datakey.keys' | translate }}
@@ -430,7 +442,7 @@ }
-
{{ 'widgets.maps.data-layer.groups' | translate }}
+
{{ 'widgets.maps.data-layer.groups' | translate }}
- {{ 'widgets.maps.data-layer.enable-snapping' | translate }} + + {{ 'widgets.maps.data-layer.enable-snapping' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index dc0123406a..e7e11284eb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -31,7 +31,8 @@ import { pathDecoratorSymbolTranslationMap, PolygonsDataLayerSettings, ShapeDataLayerSettings, - TripsDataLayerSettings + TripsDataLayerSettings, + updateDataKeyToNewDsType } from '@shared/models/widget/maps/map.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -179,6 +180,7 @@ export class MapDataLayerDialogComponent extends DialogComponent - - -
- - + formControlName="dsFilterId" + [callbacks]="context.callbacks"> +
- -
+ + +
+ [widgetTitlePanel]="widgetTitlePanel" + [widgetHeaderActionsPanel]="widgetHeaderActionsPanel">
@@ -102,6 +79,36 @@
+ +
+
+ @for (action of widget.customHeaderActions; track action.name; let last = $last) { + + } +
+ + + +
+
+ @switch (action.buttonType) { @case (widgetHeaderActionButtonType.miniFab) { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts index 430cce40b1..51ed592e7e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts @@ -48,6 +48,7 @@ import { TbContextMenuEvent } from '@shared/models/jquery-event.models'; import { WidgetHeaderActionButtonType } from '@shared/models/widget.models'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import ITooltipsterGeoHelper = JQueryTooltipster.ITooltipsterGeoHelper; +import { WidgetComponent } from '@home/components/widget/widget.component'; export enum WidgetComponentActionType { MOUSE_DOWN, @@ -216,6 +217,12 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O }); } + widgetActionAbsolute(widgetComponent: WidgetComponent, absolute = false) { + return absolute ? true : + !(this.widget.showWidgetTitlePanel && !widgetComponent.widgetContext?.embedTitlePanel && + (this.widget.showTitle||this.widget.hasAggregation)) && !widgetComponent.widgetContext?.embedActionsPanel; + } + onClicked(event: MouseEvent): void { if (event && this.isEdit) { event.stopPropagation(); 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 f8fa032c8c..fc996fb2c6 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 @@ -139,6 +139,9 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, @Input() widgetTitlePanel: TemplateRef; + @Input() + widgetHeaderActionsPanel: TemplateRef; + @Input() isEdit: boolean; @@ -365,7 +368,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, headerButtonStyle(buttonType: WidgetHeaderActionButtonType = WidgetHeaderActionButtonType.icon, customButtonStyle:{[key: string]: string}, - buttonColor: string = 'rgba(0,0,0,0.87)', + buttonColor: string = this.widget.config.color, backgroundColor: string, borderColor: string) { const buttonStyle = {}; @@ -483,6 +486,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.widgetType = this.widgetInfo.widgetTypeFunction; this.typeParameters = this.widgetInfo.typeParameters; this.widgetContext.embedTitlePanel = this.typeParameters.embedTitlePanel; + this.widgetContext.embedActionsPanel = this.typeParameters.embedActionsPanel; this.widgetContext.overflowVisible = this.typeParameters.overflowVisible; if (!this.widgetType) { diff --git a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts index 458a1d1eeb..facb159af4 100644 --- a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts @@ -468,49 +468,51 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { const resizable = item.resize; - this.heightValue = resizable.height; - this.widthValue = resizable.width; - - const setItemHeight = resizable.setItemHeight.bind(resizable); - const setItemWidth = resizable.setItemWidth.bind(resizable); - resizable.setItemHeight = (height) => { - setItemHeight(height); - this.heightValue = height; - if (this.preserveAspectRatio) { - setItemWidth(height * this.aspectRatio); - } - }; - resizable.setItemWidth = (width) => { - setItemWidth(width); - this.widthValue = width; - if (this.preserveAspectRatio) { - setItemHeight(width / this.aspectRatio); - } - }; - - Object.defineProperty(resizable, 'height', { - get: () => this.heightValue, - set: v => { - if (this.heightValue !== v) { - if (this.preserveAspectRatio) { - this.widthValue = v * this.aspectRatio; + if (resizable) { + this.heightValue = resizable.height; + this.widthValue = resizable.width; + + const setItemHeight = resizable.setItemHeight.bind(resizable); + const setItemWidth = resizable.setItemWidth.bind(resizable); + resizable.setItemHeight = (height) => { + setItemHeight(height); + this.heightValue = height; + if (this.preserveAspectRatio) { + setItemWidth(height * this.aspectRatio); + } + }; + resizable.setItemWidth = (width) => { + setItemWidth(width); + this.widthValue = width; + if (this.preserveAspectRatio) { + setItemHeight(width / this.aspectRatio); + } + }; + + Object.defineProperty(resizable, 'height', { + get: () => this.heightValue, + set: v => { + if (this.heightValue !== v) { + if (this.preserveAspectRatio) { + this.widthValue = v * this.aspectRatio; + } + this.heightValue = v; } - this.heightValue = v; } - } - }); + }); - Object.defineProperty(resizable, 'width', { - get: () => this.widthValue, - set: v => { - if (this.widthValue !== v) { - if (this.preserveAspectRatio) { - this.heightValue = v / this.aspectRatio; + Object.defineProperty(resizable, 'width', { + get: () => this.widthValue, + set: v => { + if (this.widthValue !== v) { + if (this.preserveAspectRatio) { + this.heightValue = v / this.aspectRatio; + } + this.widthValue = v; } - this.widthValue = v; } - } - }); + }); + } this.preserveAspectRatioApplied = true; } } 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 11adb2f806..00afa2b68d 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 @@ -312,6 +312,7 @@ export class WidgetContext { timeWindow?: WidgetTimewindow; embedTitlePanel?: boolean; + embedActionsPanel?: boolean; overflowVisible?: boolean; hideTitlePanel = false; diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts index bbd096bbaf..eceda5ac9a 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts @@ -166,11 +166,17 @@ export class MobileAppComponent extends EntityComponent { ? this.entityForm.get('versionInfo.latestVersionReleaseNotes').value : this.entityForm.get('versionInfo.minVersionReleaseNotes').value }; - const releaseNotesPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, EditorPanelComponent, ['leftOnly', 'leftBottomOnly', 'leftTopOnly'], true, null, - ctx, - {}, - {}, {}, false, () => {}, {padding: '16px 24px'}); + const releaseNotesPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + hostView: this.viewContainerRef, + componentType: EditorPanelComponent, + preferredPlacement: ['leftOnly', 'leftBottomOnly', 'leftTopOnly'], + context: ctx, + showCloseButton: false, + popoverContentStyle: {padding: '16px 24px'}, + isModal: false + }); releaseNotesPanelPopover.tbComponentRef.instance.popover = releaseNotesPanelPopover; releaseNotesPanelPopover.tbComponentRef.instance.editorContentApplied.subscribe((releaseNotes) => { releaseNotesPanelPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.ts index 33be223815..9f4add3195 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.ts @@ -55,6 +55,7 @@ import { TbPopoverService } from '@shared/components/popover.service'; import { CustomMobilePagePanelComponent } from '@home/pages/mobile/bundes/layout/custom-mobile-page-panel.component'; import { DefaultMobilePagePanelComponent } from '@home/pages/mobile/bundes/layout/default-mobile-page-panel.component'; import { TranslateService } from '@ngx-translate/core'; +import { DisplayPopoverConfig } from '@shared/components/popover.models'; @Component({ selector: 'tb-mobile-menu-item-row', @@ -222,27 +223,31 @@ export class MobilePageItemRowComponent implements ControlValueAccessor, OnInit, if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const ctx: any = { - disabled: this.disabled, - pageItem: deepClone(this.modelValue) + const config: DisplayPopoverConfig = { + trigger, + renderer: this.renderer, + componentType: undefined, + hostView: this.viewContainerRef, + preferredPlacement: ['right', 'bottom', 'top'], + context: { + disabled: this.disabled, + pageItem: deepClone(this.modelValue) + }, + showCloseButton: false, + popoverContentStyle: {padding: '16px 24px'}, + isModal: true }; if (this.isDefaultMenuItem) { - const defaultMobilePagePanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, DefaultMobilePagePanelComponent, ['right', 'bottom', 'top'], true, null, - ctx, - {}, - {}, {}, false, () => {}, {padding: '16px 24px'}); + config.componentType = DefaultMobilePagePanelComponent; + const defaultMobilePagePanelPopover = this.popoverService.displayPopover(config); defaultMobilePagePanelPopover.tbComponentRef.instance.popover = defaultMobilePagePanelPopover; defaultMobilePagePanelPopover.tbComponentRef.instance.defaultMobilePageApplied.subscribe((menuItem) => { defaultMobilePagePanelPopover.hide(); this.afterPageEdit(menuItem); }); } else { - const customMobilePagePanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, CustomMobilePagePanelComponent, ['right', 'bottom', 'top'], true, null, - ctx, - {}, - {}, {}, false, () => {}, {padding: '16px 24px'}); + config.componentType = CustomMobilePagePanelComponent; + const customMobilePagePanelPopover = this.popoverService.displayPopover(config); customMobilePagePanelPopover.tbComponentRef.instance.popover = customMobilePagePanelPopover; customMobilePagePanelPopover.tbComponentRef.instance.customMobilePageApplied.subscribe((page) => { customMobilePagePanelPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts index 3b1db9a595..7cde50ef29 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts @@ -203,18 +203,21 @@ export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, On if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const ctx: any = { - isAdd: add, - disabled: this.disabled, - aliasController: this.aliasController, - callbacks: this.callbacks, - behavior: deepClone(this.modelValue) - }; - const scadaSymbolBehaviorPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, ScadaSymbolBehaviorPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, - ctx, - {}, - {}, {}, true); + const scadaSymbolBehaviorPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: ScadaSymbolBehaviorPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], + context: { + isAdd: add, + disabled: this.disabled, + aliasController: this.aliasController, + callbacks: this.callbacks, + behavior: deepClone(this.modelValue) + }, + isModal: true + }); scadaSymbolBehaviorPanelPopover.tbComponentRef.instance.popover = scadaSymbolBehaviorPanelPopover; scadaSymbolBehaviorPanelPopover.tbComponentRef.instance.behaviorSettingsApplied.subscribe((behavior) => { scadaSymbolBehaviorPanelPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts index 95b49d47c2..339ac3a358 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts @@ -158,19 +158,21 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On tagFunctionControl = this.tagFormGroup.get('clickAction'); completer = this.clickActionFunctionCompleter; } - const ctx: any = { - tagFunction: tagFunctionControl.value, - tagFunctionType, - tag: this.tagFormGroup.get('tag').value, - completer, - disabled: this.disabled - }; - const scadaSymbolTagFunctionPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, ScadaSymbolMetadataTagFunctionPanelComponent, - ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, - ctx, - {}, - {}, {}, true); + const scadaSymbolTagFunctionPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: ScadaSymbolMetadataTagFunctionPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], + context: { + tagFunction: tagFunctionControl.value, + tagFunctionType, + tag: this.tagFormGroup.get('tag').value, + completer, + disabled: this.disabled + }, + isModal: true + }); scadaSymbolTagFunctionPanelPopover.tbComponentRef.instance.popover = scadaSymbolTagFunctionPanelPopover; scadaSymbolTagFunctionPanelPopover.tbComponentRef.instance.tagFunctionApplied.subscribe((tagFunction) => { scadaSymbolTagFunctionPanelPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts index 6675425f32..93c411aabe 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts @@ -816,11 +816,15 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe const ctx: any = { modules: deepClone(this.controllerScriptModules) }; - const modulesPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, JsFuncModulesComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, - ctx, - {}, - {}, {}, true); + const modulesPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + hostView: this.viewContainerRef, + componentType: JsFuncModulesComponent, + preferredPlacement: ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], + context: ctx, + isModal: true + }); modulesPanelPopover.tbComponentRef.instance.popover = modulesPanelPopover; modulesPanelPopover.tbComponentRef.instance.modulesApplied.subscribe((modules) => { modulesPanelPopover.hide(); diff --git a/ui-ngx/src/app/shared/components/color-input.component.ts b/ui-ngx/src/app/shared/components/color-input.component.ts index 00ef7059d7..d0dc7235f7 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.ts +++ b/ui-ngx/src/app/shared/components/color-input.component.ts @@ -196,14 +196,21 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, ColorPickerPanelComponent, ['left'], true, null, - { - color: this.colorFormGroup.get('color').value, - colorClearButton: this.colorClearButton, - colorCancelButton: true - }, - {}, {}, {}, false, () => {}, {padding: '12px 4px 12px 12px'}); + const colorPickerPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + hostView: this.viewContainerRef, + componentType: ColorPickerPanelComponent, + preferredPlacement: ['left'], + context: { + color: this.colorFormGroup.get('color').value, + colorClearButton: this.colorClearButton, + colorCancelButton: true + }, + showCloseButton: false, + popoverContentStyle: {padding: '12px 4px 12px 12px'}, + isModal: true + }) colorPickerPopover.tbComponentRef.instance.popover = colorPickerPopover; colorPickerPopover.tbComponentRef.instance.colorSelected.subscribe((color) => { colorPickerPopover.hide(); diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.html index c85fc4eb21..b21734cfdd 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.html @@ -15,9 +15,9 @@ limitations under the License. --> - + {{ 'entity.type' | translate }} - + {{ displayEntityTypeFn(type) }} diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts index 773222dcec..ce599a0c08 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts @@ -14,24 +14,14 @@ /// limitations under the License. /// -import { - AfterViewInit, - Component, - DestroyRef, - forwardRef, - Input, - OnChanges, - OnInit, - SimpleChanges -} from '@angular/core'; +import { Component, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { Store } from '@ngrx/store'; -import { AppState } from '@app/core/core.state'; import { TranslateService } from '@ngx-translate/core'; import { AliasEntityType, EntityType, entityTypeTranslations } from '@app/shared/models/entity-type.models'; import { EntityService } from '@core/http/entity.service'; import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; @Component({ selector: 'tb-entity-type-select', @@ -43,7 +33,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; multi: true }] }) -export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges { +export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, OnChanges { entityTypeFormGroup: UntypedFormGroup; @@ -72,12 +62,14 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, @Input() additionEntityTypes: {[key in string]: string} = {}; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + entityTypes: Array; private propagateChange = (v: any) => { }; - constructor(private store: Store, - private entityService: EntityService, + constructor(private entityService: EntityService, public translate: TranslateService, private fb: UntypedFormBuilder, private destroyRef: DestroyRef) { @@ -136,9 +128,6 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, } } - ngAfterViewInit(): void { - } - setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { diff --git a/ui-ngx/src/app/shared/components/js-func.component.ts b/ui-ngx/src/app/shared/components/js-func.component.ts index 5568e6779d..435b0495ec 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.ts +++ b/ui-ngx/src/app/shared/components/js-func.component.ts @@ -336,7 +336,7 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal } private updatedScriptLanguage() { - this.jsEditor.session.setMode(`ace/mode/${ScriptLanguage.TBEL === this.scriptLanguage ? 'tbel' : 'javascript'}`); + this.jsEditor?.session?.setMode(`ace/mode/${ScriptLanguage.TBEL === this.scriptLanguage ? 'tbel' : 'javascript'}`); } validateOnSubmit(): Observable { @@ -518,14 +518,17 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const ctx: any = { - modules: deepClone(this.modules) - }; - const modulesPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, JsFuncModulesComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, - ctx, - {}, - {}, {}, true); + const modulesPanelPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: JsFuncModulesComponent, + hostView: this.viewContainerRef, + preferredPlacement: 'leftTop', + context: { + modules: deepClone(this.modules) + }, + isModal: true + }); modulesPanelPopover.tbComponentRef.instance.popover = modulesPanelPopover; modulesPanelPopover.tbComponentRef.instance.modulesApplied.subscribe((modules) => { modulesPanelPopover.hide(); @@ -582,7 +585,7 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal private updateHighlightRules(): void { // @ts-ignore - if (!!this.jsEditor.session.$mode) { + if (!!this.jsEditor?.session?.$mode) { // @ts-ignore const newMode = new this.jsEditor.session.$mode.constructor(); newMode.$highlightRules = new newMode.HighlightRules(); @@ -598,7 +601,7 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal if (this.scriptLanguage === ScriptLanguage.TBEL) { newMode.$highlightRules.$rules.start = [...tbelUtilsFuncHighlightRules, ...newMode.$highlightRules.$rules.start]; } - const identifierRule = newMode.$highlightRules.$rules.no_regex.find(rule => rule.token?.includes('identifier')); + const identifierRule = newMode.$highlightRules.$rules.no_regex.find(rule => Array.isArray(rule.token) && rule.token.includes('identifier')); if (identifierRule && identifierRule.next === 'no_regex') { identifierRule.next = 'start'; } @@ -609,7 +612,7 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal private updateJsWorkerGlobals() { // @ts-ignore - if (!!this.jsEditor.session.$worker) { + if (!!this.jsEditor?.session?.$worker) { const jsWorkerOptions = { undef: !this.disableUndefinedCheck, unused: true, @@ -638,6 +641,9 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal } updateCompleters() { + if (!this.jsEditor) { + return; + } let modulesCompleterObservable: Observable; if (this.withModules) { modulesCompleterObservable = loadModulesCompleter(this.http, this.modules); diff --git a/ui-ngx/src/app/shared/components/popover.component.ts b/ui-ngx/src/app/shared/components/popover.component.ts index 21cf4a83c5..4344b32fd7 100644 --- a/ui-ngx/src/app/shared/components/popover.component.ts +++ b/ui-ngx/src/app/shared/components/popover.component.ts @@ -311,6 +311,7 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit { #overlay="cdkConnectedOverlay" cdkConnectedOverlay [cdkConnectedOverlayHasBackdrop]="hasBackdrop" + [cdkConnectedOverlayBackdropClass]="backdropClass" [cdkConnectedOverlayOrigin]="origin" [cdkConnectedOverlayPositions]="positions" [cdkConnectedOverlayScrollStrategy]="scrollStrategy" @@ -382,6 +383,7 @@ export class TbPopoverComponent implements OnDestroy, OnInit { tbMouseLeaveDelay?: number; tbHideOnClickOutside = true; tbShowCloseButton = true; + tbModal = false; tbAnimationState = 'active'; @@ -461,7 +463,11 @@ export class TbPopoverComponent implements OnDestroy, OnInit { } get hasBackdrop(): boolean { - return this.tbTrigger === 'click' ? this.tbBackdrop : false; + return this.tbModal || (this.tbTrigger === 'click' && this.tbBackdrop); + } + + get backdropClass(): string { + return this.tbModal ? 'tb-popover-overlay-backdrop' : ''; } preferredPlacement: PopoverPlacement = 'top'; @@ -634,7 +640,7 @@ export class TbPopoverComponent implements OnDestroy, OnInit { } onClickOutside(event: MouseEvent): void { - if (this.tbHideOnClickOutside && !this.origin.elementRef.nativeElement.contains(event.target) && this.tbTrigger !== null) { + if (!this.tbModal && this.tbHideOnClickOutside && !this.origin.elementRef.nativeElement.contains(event.target) && this.tbTrigger !== null) { if (!this.isTopOverlay(event.target as Element)) { this.hide(); } diff --git a/ui-ngx/src/app/shared/components/popover.models.ts b/ui-ngx/src/app/shared/components/popover.models.ts index 15db9b1f93..dc7e1f9ba7 100644 --- a/ui-ngx/src/app/shared/components/popover.models.ts +++ b/ui-ngx/src/app/shared/components/popover.models.ts @@ -18,6 +18,7 @@ import { animate, AnimationTriggerMetadata, style, transition, trigger } from '@ import { ConnectedOverlayPositionChange } from '@angular/cdk/overlay'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { POSITION_MAP } from '@shared/models/overlay.models'; +import { ComponentRef, Injector, Renderer2, Type, ViewContainerRef } from '@angular/core'; export const popoverMotion: AnimationTriggerMetadata = trigger('popoverMotion', [ transition('void => active', [ @@ -88,3 +89,36 @@ export interface PopoverWithTrigger { trigger: Element; popoverComponent: TbPopoverComponent; } + +export interface DisplayPopoverConfig extends Omit, 'componentRef'>{ + hostView: ViewContainerRef; +} + +export interface DisplayPopoverWithComponentRefConfig { + componentRef: ComponentRef + trigger: Element; + renderer: Renderer2; + componentType: Type; + preferredPlacement?: PopoverPreferredPlacement; + hideOnClickOutside?: boolean; + injector?: Injector; + context?: any; + overlayStyle?: any; + popoverStyle?: any; + style?: any, + showCloseButton?: boolean; + visibleFn?: (visible: boolean) => void; + popoverContentStyle?: any; + isModal?: boolean; +} + +export const defaultPopoverConfig: Partial> = { + preferredPlacement: 'top', + hideOnClickOutside: true, + overlayStyle: {}, + popoverStyle: {}, + showCloseButton: true, + visibleFn: () => {}, + popoverContentStyle: {}, + isModal: false +}; diff --git a/ui-ngx/src/app/shared/components/popover.service.ts b/ui-ngx/src/app/shared/components/popover.service.ts index ce1288651c..15a18fb2fc 100644 --- a/ui-ngx/src/app/shared/components/popover.service.ts +++ b/ui-ngx/src/app/shared/components/popover.service.ts @@ -24,12 +24,19 @@ import { Type, ViewContainerRef } from '@angular/core'; -import { PopoverPreferredPlacement, PopoverWithTrigger } from '@shared/components/popover.models'; +import { + defaultPopoverConfig, + DisplayPopoverConfig, + DisplayPopoverWithComponentRefConfig, + PopoverPreferredPlacement, + PopoverWithTrigger +} from '@shared/components/popover.models'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { ComponentType } from '@angular/cdk/portal'; import { HELP_MARKDOWN_COMPONENT_TOKEN } from '@shared/components/tokens'; import { CdkOverlayOrigin } from '@angular/cdk/overlay'; import { Observable } from 'rxjs'; +import { mergeDeep } from '@core/utils'; @Injectable() export class TbPopoverService { @@ -58,57 +65,108 @@ export class TbPopoverService { return hostView.createComponent(TbPopoverComponent); } + displayPopover(config: DisplayPopoverConfig): TbPopoverComponent; displayPopover(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, - componentType: Type, preferredPlacement: PopoverPreferredPlacement = 'top', - hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: any = {}, - popoverStyle: any = {}, style?: any, - showCloseButton = true, visibleFn: (visible: boolean) => void = () => {}, - popoverContentStyle: any = {}): TbPopoverComponent { - const componentRef = this.createPopoverRef(hostView); - return this.displayPopoverWithComponentRef(componentRef, trigger, renderer, componentType, preferredPlacement, hideOnClickOutside, - injector, context, overlayStyle, popoverStyle, style, showCloseButton, visibleFn, popoverContentStyle); + componentType: Type, preferredPlacement: PopoverPreferredPlacement, + hideOnClickOutside: boolean, injector?: Injector, context?: any, overlayStyle?: any, + popoverStyle?: any, style?: any, + showCloseButton?: boolean, visibleFn?: (visible: boolean) => void, + popoverContentStyle?: any): TbPopoverComponent; + displayPopover(config: Element | DisplayPopoverConfig, renderer?: Renderer2, hostView?: ViewContainerRef, + componentType?: Type, preferredPlacement?: PopoverPreferredPlacement, + hideOnClickOutside?: boolean, injector?: Injector, context?: any, overlayStyle?: any, + popoverStyle?: any, style?: any, + showCloseButton?: boolean, visibleFn?: (visible: boolean) => void, + popoverContentStyle?: any): TbPopoverComponent { + if (!(config instanceof Element) && 'trigger' in config && 'renderer' in config && 'componentType' in config) { + const componentRef = this.createPopoverRef(config.hostView); + return this.displayPopoverWithComponentRef({ ...config, componentRef }) + } else if (config instanceof Element) { + const componentRef = this.createPopoverRef(hostView); + return this.displayPopoverWithComponentRef(componentRef, config, renderer, componentType, preferredPlacement, hideOnClickOutside, + injector, context, overlayStyle, popoverStyle, style, showCloseButton, visibleFn, popoverContentStyle); + } else { + throw new Error("Invalid configuration provided for displayPopover"); + } } + displayPopoverWithComponentRef(config: DisplayPopoverWithComponentRefConfig): TbPopoverComponent; displayPopoverWithComponentRef(componentRef: ComponentRef, trigger: Element, renderer: Renderer2, - componentType: Type, preferredPlacement: PopoverPreferredPlacement = 'top', - hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: any = {}, - popoverStyle: any = {}, style?: any, showCloseButton = true, - visibleFn: (visible: boolean) => void = () => {}, + componentType: Type, preferredPlacement: PopoverPreferredPlacement, + hideOnClickOutside: boolean, injector?: Injector, context?: any, overlayStyle?: any, + popoverStyle?: any, style?: any, showCloseButton?: boolean, + visibleFn?: (visible: boolean) => void, popoverContentStyle?: any): TbPopoverComponent; + displayPopoverWithComponentRef(config: ComponentRef | DisplayPopoverWithComponentRefConfig, + trigger?: Element, renderer?: Renderer2, componentType?: Type, + preferredPlacement?: PopoverPreferredPlacement, hideOnClickOutside?: boolean, + injector?: Injector, context?: any, overlayStyle?: any, + popoverStyle?: any, style?: any, showCloseButton?: boolean, + visibleFn?: (visible: boolean) => void, popoverContentStyle: any = {}): TbPopoverComponent { - const component = componentRef.instance; + let popoverConfig: DisplayPopoverWithComponentRefConfig; + if (!(config instanceof ComponentRef) && 'trigger' in config && 'renderer' in config && 'componentType' in config) { + popoverConfig = config; + } else if(config instanceof ComponentRef) { + popoverConfig = { + componentRef: config, + trigger, + renderer, + componentType, + preferredPlacement, + hideOnClickOutside, + injector, + context, + overlayStyle, + popoverStyle, + style, + showCloseButton, + visibleFn, + popoverContentStyle + } + } else { + throw new Error("Invalid configuration provided for displayPopoverWithComponentRef"); + } + popoverConfig = mergeDeep({} as any, defaultPopoverConfig, popoverConfig); + return this._displayPopoverWithComponentRef(popoverConfig); + } + + + private _displayPopoverWithComponentRef(conf: DisplayPopoverWithComponentRefConfig): TbPopoverComponent { + const component = conf.componentRef.instance; this.popoverWithTriggers.push({ - trigger, + trigger: conf.trigger, popoverComponent: component }); - renderer.removeChild( - renderer.parentNode(trigger), - componentRef.location.nativeElement + conf.renderer.removeChild( + conf.renderer.parentNode(conf.trigger), + conf.componentRef.location.nativeElement ); - const originElementRef = new ElementRef(trigger); + const originElementRef = new ElementRef(conf.trigger); component.setOverlayOrigin(new CdkOverlayOrigin(originElementRef)); - component.tbPlacement = preferredPlacement; - component.tbComponent = componentType; - component.tbComponentInjector = injector; - component.tbComponentContext = context; - component.tbOverlayStyle = overlayStyle; - component.tbPopoverInnerStyle = popoverStyle; - component.tbPopoverInnerContentStyle = popoverContentStyle; - component.tbComponentStyle = style; - component.tbHideOnClickOutside = hideOnClickOutside; - component.tbShowCloseButton = showCloseButton; + component.tbPlacement = conf.preferredPlacement; + component.tbComponent = conf.componentType; + component.tbComponentInjector = conf.injector; + component.tbComponentContext = conf.context; + component.tbOverlayStyle = conf.overlayStyle; + component.tbModal = conf.isModal; + component.tbPopoverInnerStyle = conf.popoverStyle; + component.tbPopoverInnerContentStyle = conf.popoverContentStyle; + component.tbComponentStyle = conf.style; + component.tbHideOnClickOutside = conf.hideOnClickOutside; + component.tbShowCloseButton = conf.showCloseButton; component.tbVisibleChange.subscribe((visible: boolean) => { if (!visible) { - componentRef.destroy(); + conf.componentRef.destroy(); } }); component.tbDestroy.subscribe(() => { this.removePopoverByComponent(component); }); component.tbHideStart.subscribe(() => { - visibleFn(false); + conf.visibleFn(false); }); component.show(); - visibleFn(true); + conf.visibleFn(true); return component; } diff --git a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts index 68ec6bcdd9..1ad4dab88c 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts @@ -558,9 +558,13 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const aggregationConfigPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, AggregationOptionsConfigPanelComponent, ['left', 'leftTop', 'leftBottom'], true, null, - { + const aggregationConfigPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + hostView: this.viewContainerRef, + componentType: AggregationOptionsConfigPanelComponent, + preferredPlacement: ['left', 'leftTop', 'leftBottom'], + context: { allowedAggregationTypes: deepClone(this.timewindowForm.get('allowedAggTypes').value), onClose: (result: Array | null) => { aggregationConfigPopover.hide(); @@ -570,8 +574,10 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On } } }, - {maxHeight: '500px', height: '100%'}, - {}, {}, true, () => {}, {padding: 0}); + overlayStyle: {maxHeight: '500px', height: '100%'}, + popoverContentStyle: {padding: 0}, + isModal: true + }); aggregationConfigPopover.tbComponentRef.instance.popoverComponent = aggregationConfigPopover; } this.cd.detectChanges(); @@ -612,9 +618,13 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const intervalsConfigPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, IntervalOptionsConfigPanelComponent, ['left', 'leftTop', 'leftBottom'], true, null, - { + const intervalsConfigPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + hostView: this.viewContainerRef, + componentType: IntervalOptionsConfigPanelComponent, + preferredPlacement: ['left', 'leftTop', 'leftBottom'], + context: { aggregation: this.aggregation, allowedIntervals: deepClone(this.timewindowForm.get(allowedIntervalsControlName).value), aggIntervalsConfig: deepClone(this.timewindowForm.get(aggIntervalsConfigControlName).value), @@ -629,8 +639,10 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On } } }, - {maxHeight: '500px', height: '100%'}, - {}, {}, true, () => {}, {padding: 0}); + overlayStyle: {maxHeight: '500px', height: '100%'}, + popoverContentStyle: {padding: 0}, + isModal: true + }); intervalsConfigPopover.tbComponentRef.instance.popoverComponent = intervalsConfigPopover; } this.cd.detectChanges(); diff --git a/ui-ngx/src/app/shared/components/time/timezone.component.ts b/ui-ngx/src/app/shared/components/time/timezone.component.ts index 78a938f53f..34e2486106 100644 --- a/ui-ngx/src/app/shared/components/time/timezone.component.ts +++ b/ui-ngx/src/app/shared/components/time/timezone.component.ts @@ -155,9 +155,13 @@ export class TimezoneComponent implements ControlValueAccessor, OnInit { if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { - const timezoneSelectionPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, TimezonePanelComponent, ['bottomRight', 'leftBottom'], true, null, - { + const timezoneSelectionPopover = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + hostView: this.viewContainerRef, + componentType: TimezonePanelComponent, + preferredPlacement: ['bottomRight', 'leftBottom'], + context: { timezone: this.modelValue, userTimezoneByDefault: this.userTimezoneByDefaultValue, localBrowserTimezonePlaceholderOnEmpty: this.localBrowserTimezonePlaceholderOnEmptyValue, @@ -173,8 +177,9 @@ export class TimezoneComponent implements ControlValueAccessor, OnInit { } } }, - {}, - {}, {}, false); + showCloseButton: false, + isModal: true + }); timezoneSelectionPopover.tbComponentRef.instance.popoverComponent = timezoneSelectionPopover; } this.cd.detectChanges(); diff --git a/ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts b/ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts index f29ae083be..c988f5ca0a 100644 --- a/ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts +++ b/ui-ngx/src/app/shared/models/ace/tbel-utils.models.ts @@ -1245,6 +1245,56 @@ const tbelEditorCompletions:TbEditorCompletions = { type: 'boolean' } }, + isInsidePolygon: { + meta: 'function', + description: 'Checks if a given point is inside a polygon.', + args: [ + { + name: 'latitude', + description: 'The latitude of the point', + type: 'number' + }, + { + name: 'longitude', + description: 'The longitude of the point', + type: 'number' + }, + { + name: 'perimeter', + description: 'The polygon perimeter represented as a string', + type: 'string' + } + ], + return: { + description: 'True if the point is inside the polygon, false otherwise.', + type: 'boolean' + } + }, + isInsideCircle: { + meta: 'function', + description: 'Checks if a given point is inside a circular area.', + args: [ + { + name: 'latitude', + description: 'The latitude of the point', + type: 'number' + }, + { + name: 'longitude', + description: 'The longitude of the point', + type: 'number' + }, + { + name: 'perimeter', + description: 'A string representation of the circle, containing center coordinates and radius', + type: 'string' + } + ], + return: { + description: 'True if the point is inside the circle, false otherwise.', + type: 'boolean' + } + } } export const tbelUtilsAutocompletes = new TbEditorCompleter(tbelEditorCompletions); diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index 383f7c7f72..9cc9a62da8 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -144,6 +144,7 @@ export interface RefEntityId { export interface CalculatedFieldArgumentValue extends CalculatedFieldArgument { argumentName: string; + entityName?: string; } export type CalculatedFieldTestScriptFn = (calculatedField: CalculatedField, argumentsObj?: Record, closeAllOnSave?: boolean) => Observable; diff --git a/ui-ngx/src/app/shared/models/js-function.models.ts b/ui-ngx/src/app/shared/models/js-function.models.ts index 94c04efc8a..40b9175e10 100644 --- a/ui-ngx/src/app/shared/models/js-function.models.ts +++ b/ui-ngx/src/app/shared/models/js-function.models.ts @@ -141,7 +141,7 @@ export const loadModuleMarkdownSourceCode = (http: HttpClient, translate: Transl let sourceCode = `
${resource.title}
${translate.instant('js-func.source-code')}
\n\n`; return loadFunctionModuleSource(http, resource.link).pipe( map((source) => { - sourceCode += '```javascript\n{:code-style="margin-left: -16px; margin-right: -16px;"}\n' + source + '\n```'; + sourceCode += '```javascript\n{:code-style="margin-left: -16px; margin-right: -16px; max-height: 65vh;"}\n' + source + '\n```'; return sourceCode; }), catchError(err => { diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 2300ae2341..038c738bb0 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -185,6 +185,7 @@ export interface WidgetTypeParameters { previewWidth?: string; previewHeight?: string; embedTitlePanel?: boolean; + embedActionsPanel?: boolean; overflowVisible?: boolean; hideDataTab?: boolean; hideDataSettings?: boolean; diff --git a/ui-ngx/src/app/shared/models/widget/maps/map-export.models.ts b/ui-ngx/src/app/shared/models/widget/maps/map-export.models.ts index f7750db731..89d5beb963 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/map-export.models.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/map-export.models.ts @@ -18,14 +18,25 @@ import { EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models import { FilterInfo, Filters, getFilterId } from '@shared/models/query/query.models'; import { Dashboard } from '@shared/models/dashboard.models'; import { DatasourceType, Widget } from '@shared/models/widget.models'; -import { BaseMapSettings, MapDataSourceSettings, MapType } from '@shared/models/widget/maps/map.models'; +import { + BaseMapSettings, + MapDataLayerSettings, + MapDataSourceSettings, + MapType +} from '@shared/models/widget/maps/map.models'; import { WidgetExportDefinition } from '@shared/models/widget/widget-export.models'; -interface ExportDataSourceInfo { - aliases: {[dataLayerIndex: number]: EntityAliasInfo}; - filters: {[dataLayerIndex: number]: FilterInfo}; +interface AliasFilterPair { + alias?: EntityAliasInfo, + filter?: FilterInfo } +interface MapDataLayerDsInfo extends AliasFilterPair { + additionalDsInfo?: {[dsIndex: number]: AliasFilterPair} +} + +type ExportDataSourceInfo = {[dataLayerIndex: number]: MapDataLayerDsInfo}; + interface MapDatasourcesInfo { trips?: ExportDataSourceInfo; markers?: ExportDataSourceInfo; @@ -96,43 +107,67 @@ export const MapExportDefinition: WidgetExportDefinition = { }; const updateMapDatasourceFromExportInfo = (entityAliases: EntityAliases, - filters: Filters, settings: MapDataSourceSettings[], info: ExportDataSourceInfo): void => { - if (info.aliases) { - for (const dsIndexStr of Object.keys(info.aliases)) { - const dsIndex = Number(dsIndexStr); - if (settings[dsIndex] && settings[dsIndex].dsType === DatasourceType.entity) { - const aliasInfo = info.aliases[dsIndex]; - settings[dsIndex].dsEntityAliasId = getEntityAliasId(entityAliases, aliasInfo); + filters: Filters, settings: MapDataLayerSettings[] | MapDataSourceSettings[], + info: MapDataLayerDsInfo | {[dsIndex: number]: AliasFilterPair}): void => { + for (const dsIndexStr of Object.keys(info)) { + const dsIndex = Number(dsIndexStr); + const dsInfo = info[dsIndex]; + if (settings[dsIndex]) { + if (settings[dsIndex].dsType === DatasourceType.entity) { + if (settings[dsIndex].dsType === DatasourceType.entity) { + if (dsInfo.alias) { + settings[dsIndex].dsEntityAliasId = getEntityAliasId(entityAliases, dsInfo.alias); + } + if (dsInfo.filter) { + settings[dsIndex].dsFilterId = getFilterId(filters, dsInfo.filter); + } + } } - } - } - if (info.filters) { - for (const dsIndexStr of Object.keys(info.filters)) { - const dsIndex = Number(dsIndexStr); - if (settings[dsIndex] && settings[dsIndex].dsType === DatasourceType.entity) { - const filterInfo = info.filters[dsIndex]; - settings[dsIndex].dsFilterId = getFilterId(filters, filterInfo); + if (dsInfo.additionalDsInfo && (settings[dsIndex] as MapDataLayerSettings).additionalDataSources?.length) { + updateMapDatasourceFromExportInfo(entityAliases, + filters, (settings[dsIndex] as MapDataLayerSettings).additionalDataSources, dsInfo.additionalDsInfo); } } } } -const prepareExportDataSourcesInfo = (dashboard: Dashboard, settings: MapDataSourceSettings[]): ExportDataSourceInfo => { - const info: ExportDataSourceInfo = { - aliases: {}, - filters: {} - }; +const prepareExportDataSourcesInfo = (dashboard: Dashboard, settings: MapDataLayerSettings[] | MapDataSourceSettings[]): ExportDataSourceInfo => { + const info: ExportDataSourceInfo = {}; settings.forEach((dsSettings, index) => { prepareExportDataSourceInfo(dashboard, info, dsSettings, index); }); return info; } -const prepareExportDataSourceInfo = (dashboard: Dashboard, info: ExportDataSourceInfo, settings: MapDataSourceSettings, index: number): void => { +const prepareExportDataSourceInfo = (dashboard: Dashboard, info: ExportDataSourceInfo, settings: MapDataLayerSettings | MapDataSourceSettings, index: number): void => { + const dsInfo: MapDataLayerDsInfo = {}; + const aliasAndFilter = prepareAliasAndFilterPair(dashboard, settings); + if (aliasAndFilter) { + dsInfo.alias = aliasAndFilter.alias; + dsInfo.filter = aliasAndFilter.filter; + } + if ((settings as MapDataLayerSettings).additionalDataSources?.length && settings.dsType !== DatasourceType.function) { + (settings as MapDataLayerSettings).additionalDataSources.forEach((ds, index) => { + const dsAliasAndFilter = prepareAliasAndFilterPair(dashboard, ds); + if (dsAliasAndFilter) { + if (!dsInfo.additionalDsInfo) { + dsInfo.additionalDsInfo = {}; + } + dsInfo.additionalDsInfo[index] = dsAliasAndFilter; + } + }); + } + if (!!dsInfo.alias || !!dsInfo.filter || !!dsInfo.additionalDsInfo) { + info[index] = dsInfo; + } +} + +const prepareAliasAndFilterPair = (dashboard: Dashboard, settings: MapDataSourceSettings): AliasFilterPair => { + const aliasAndFilter: AliasFilterPair = {}; if (settings.dsType === DatasourceType.entity) { const entityAlias = dashboard.configuration.entityAliases[settings.dsEntityAliasId]; if (entityAlias) { - info.aliases[index] = { + aliasAndFilter.alias = { alias: entityAlias.alias, filter: entityAlias.filter }; @@ -140,7 +175,7 @@ const prepareExportDataSourceInfo = (dashboard: Dashboard, info: ExportDataSourc if (settings.dsFilterId && dashboard.configuration.filters) { const filter = dashboard.configuration.filters[settings.dsFilterId]; if (filter) { - info.filters[index] = { + aliasAndFilter.filter = { filter: filter.filter, keyFilters: filter.keyFilters, editable: filter.editable @@ -148,4 +183,9 @@ const prepareExportDataSourceInfo = (dashboard: Dashboard, info: ExportDataSourc } } } + if (!!aliasAndFilter.alias || !!aliasAndFilter.filter) { + return aliasAndFilter; + } else { + return null; + } } diff --git a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts index 1884e86215..bac8c8fb75 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts @@ -42,6 +42,7 @@ import { map } from 'rxjs/operators'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { MarkerIconContainer, MarkerShape } from '@shared/models/widget/maps/marker-shape.models'; import { ColorRange, DateFormatSettings, simpleDateFormat } from '@shared/models/widget-settings.models'; +import _ from 'lodash'; export enum MapType { geoMap = 'geoMap', @@ -60,7 +61,7 @@ export interface TbMapDatasource extends Datasource { mapDataIds: string[]; } -export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSettings): TbMapDatasource => { +export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSettings, id = guid()): TbMapDatasource => { return { type: settings.dsType, name: settings.dsLabel, @@ -68,7 +69,8 @@ export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSetting entityAliasId: settings.dsEntityAliasId, filterId: settings.dsFilterId, dataKeys: [], - mapDataIds: [guid()] + latestDataKeys: [], + mapDataIds: [id] }; }; @@ -132,6 +134,7 @@ export interface DataLayerEditSettings { } export interface MapDataLayerSettings extends MapDataSourceSettings { + additionalDataSources?: MapDataSourceSettings[]; additionalDataKeys?: DataKey[]; label: DataLayerPatternSettings; tooltip: DataLayerTooltipSettings; @@ -586,6 +589,34 @@ export const additionalMapDataSourcesToDatasources = (additionalMapDataSources: }); }; +export const mapDataSourceValid = (dataSource: MapDataSourceSettings): boolean => { + if (!dataSource.dsType || ![DatasourceType.device, DatasourceType.entity].includes(dataSource.dsType)) { + return false; + } + if (dataSource.dsType === DatasourceType.device && !dataSource.dsDeviceId) { + return false; + } + if (dataSource.dsType === DatasourceType.entity && !dataSource.dsEntityAliasId) { + return false; + } + return true; +}; + +export const mapDataSourceValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { + const dataSource: MapDataSourceSettings = control.value; + if (!mapDataSourceValid(dataSource)) { + return { + dataSource: true + }; + } + return null; +}; + +export const defaultMapDataSourceSettings = { + dsType: DatasourceType.entity, + dsLabel: '' +}; + export const additionalMapDataSourceValid = (dataSource: AdditionalMapDataSourceSettings): boolean => { if (!dataSource.dsType || ![DatasourceType.function, DatasourceType.device, DatasourceType.entity].includes(dataSource.dsType)) { return false; @@ -1151,6 +1182,21 @@ export const parseCenterPosition = (position: string | [number, number]): [numbe return [0, 0]; } +export const updateDataKeyToNewDsType = (dataKey: DataKey | null, newDsType: DatasourceType, timeSeries = false): boolean => { + if (newDsType === DatasourceType.function) { + if (dataKey && dataKey.type !== DataKeyType.function) { + dataKey.type = DataKeyType.function; + return true; + } + } else { + if (dataKey?.type === DataKeyType.function) { + dataKey.type = timeSeries ? DataKeyType.timeseries : DataKeyType.attribute; + return true; + } + } + return false; +} + export const mergeMapDatasources = (target: TbMapDatasource[], source: TbMapDatasource[]): TbMapDatasource[] => { const appendDatasources: TbMapDatasource[] = []; for (const sourceDs of source) { @@ -1191,7 +1237,7 @@ const mapDatasourceIsSame = (ds1: TbMapDatasource, ds2: TbMapDatasource): boolea } const mergeMapDatasource = (target: TbMapDatasource, source: TbMapDatasource): TbMapDatasource => { - target.mapDataIds.push(...source.mapDataIds); + target.mapDataIds = _.union(target.mapDataIds, source.mapDataIds); const appendKeys: DataKey[] = []; for (const sourceKey of source.dataKeys) { const found = diff --git a/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts b/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts index f0783a4ac9..76fe239264 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/marker-shape.models.ts @@ -50,6 +50,10 @@ export enum MarkerIconContainer { iconContainer1 = 'iconContainer1', iconContainer2 = 'iconContainer2', iconContainer3 = 'iconContainer3', + iconContainer4 = 'iconContainer4', + iconContainer5 = 'iconContainer5', + iconContainer6 = 'iconContainer6', + iconContainer7 = 'iconContainer7', tripIconContainer1 = 'tripIconContainer1', tripIconContainer2 = 'tripIconContainer2', tripIconContainer3 = 'tripIconContainer3' @@ -85,6 +89,10 @@ const markerIconContainerMap = new Map( [MarkerIconContainer.iconContainer1, '/assets/markers/iconContainer1.svg'], [MarkerIconContainer.iconContainer2, '/assets/markers/iconContainer2.svg'], [MarkerIconContainer.iconContainer3, '/assets/markers/iconContainer3.svg'], + [MarkerIconContainer.iconContainer4, '/assets/markers/iconContainer4.svg'], + [MarkerIconContainer.iconContainer5, '/assets/markers/iconContainer5.svg'], + [MarkerIconContainer.iconContainer6, '/assets/markers/iconContainer6.svg'], + [MarkerIconContainer.iconContainer7, '/assets/markers/iconContainer7.svg'], [MarkerIconContainer.tripIconContainer1, '/assets/markers/tripIconContainer1.svg'], [MarkerIconContainer.tripIconContainer2, '/assets/markers/tripIconContainer2.svg'], [MarkerIconContainer.tripIconContainer3, '/assets/markers/tripIconContainer3.svg'] @@ -106,11 +114,11 @@ const emptyIconContainerDefinition: MarkerIconContainerDefinition = { const defaultIconContainerDefinition: MarkerIconContainerDefinition = { iconSize: 12, - iconColor: (color) => tinycolor.mix(color.clone().setAlpha(1), tinycolor('rgba(0,0,0,0.38)')), + iconColor: (color) => color, iconAlpha: color => color.getAlpha() } -const defaultTripIconContainerDefinition: MarkerIconContainerDefinition = { +const defaultMaskIconContainerDefinition: MarkerIconContainerDefinition = { iconSize: 24, iconColor: () => tinycolor('#000'), iconAlpha: () => 1, @@ -142,9 +150,13 @@ const markerIconContainerDefinitionMap = new Map { diff --git a/ui-ngx/src/assets/help/en_US/widget/action/custom_additional_params.md b/ui-ngx/src/assets/help/en_US/widget/action/custom_additional_params.md index 7d5aa04336..704e25b7f1 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/custom_additional_params.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/custom_additional_params.md @@ -31,7 +31,14 @@ An optional key/value object holding additional entity parameters depending on w presenting formattedTs (a string value of formatted timestamp) and
timeseries values for each column declared in widget datasource configuration. - + +
  • Entities hierarchy widget (On node selected) - additionalParams: { nodeCtx: HierarchyNodeContext }:
    • nodeCtx: HierarchyNodeContext - An diff --git a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_html.md b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_html.md new file mode 100644 index 0000000000..c3a58747e8 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_html.md @@ -0,0 +1,87 @@ +#### HTML template of dialog to create a device or an asset + +```html +{:code-style="max-height: 400px;"} +
      + +

      Add entity

      + + +
      + + +
      +
      +
      + + Entity Name + + + Entity name is required. + + + + Entity Label + + +
      +
      + + + +
      +
      +
      + + Address + + + + Owner + + +
      +
      +
      +
      + + +
      +
      +{:copy-code} +``` + +
      +
      diff --git a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md new file mode 100644 index 0000000000..bc8823c777 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md @@ -0,0 +1,94 @@ +#### Function displaying dialog to create a device or an asset + +```javascript +{:code-style="max-height: 400px;"} +let $injector = widgetContext.$scope.$injector; +let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); +let assetService = $injector.get(widgetContext.servicesMap.get('assetService')); +let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); +let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService')); + +openAddEntityDialog(); + +function openAddEntityDialog() { + customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe(); +} + +function AddEntityDialogController(instance) { + let vm = instance; + + vm.allowedEntityTypes = ['ASSET', 'DEVICE']; + + vm.addEntityFormGroup = vm.fb.group({ + entityName: ['', [vm.validators.required]], + entityType: ['DEVICE'], + entityLabel: [null], + type: ['', [vm.validators.required]], + attributes: vm.fb.group({ + address: [null], + owner: [null] + }) + }); + + vm.cancel = function() { + vm.dialogRef.close(null); + }; + + vm.save = function() { + vm.addEntityFormGroup.markAsPristine(); + saveEntityObservable().pipe( + widgetContext.rxjs.switchMap((entity) => saveAttributes(entity.id)) + ).subscribe(() => { + widgetContext.updateAliases(); + vm.dialogRef.close(null); + }); + }; + + function saveEntityObservable() { + const formValues = vm.addEntityFormGroup.value; + let entity = { + name: formValues.entityName, + type: formValues.type, + label: formValues.entityLabel + }; + if (formValues.entityType == 'ASSET') { + return assetService.saveAsset(entity); + } else if (formValues.entityType == 'DEVICE') { + return deviceService.saveDevice(entity); + } + } + + function saveAttributes(entityId) { + let attributes = vm.addEntityFormGroup.get('attributes').value; + let attributesArray = getMapItemLocationAttributes(); + for (let key in attributes) { + if(attributes[key] !== null) { + attributesArray.push({key: key, value: attributes[key]}); + } + } + if (attributesArray.length > 0) { + return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray); + } + return widgetContext.rxjs.of([]); + } + + function getMapItemLocationAttributes() { + const attributes = []; + const mapItemType = $event.shape; + if (mapItemType === 'Marker') { + const mapType = widgetContext.mapInstance.type(); + attributes.push({key: mapType === 'image' ? 'xPos' : 'latitude', value: additionalParams.coordinates.x}); + attributes.push({key: mapType === 'image' ? 'yPos' : 'longitude', value: additionalParams.coordinates.y}); + } else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon') { + attributes.push({key: 'perimeter', value: additionalParams.coordinates}); + } else if (mapItemType === 'Circle') { + attributes.push({key: 'circle', value: additionalParams.coordinates}); + } + return attributes; + } +} +{:copy-code} +``` + +
      +
      diff --git a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md new file mode 100644 index 0000000000..131a674099 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md @@ -0,0 +1,66 @@ +#### Place map item function + +
      +
      + +*function ($event, widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel): void* + +A JavaScript function triggered after a map item is placed. Optionally uses an HTML template to render dialog. + +**Parameters:** + +
        +
      • $event: {shape: PM.SUPPORTED_SHAPES; layer: L.Layer} - Event payload containing the created shape type and its associated map layer. +
      • +
      • widgetContext: WidgetContext - A reference to WidgetContext that has all necessary API + and data used by widget instance. +
      • +
      • entityId: string - An optional string id of the target entity. +
      • +
      • entityName: string - An optional string name of the target entity. +
      • +
      • htmlTemplate: string - An optional HTML template string defined in HTML tab.
        Used to render custom dialog (see Examples for more details). +
      • +
      • additionalParams: {coordinates: Coordinates; layer: L.Layer}: +
          +
        • coordinates: Coordinates - Represents geographical coordinates of the placed map item. The actual format of this parameter depends on the type of the selected map item: +
            +
          • Marker: {x: number; y: number}, where x represents latitude, and y represents longitude.
          • +
          • Polygon, Rectangle: TbPolygonRawCoordinates contains an array of points defining the shape boundaries.
          • +
          • Circle: TbCircleData contains center coordinates and radius information.
          • +
          + Note: The coordinates will be automatically converted according to the selected map type. +
        • +
        • layer: L.Layer - The Leaflet map layer instance (e.g., marker, polygon, circle) associated with the placed map item. This object provides access to layer properties and methods defined in Leaflet's API. +
        • +
        +
      • +
      • entityLabel: string - An optional string label of the target entity. +
      • +
      + +
      + +##### Examples + +###### Display dialog to create a device or an asset + +
      + +
      +
      + +
      + +
      +
      diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/gateway/rest-json_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/rest-json_fn.md new file mode 100644 index 0000000000..32143694df --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/rest-json_fn.md @@ -0,0 +1,50 @@ +### Expressions +#### JSON Path + +The expression field is used to specify the path for extracting data from an incoming HTTP request body. + +JSONPath expressions specifies the items within a JSON structure (which could be an object, array, or nested combination of both) that you want to access. +These expressions can select elements from JSON data on specific criteria. +Basic overview of how JSONPath expressions are structured: + +- `$`: The root element of the JSON document; +- `.`: Child operator used to select child elements. For example, $.store.book ; +- `[]`: Child operator used to select child elements. $['store']['book'] accesses the book array within a store object; + +#### Examples + +Incoming request body: + +```json +{ + "sensorModelInfo": { + "sensorName": "AM-123", + "sensorType": "myDeviceType" + }, + "data": { + "temp": 12.2, + "hum": 56, + "status": "ok" + }, + "rxInfo": [ + { + "rssi": 50, + "snr": 3 + }, + { + "rssi": 22, + "snr": 1 + } + ] +} +``` +
      + +Examples below shows on how to extract values from incoming request body. + +| JSON Path Expression | Extracted data | +|---------------------------------|---------------------------------------------| +| `${sensorModelInfo.sensorName}` | `AM-123` | +| `${data}` | `{"temp": 12.2, "hum": 56, "status": "ok"}` | +| `${$.data.temp}` | `12.2` | +| `${$.rxInfo[0].rssi}` | `50` | 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 5a0c401aa6..7af62838cf 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1000,8 +1000,8 @@ "calculated-field": "calculated field", "hint": { "main-limited": "No more than {{msg}} {{entity}} debug messages per {{time}} will be recorded.", - "on-failure": "Log all debug messages.", - "all-messages": "Log error messages only. " + "on-failure": "Log error messages only.", + "all-messages": "Log all debug messages." } }, "calculated-fields": { @@ -6668,6 +6668,7 @@ "units-short": "Units", "decimals-short": "Decimals", "decimals-suffix": "decimals", + "digits-suffix": "digits", "timewindow": "Time window", "use-dashboard-timewindow": "Use dashboard time window", "use-widget-timewindow": "Use widget time window", @@ -7975,6 +7976,7 @@ }, "overlays": { "overlays": "Overlays", + "overlays-hint": "Configure datasources, appearance, behavior, editing options, and grouping for map entities", "trips": "Trips", "markers": "Markers", "polygons": "Polygons", @@ -7982,8 +7984,11 @@ }, "data-layer": { "source": "Source", + "filter": "Filter", "additional-data-keys": "Additional data keys", "additional-datasources": "Additional datasources", + "additional-datasources-hint": "Datasource for accessing attributes or telemetry from entities not displayed on the map, usable in map overlay functions.", + "more-datasources": "More datasources", "data-keys": "Data keys", "add-datasource": "Add datasource", "no-datasources": "No datasources configured", @@ -7992,6 +7997,7 @@ "on-click": "On click", "on-click-hint": "Action invoked when user clicks on the map item.", "groups": "Groups", + "groups-hint": "List of group names assigned to the overlay, used to toggle its visibility on the map.", "color": "Color", "fill-color": "Fill color", "stroke": "Stroke", @@ -8009,7 +8015,7 @@ "pattern-type-function": "Function", "label-pattern": "Label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )", "label-function": "Label function", - "tooltip-pattern": "Tooltip (for ex. 'Text ${keyName} units.' or Link text')", + "tooltip-pattern": "Tooltip (for ex. 'Text ${keyName} units.' or Link text)", "tooltip-function": "Tooltip function", "tooltip-trigger": "Tooltip trigger", "tooltip-trigger-click": "Show tooltip on click", @@ -8029,6 +8035,7 @@ "edit-instruments": "Instruments", "persist-location-attribute-scope": "Scope of the attribute to persist location", "enable-snapping": "Enable snapping to other vertices for precision drawing", + "enable-snapping-hint": "Automatically aligns new points with existing shapes to make drawing easier and more accurate.", "drag-drop-mode": "Drag-drop mode", "trip": { "no-trips": "No trips configured", diff --git a/ui-ngx/src/assets/markers/iconContainer4.svg b/ui-ngx/src/assets/markers/iconContainer4.svg new file mode 100644 index 0000000000..15a0f3b219 --- /dev/null +++ b/ui-ngx/src/assets/markers/iconContainer4.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/iconContainer5.svg b/ui-ngx/src/assets/markers/iconContainer5.svg new file mode 100644 index 0000000000..62d8a9b363 --- /dev/null +++ b/ui-ngx/src/assets/markers/iconContainer5.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/iconContainer6.svg b/ui-ngx/src/assets/markers/iconContainer6.svg new file mode 100644 index 0000000000..749ee54963 --- /dev/null +++ b/ui-ngx/src/assets/markers/iconContainer6.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/markers/iconContainer7.svg b/ui-ngx/src/assets/markers/iconContainer7.svg new file mode 100644 index 0000000000..a383907203 --- /dev/null +++ b/ui-ngx/src/assets/markers/iconContainer7.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index 8f3c7f0cd0..e516810895 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -360,9 +360,10 @@ } } .tb-small-label { + display: flex; + align-items: center; height: 40px; font-size: 14px; - line-height: 40px; letter-spacing: 0.2px; color: rgba(0, 0, 0, 0.38); } diff --git a/ui-ngx/tailwind.config.js b/ui-ngx/tailwind.config.js index e84ba47abc..d675da4cda 100644 --- a/ui-ngx/tailwind.config.js +++ b/ui-ngx/tailwind.config.js @@ -102,6 +102,7 @@ module.exports = { minWidth: { '7.5': '1.875rem', '25': '6.25rem', + '30': '7.5rem', '37.5': '9.375rem', '62.5': '15.625rem', '72.5': '18.125rem',
  • Map widgets (On marker/polygon/circle click or Tag action) - additionalParams: FormattedData: +
      +
    • additionalParams: FormattedData - An object associated with a data layer (markers, polygons, circles) or with a specific data point of a route (for trips data layers).
      + It contains basic entity properties (ex. entityId, entityName) and provides access to additional attributes and timeseries defined in datasource of the data layer configuration. +
    • +
    +