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/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/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 93214e4403..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 @@ -422,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/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/DefaultEdqsService.java b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java index 7d5a0cb0fd..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,7 +95,7 @@ public class DefaultEdqsService implements EdqsService { private void init() { executor = ThingsBoardExecutors.newWorkStealingPool(12, getClass()); eventsProducer = EdqsProducer.builder() - .producer(queueFactory.createEdqsMsgProducer(EdqsQueue.EVENTS)) + .producer(queueFactory.createEdqsEventsProducer()) .partitionService(edqsPartitionService) .build(); syncLock = distributedLockService.getLock("edqs_sync"); @@ -148,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 ad7b7b970d..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 @@ -19,7 +19,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; 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.TbKafkaSettings; @@ -37,7 +36,7 @@ public class KafkaEdqsSyncService extends EdqsSyncService { TbKafkaAdmin kafkaAdmin = new TbKafkaAdmin(kafkaSettings, Collections.emptyMap()); this.syncNeeded = kafkaAdmin.areAllTopicsEmpty(IntStream.range(0, edqsConfig.getPartitions()) .mapToObj(partition -> TopicPartitionInfo.builder() - .topic(EdqsQueue.EVENTS.getTopic()) + .topic(edqsConfig.getEventsTopic()) .partition(partition) .build().getFullTopicName()) .collect(Collectors.toSet())); 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 bce1992932..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 @@ -54,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.AbstractConsumerPartitionedService; +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; @@ -71,7 +71,7 @@ import java.util.stream.Collectors; @Service @TbRuleEngineComponent @Slf4j -public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerPartitionedService implements TbCalculatedFieldConsumerService { +public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBasedConsumerService implements TbCalculatedFieldConsumerService { @Value("${queue.calculated_fields.poll_interval:25}") private long pollInterval; @@ -99,7 +99,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerPar } @Override - protected void doAfterStartUp() { + protected void onStartUp() { var queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME); PartitionedQueueConsumerManager> eventConsumer = PartitionedQueueConsumerManager.>create() .queueKey(queueKey) @@ -126,7 +126,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerPar } @Override - protected void processPartitionChangeEvent(PartitionChangeEvent event) { + protected void onPartitionChangeEvent(PartitionChangeEvent event) { try { event.getNewPartitions().forEach((queueKey, partitions) -> { if (queueKey.getQueueName().equals(DataConstants.CF_QUEUE_NAME)) { @@ -143,11 +143,6 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerPar } } - @Override - protected String getPrefix() { - return "tb-cf"; - } - private void processMsgs(List> msgs, TbQueueConsumer> consumer, QueueConfig config) throws Exception { List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); ConcurrentMap> pendingMap = orderedMsgList.stream().collect( @@ -195,6 +190,11 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerPar return ServiceType.TB_RULE_ENGINE; } + @Override + protected String getPrefix() { + return "tb-cf"; + } + @Override protected long getNotificationPollDuration() { return pollInterval; 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 3a9be3874a..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 @@ -49,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.AbstractConsumerPartitionedService; +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; @@ -66,7 +66,7 @@ import java.util.stream.Collectors; @Service @TbRuleEngineComponent @Slf4j -public class DefaultTbRuleEngineConsumerService extends AbstractConsumerPartitionedService implements TbRuleEngineConsumerService { +public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedConsumerService implements TbRuleEngineConsumerService { private final TbRuleEngineConsumerContext ctx; private final QueueService queueService; @@ -93,7 +93,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerPartitio } @Override - protected void doAfterStartUp() { + protected void onStartUp() { List queues = queueService.findAllQueues(); for (Queue configuration : queues) { if (partitionService.isManagedByCurrentService(configuration.getTenantId())) { @@ -104,7 +104,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerPartitio } @Override - protected void processPartitionChangeEvent(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; @@ -136,11 +136,6 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerPartitio }); } - @Override - protected String getPrefix() { - return "tb-rule-engine"; - } - @Override protected void stopConsumers() { super.stopConsumers(); @@ -153,6 +148,11 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerPartitio 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/AbstractConsumerPartitionedService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java similarity index 62% rename from application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerPartitionedService.java rename to application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java index 94b5390be1..97aa81d41c 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerPartitionedService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java @@ -31,24 +31,22 @@ import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsServ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -public abstract class AbstractConsumerPartitionedService extends AbstractConsumerService { +public abstract class AbstractPartitionBasedConsumerService extends AbstractConsumerService { - private final Lock startupLock; - private volatile boolean consumersInitialized; + private final Lock startupLock = new ReentrantLock(); + private volatile boolean started = false; private PartitionChangeEvent lastPartitionChangeEvent; - public AbstractConsumerPartitionedService(ActorSystemContext actorContext, - TbTenantProfileCache tenantProfileCache, - TbDeviceProfileCache deviceProfileCache, - TbAssetProfileCache assetProfileCache, - CalculatedFieldCache calculatedFieldCache, - TbApiUsageStateService apiUsageStateService, - PartitionService partitionService, - ApplicationEventPublisher eventPublisher, - JwtSettingsService jwtSettingsService) { + 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); - this.startupLock = new ReentrantLock(); - this.consumersInitialized = false; } @PostConstruct @@ -57,13 +55,14 @@ public abstract class AbstractConsumerPartitionedService 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 fb15c3fc73..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) @@ -165,7 +164,7 @@ public class EdqsProcessor implements TbQueueHandler, try { Set newPartitions = event.getNewPartitions().get(new QueueKey(ServiceType.EDQS)); - stateService.process(withTopic(newPartitions, EdqsQueue.STATE.getTopic())); + stateService.process(withTopic(newPartitions, config.getStateTopic())); // eventsConsumer's partitions are updated by stateService responseTemplate.subscribe(withTopic(newPartitions, config.getRequestsTopic())); // TODO: we subscribe to partitions before we are ready. implement consumer-per-partition version for request template @@ -235,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(); @@ -252,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/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/KafkaEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java index c59707c9c3..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,12 +36,14 @@ 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; @@ -56,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; @@ -73,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()); } @@ -91,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()) @@ -137,15 +139,35 @@ 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() - .producer(queueFactory.createEdqsMsgProducer(EdqsQueue.STATE)) + .producer(queueFactory.createEdqsStateProducer()) .partitionService(partitionService) .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 @@ -153,7 +175,7 @@ public class KafkaEdqsStateService implements EdqsStateService { if (queueStateService.getPartitions().isEmpty()) { Set allPartitions = IntStream.range(0, config.getPartitions()) .mapToObj(partition -> TopicPartitionInfo.builder() - .topic(EdqsQueue.EVENTS.getTopic()) + .topic(config.getEventsTopic()) .partition(partition) .build()) .collect(Collectors.toSet()); 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/consumer/MainQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java index 14394bbbe9..ef9728344c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java @@ -26,6 +26,7 @@ import org.thingsboard.server.queue.TbQueueMsg; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.UpdateConfigTask; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.UpdatePartitionsTask; import org.thingsboard.server.queue.discovery.QueueKey; +import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; import java.util.Collection; import java.util.Collections; @@ -43,6 +44,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.function.BiFunction; import java.util.function.Consumer; +import java.util.function.Function; @Slf4j public class MainQueueConsumerManager { @@ -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/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 a322cc5434..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,55 +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()) - .defaultTopic(topicService.buildTopicName(queue.getTopic())) + .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()) @@ -129,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 3496aac76a..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,6 +188,11 @@ 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)); } 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 bd8b4bd4f8..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; @@ -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 c50344a23f..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; @@ -590,10 +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()) - .defaultTopic(topicService.buildTopicName(queue.getTopic())) + .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 2a1dc4171f..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,10 +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()) - .defaultTopic(topicService.buildTopicName(queue.getTopic())) + .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 2e342b0dd2..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()); @@ -385,10 +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()) - .defaultTopic(topicService.buildTopicName(queue.getTopic())) + .clientId("edqs-events-producer-" + serviceInfoProvider.getServiceId()) + .defaultTopic(topicService.buildTopicName(edqsConfig.getEventsTopic())) .settings(kafkaSettings) .admin(edqsEventsAdmin) .build(); diff --git a/edqs/src/main/resources/edqs.yml b/edqs/src/main/resources/edqs.yml index c101eff68e..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 @@ -149,7 +153,7 @@ queue: # value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) topic-properties: # Kafka properties for EDQS events topics - edqs-events: "${TB_QUEUE_KAFKA_EDQS_EVENTS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:-1;partitions:1;min.insync.replicas:1}" + 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) 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/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/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 2b4b52ccfb..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 @@ -793,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/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/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts index 63d77c48b2..5dcb80ec41 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -41,7 +41,7 @@ import L from 'leaflet'; import { CompiledTbFunction } from '@shared/models/js-function.models'; import { forkJoin, Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; -import { DataKey, FormattedData } from '@shared/models/widget.models'; +import { DataKey, DatasourceType, FormattedData } from '@shared/models/widget.models'; import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import { TbMap } from '@home/components/widget/lib/maps/map'; import { WidgetContext } from '@home/models/widget-component.models'; @@ -170,7 +170,7 @@ export abstract class TbMapDataLayer { - 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 3696435683..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 { @@ -628,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 72f075e8f1..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 @@ -308,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 { @@ -349,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, @@ -926,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; 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/settings/chart/time-series-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts index dc56c2a328..68d849fd0c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts @@ -105,6 +105,17 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon const params = widgetConfig.typeParameters as any; if (isDefinedAndNotNull(params.chartType)) { this.chartType = params.chartType; + } else { + this.chartType = TimeSeriesChartType.default; + } + if (this.timeSeriesChartWidgetSettingsForm) { + const isStateChartType = this.chartType === TimeSeriesChartType.state; + const hasStatesControl = this.timeSeriesChartWidgetSettingsForm.contains('states'); + if (isStateChartType && !hasStatesControl) { + this.timeSeriesChartWidgetSettingsForm.addControl('states', this.fb.control(widgetConfig.config.settings.states), { emitEvent: false }); + } else if (!isStateChartType && hasStatesControl) { + this.timeSeriesChartWidgetSettingsForm.removeControl('states', { emitEvent: false }); + } } } 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 a2a0e8c8fa..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 }} 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 bd0fe36910..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, @@ -102,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; @@ -191,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 { 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..0957d991f5 --- /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,207 @@ +/// +/// 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 } 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 = this.updateDataKeyToNewDsType(key, newDsType) || updateModel; + } + if (updateModel) { + this.dataSourceFormGroup.get('dataKeys').patchValue(dataKeys, {emitEvent: false}); + } + } + this.updateValidators(); + if (updateModel) { + this.updateModel(); + } + } + + private updateDataKeyToNewDsType(dataKey: DataKey, newDsType: DatasourceType): boolean { + if (newDsType === DatasourceType.function) { + if (dataKey.type !== DataKeyType.function) { + dataKey.type = DataKeyType.function; + return true; + } + } else { + if (dataKey.type === DataKeyType.function) { + dataKey.type = DataKeyType.attribute; + return true; + } + } + return false; + } + + 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 281f20ccd4..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 @@
-
+ + + + widgets.maps.data-layer.more-datasources + + + + + + +
{{ 'datakey.keys' | 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 ae752ebca7..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 @@ -180,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 a15da83a28..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; @@ -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/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/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 44a802ec78..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; @@ -1206,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/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 5ee24c7c51..7af62838cf 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7984,9 +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",