diff --git a/application/pom.xml b/application/pom.xml index 6ce77646f8..7960db3374 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC thingsboard application diff --git a/application/src/main/data/json/demo/dashboards/thermostats.json b/application/src/main/data/json/demo/dashboards/thermostats.json index 43a11a8709..40866db9ef 100644 --- a/application/src/main/data/json/demo/dashboards/thermostats.json +++ b/application/src/main/data/json/demo/dashboards/thermostats.json @@ -17,7 +17,10 @@ "realtimeType": 1, "interval": 1000, "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY" + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false }, "history": { "historyType": 0, @@ -27,7 +30,11 @@ "startTimeMs": 1694085177686, "endTimeMs": 1694171577686 }, - "quickInterval": "CURRENT_DAY" + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", @@ -407,7 +414,10 @@ "realtimeType": 1, "interval": 1000, "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false }, "history": { "historyType": 0, @@ -417,7 +427,11 @@ "startTimeMs": 1694085177686, "endTimeMs": 1694171577686 }, - "quickInterval": "CURRENT_DAY" + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "AVG", @@ -460,595 +474,132 @@ "id": "c4631f94-2db3-523b-4d09-2a1a0a75d93f", "typeFullFqn": "system.input_widgets.update_multiple_attributes" }, - "3da9a9a1-0b9a-2e1f-0dcb-0ff34a695abb": { - "type": "latest", - "sizeX": 13, - "sizeY": 6, + "eda8a397-0959-690c-405c-11e2c9b2bc7e": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, "config": { "datasources": [ { "type": "entity", + "name": "", + "entityAliasId": "12ae98c7-1ea2-52cf-64d5-763e9d993547", "dataKeys": [ { "name": "temperature", "type": "timeseries", - "label": "temperature", - "color": "#2196f3", - "settings": {}, - "_hash": 0.1371919646686739, + "label": "Temperature", + "color": "#EF5350", + "settings": { + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "line", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": true, + "lineType": "solid", + "lineWidth": 2.5, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "circle", + "pointSize": 14, + "fillAreaSettings": { + "type": "gradient", + "opacity": 0.4, + "gradient": { + "start": 60, + "end": 10 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "_hash": 0.5973804076994531, + "units": "°C", "decimals": 1, - "postFuncBody": "return value || \"\";" - }, - { - "name": "humidity", - "type": "timeseries", - "label": "humidity", - "color": "#4caf50", - "settings": {}, - "_hash": 0.043177186765847475, - "decimals": 0, - "postFuncBody": "return value || \"\";" - }, - { - "name": "longitude", - "type": "attribute", - "label": "longitude", - "color": "#f44336", - "settings": {}, - "_hash": 0.5548964320315584 - }, - { - "name": "latitude", - "type": "attribute", - "label": "latitude", - "color": "#ffc107", - "settings": {}, - "_hash": 0.1803778014971602 - }, - { - "name": "active", - "type": "attribute", - "label": "active", - "color": "#607d8b", - "settings": {}, - "_hash": 0.30926987994082844 + "aggregationType": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null } ], - "entityAliasId": "68a058e1-fdda-8482-715b-3ae4a488568e" - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1694085177686, - "endTimeMs": 1694171577686 + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "8px", - "settings": { - "fitMapBounds": true, - "latKeyName": "latitude", - "lngKeyName": "longitude", - "showLabel": true, - "label": "${entityName}", - "tooltipPattern": "${entityName}

Temperature: ${temperature:1} °C
Humidity: ${humidity:0} %

Thermostat details
", - "markerImageSize": 48, - "useColorFunction": false, - "markerImages": [ - "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_0.svg", - "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_1.svg" - ], - "useMarkerImageFunction": true, - "colorFunction": "\n", - "color": "#fe7569", - "mapProvider": "OpenStreetMap.HOT", - "showTooltip": true, - "autocloseTooltip": true, - "customProviderTileUrl": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", - "defaultCenterPosition": [ - 0, - 0 - ], - "showTooltipAction": "click", - "polygonKeyName": "coordinates", - "polygonOpacity": 0.5, - "polygonStrokeOpacity": 1, - "polygonStrokeWeight": 1, - "zoomOnClick": true, - "showCoverageOnHover": true, - "animate": true, - "maxClusterRadius": 80, - "removeOutsideVisibleBounds": true, - "useLabelFunction": true, - "labelFunction": "var color;\nif(dsData[dsIndex].active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '' + \n '${entityLabel}' + \n ''", - "defaultZoomLevel": 14, - "markerImageFunction": "var res;\nif(dsData[dsIndex].active !== \"true\"){\n\tvar res = {\n\t url: images[0],\n\t size: 48\n\t}\n} else {\n var res = {\n\t url: images[1],\n\t size: 48\n\t}\n}\nreturn res;" - }, - "title": "Thermostat maps", - "dropShadow": true, - "enableFullscreen": false, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "useDashboardTimewindow": true, - "showLegend": false, - "widgetStyle": {}, - "actions": { - "headerButton": [], - "tooltipAction": [ - { - "id": "bef25673-b37a-8821-bc0f-5d6dd3680f24", - "name": "navigate_to_details", - "icon": "more_horiz", - "type": "openDashboardState", - "targetDashboardStateId": "chart", - "setEntityId": true - } - ] - }, - "showTitleIcon": false, - "titleIcon": null, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "displayTimewindow": true - }, - "id": "3da9a9a1-0b9a-2e1f-0dcb-0ff34a695abb", - "typeFullFqn": "system.maps_v2.openstreetmap" - }, - "00fb2742-ba1f-7e43-673f-d6c08b72ed06": { - "type": "latest", - "sizeX": 24, - "sizeY": 12, - "config": { - "datasources": [ - { - "type": "entity", - "dataKeys": [ - { - "name": "longitude", - "type": "attribute", - "label": "longitude", - "color": "#2196f3", - "settings": {}, - "_hash": 0.3640193654284214 - }, + "latestDataKeys": [ { - "name": "latitude", + "name": "temperatureAlarmThreshold", "type": "attribute", - "label": "latitude", + "label": "temperatureAlarmThreshold", "color": "#4caf50", - "settings": {}, - "_hash": 0.49020393887695923 - }, - { - "name": "temperature", - "type": "timeseries", - "label": "temperature", - "color": "#f44336", - "settings": {}, - "_hash": 0.5885892766009955, - "postFuncBody": "return value || \"\";" - }, - { - "name": "humidity", - "type": "timeseries", - "label": "humidity", - "color": "#ffc107", - "settings": {}, - "_hash": 0.21077893588180707, - "postFuncBody": "return value || \"\";" - }, - { - "name": "active", - "type": "attribute", - "label": "active", - "color": "#607d8b", - "settings": {}, - "_hash": 0.34722983638504346 + "settings": { + "__thresholdKey": true + }, + "_hash": 0.7120450032526351 } - ], - "entityAliasId": "68a058e1-fdda-8482-715b-3ae4a488568e" + ] } ], "timewindow": { - "displayValue": "", + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 0, "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1694085177686, - "endTimeMs": 1694171577686 - }, - "quickInterval": "CURRENT_DAY" + "realtimeType": 0, + "timewindowMs": 3600000, + "quickInterval": "CURRENT_DAY", + "interval": 30000 }, "aggregation": { "type": "AVG", "limit": 25000 - } + }, + "timezone": null }, - "showTitle": false, - "backgroundColor": "#fff", + "showTitle": true, + "backgroundColor": "rgba(0, 0, 0, 0)", "color": "rgba(0, 0, 0, 0.87)", - "padding": "8px", - "settings": { - "fitMapBounds": true, - "latKeyName": "latitude", - "lngKeyName": "longitude", - "showLabel": true, - "label": "${entityName}", - "tooltipPattern": "${entityName}

Temperature: ${temperature:1} °C
Humidity: ${humidity:0} %

Delete", - "markerImageSize": 34, - "useColorFunction": false, - "markerImages": [ - "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_0.svg", - "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_1.svg" - ], - "useMarkerImageFunction": true, - "color": "#fe7569", - "mapProvider": "OpenStreetMap.HOT", - "showTooltip": true, - "autocloseTooltip": true, - "defaultCenterPosition": "0,0", - "customProviderTileUrl": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", - "showTooltipAction": "click", - "polygonKeyName": "coordinates", - "polygonOpacity": 0.5, - "polygonStrokeOpacity": 1, - "polygonStrokeWeight": 1, - "zoomOnClick": true, - "showCoverageOnHover": true, - "animate": true, - "maxClusterRadius": 80, - "removeOutsideVisibleBounds": true, - "defaultZoomLevel": 12, - "labelFunction": "var color;\nif(dsData[dsIndex].active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '' + \n '${entityLabel}' + \n ''", - "markerImageFunction": "var res;\nif(dsData[dsIndex].active !== \"true\"){\n\tvar res = {\n\t url: images[0],\n\t size: 48\n\t}\n} else {\n var res = {\n\t url: images[1],\n\t size: 48\n\t}\n}\nreturn res;", - "useLabelFunction": true, - "provider": "openstreet-map", - "draggableMarker": true - }, - "title": "New Markers Placement - OpenStreetMap", - "dropShadow": true, - "enableFullscreen": false, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "useDashboardTimewindow": true, - "showLegend": false, - "widgetStyle": {}, - "actions": { - "tooltipAction": [ - { - "name": "delete", - "icon": "more_horiz", - "type": "custom", - "customFunction": "var entityDatasource = widgetContext.mapInstance.datasources.filter(\n function(entity) {\n return entity.entityId === entityId.id\n });\n\nwidgetContext.mapInstance.saveMarkerLocation(entityDatasource[0], null, null).subscribe(function success() {\n widgetContext.updateAliases();\n});", - "id": "54c293c4-9ca6-e34f-dc6a-0271944c1c66" - } - ] - }, - "showTitleIcon": false, - "titleIcon": null, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "displayTimewindow": true - }, - "id": "00fb2742-ba1f-7e43-673f-d6c08b72ed06", - "typeFullFqn": "system.input_widgets.markers_placement_openstreetmap" - }, - "0a430429-9078-9ae6-2b67-e4a15a2bf8bf": { - "type": "latest", - "sizeX": 6, - "sizeY": 6, - "config": { - "datasources": [ - { - "type": "entity", - "dataKeys": [ - { - "name": "longitude", - "type": "attribute", - "label": "longitude", - "color": "#2196f3", - "settings": {}, - "_hash": 0.3640193654284214 - }, - { - "name": "latitude", - "type": "attribute", - "label": "latitude", - "color": "#4caf50", - "settings": {}, - "_hash": 0.49020393887695923 - }, - { - "name": "temperature", - "type": "timeseries", - "label": "temperature", - "color": "#f44336", - "settings": {}, - "_hash": 0.5885892766009955, - "postFuncBody": "return value || \"\";" - }, - { - "name": "humidity", - "type": "timeseries", - "label": "humidity", - "color": "#ffc107", - "settings": {}, - "_hash": 0.21077893588180707, - "postFuncBody": "return value || \"\";" - }, - { - "name": "active", - "type": "attribute", - "label": "active", - "color": "#607d8b", - "settings": {}, - "_hash": 0.34722983638504346 - } - ], - "entityAliasId": "12ae98c7-1ea2-52cf-64d5-763e9d993547" - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1694085177686, - "endTimeMs": 1694171577686 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "8px", - "settings": { - "fitMapBounds": true, - "latKeyName": "latitude", - "lngKeyName": "longitude", - "showLabel": true, - "label": "${entityName}", - "tooltipPattern": "${entityName}

Temperature: ${temperature:1} °C
Humidity: ${humidity:0} %

Delete", - "markerImageSize": 34, - "useColorFunction": false, - "markerImages": [ - "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_0.svg", - "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_1.svg" - ], - "useMarkerImageFunction": true, - "color": "#fe7569", - "mapProvider": "OpenStreetMap.HOT", - "showTooltip": true, - "autocloseTooltip": true, - "defaultCenterPosition": "0,0", - "customProviderTileUrl": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", - "showTooltipAction": "click", - "polygonKeyName": "coordinates", - "polygonOpacity": 0.5, - "polygonStrokeOpacity": 1, - "polygonStrokeWeight": 1, - "zoomOnClick": true, - "showCoverageOnHover": true, - "animate": true, - "maxClusterRadius": 80, - "removeOutsideVisibleBounds": true, - "defaultZoomLevel": 5, - "labelFunction": "var color;\nif(dsData[dsIndex].active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '' + \n '${entityLabel}' + \n ''", - "markerImageFunction": "var res;\nif(dsData[dsIndex].active !== \"true\"){\n\tvar res = {\n\t url: images[0],\n\t size: 48\n\t}\n} else {\n var res = {\n\t url: images[1],\n\t size: 48\n\t}\n}\nreturn res;", - "useLabelFunction": true, - "provider": "openstreet-map", - "draggableMarker": true, - "editablePolygon": true - }, - "title": "New Markers Placement - OpenStreetMap", - "dropShadow": true, - "enableFullscreen": false, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "useDashboardTimewindow": true, - "showLegend": false, - "widgetStyle": {}, - "actions": { - "tooltipAction": [ - { - "name": "delete", - "icon": "more_horiz", - "type": "custom", - "customFunction": "var entityDatasource = widgetContext.mapInstance.datasources.filter(\n function(entity) {\n return entity.entityId === entityId.id\n });\n\nwidgetContext.mapInstance.saveMarkerLocation(entityDatasource[0], null, null).subscribe(function success() {\n widgetContext.updateAliases();\n});", - "id": "54c293c4-9ca6-e34f-dc6a-0271944c1c66" - } - ] - }, - "showTitleIcon": false, - "titleIcon": null, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "displayTimewindow": true - }, - "id": "0a430429-9078-9ae6-2b67-e4a15a2bf8bf", - "typeFullFqn": "system.input_widgets.markers_placement_openstreetmap" - }, - "eda8a397-0959-690c-405c-11e2c9b2bc7e": { - "typeFullFqn": "system.time_series_chart", - "type": "timeseries", - "sizeX": 8, - "sizeY": 5, - "config": { - "datasources": [ - { - "type": "entity", - "name": "", - "entityAliasId": "12ae98c7-1ea2-52cf-64d5-763e9d993547", - "dataKeys": [ - { - "name": "temperature", - "type": "timeseries", - "label": "Temperature", - "color": "#EF5350", - "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "line", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": true, - "lineType": "solid", - "lineWidth": 2.5, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "circle", - "pointSize": 14, - "fillAreaSettings": { - "type": "gradient", - "opacity": 0.4, - "gradient": { - "start": 60, - "end": 10 - } - } - }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } - }, - "_hash": 0.5973804076994531, - "units": "°C", - "decimals": 1, - "aggregationType": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - }, - "latestDataKeys": [ - { - "name": "temperatureAlarmThreshold", - "type": "attribute", - "label": "temperatureAlarmThreshold", - "color": "#4caf50", - "settings": { - "__thresholdKey": true - }, - "_hash": 0.7120450032526351 - } - ] - } - ], - "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, - "selectedTab": 0, - "realtime": { - "realtimeType": 0, - "timewindowMs": 3600000, - "quickInterval": "CURRENT_DAY", - "interval": 30000 - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - }, - "timezone": null - }, - "showTitle": true, - "backgroundColor": "rgba(0, 0, 0, 0)", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", + "padding": "0px", "settings": { "showLegend": true, "legendConfig": { @@ -1419,9 +970,6 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, @@ -1705,17 +1253,793 @@ "row": 0, "col": 0, "id": "ac90f089-197f-b767-82c3-2668844265a2" - } - }, - "states": { - "default": { - "name": "Thermostats", - "root": true, - "layouts": { - "main": { - "widgets": { - "f33c746c-0dfc-c212-395b-b448c8a17209": { - "sizeX": 11, + }, + "5186d1e3-f076-e062-5129-e4dd8e4adfb0": { + "typeFullFqn": "system.map", + "type": "latest", + "sizeX": 8.5, + "sizeY": 6, + "config": { + "datasources": [], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1741884755143, + "endTimeMs": 1741971155143 + }, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "rgba(0, 0, 0, 0)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "mapType": "geoMap", + "layers": [ + { + "label": "{i18n:widgets.maps.layer.roadmap}", + "provider": "openstreet", + "layerType": "OpenStreetMap.Mapnik" + }, + { + "label": "{i18n:widgets.maps.layer.satellite}", + "provider": "openstreet", + "layerType": "Esri.WorldImagery" + }, + { + "label": "{i18n:widgets.maps.layer.hybrid}", + "provider": "openstreet", + "layerType": "Esri.WorldImagery", + "referenceLayer": "openstreetmap_hybrid" + } + ], + "imageSource": null, + "markers": [ + { + "dsType": "entity", + "dsLabel": "", + "dsDeviceId": null, + "dsEntityAliasId": "68a058e1-fdda-8482-715b-3ae4a488568e", + "dsFilterId": null, + "additionalDataKeys": [ + { + "name": "temperature", + "type": "timeseries", + "label": "temperature", + "color": "#2196f3", + "settings": {}, + "_hash": 0.570889787682481, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return value || \"\";" + }, + { + "name": "humidity", + "type": "timeseries", + "label": "humidity", + "color": "#2196f3", + "settings": {}, + "_hash": 0.13597394595782442, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return value || \"\";" + }, + { + "name": "active", + "type": "attribute", + "label": "active", + "color": "#2196f3", + "settings": {}, + "_hash": 0.21080919932756603 + } + ], + "label": { + "show": true, + "type": "function", + "pattern": "${entityName}", + "patternFunction": "var color;\nif(data.active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '' + \n '${entityLabel}' + \n ''" + }, + "tooltip": { + "show": true, + "trigger": "click", + "autoclose": true, + "type": "pattern", + "pattern": "${entityName}

Temperature: ${temperature:1} °C
Humidity: ${humidity:0} %

Thermostat details
", + "offsetX": 0, + "offsetY": -1, + "patternFunction": null, + "tagActions": [ + { + "name": "navigate_to_details", + "type": "openDashboardState", + "targetDashboardStateId": "chart", + "setEntityId": true, + "stateEntityParamName": null, + "openRightLayout": false, + "openInSeparateDialog": false, + "openInPopover": false + } + ] + }, + "click": { + "type": "doNothing" + }, + "groups": null, + "edit": { + "enabledActions": [], + "attributeScope": "SERVER_SCOPE", + "snappable": false + }, + "xKey": { + "name": "latitude", + "label": "latitude", + "type": "attribute", + "settings": {}, + "color": "#2196f3" + }, + "yKey": { + "name": "longitude", + "label": "longitude", + "type": "attribute", + "settings": {}, + "color": "#2196f3" + }, + "markerType": "icon", + "markerShape": { + "shape": "markerShape1", + "size": 34, + "color": { + "type": "constant", + "color": "#307FE5" + } + }, + "markerIcon": { + "size": 48, + "color": { + "type": "function", + "color": "rgb(255, 0, 0)", + "range": null, + "colorFunction": "if (data.active !== \"true\"){\n return 'rgb(255, 0, 0)';\n} else {\n return 'rgb(39, 134, 34)';\n}" + }, + "iconContainer": "iconContainer1", + "icon": "thermostat" + }, + "markerImage": { + "type": "function", + "imageFunction": "return {};", + "images": [] + }, + "markerOffsetX": 0.5, + "markerOffsetY": 1, + "markerClustering": { + "enable": false, + "zoomOnClick": true, + "maxZoom": null, + "maxClusterRadius": 80, + "zoomAnimation": true, + "showCoverageOnHover": true, + "spiderfyOnMaxZoom": false, + "chunkedLoad": false, + "lazyLoad": true, + "useClusterMarkerColorFunction": false, + "clusterMarkerColorFunction": null + } + } + ], + "polygons": [], + "circles": [], + "additionalDataSources": [], + "controlsPosition": "topleft", + "zoomActions": [ + "scroll", + "doubleClick", + "controlButtons" + ], + "scales": [], + "dragModeButton": false, + "fitMapBounds": true, + "useDefaultCenterPosition": false, + "defaultCenterPosition": "0,0", + "defaultZoomLevel": 14, + "mapPageSize": 16384, + "mapActionButtons": [], + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "padding": "8px" + }, + "title": "Map", + "useDashboardTimewindow": true, + "displayTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "dropShadow": true, + "enableFullscreen": false, + "widgetStyle": {}, + "widgetCss": "", + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "pageSize": 1024, + "noDataDisplayMessage": "", + "configMode": "advanced", + "titleFont": null, + "titleColor": null, + "margin": "0px", + "borderRadius": "0px", + "iconSize": "24px", + "titleIcon": "map", + "iconColor": "#1F6BDD", + "actions": {} + }, + "row": 0, + "col": 0, + "id": "5186d1e3-f076-e062-5129-e4dd8e4adfb0" + }, + "1bbd4b8d-db42-ed7c-70c2-3d32c771843d": { + "typeFullFqn": "system.map", + "type": "latest", + "sizeX": 8.5, + "sizeY": 6, + "config": { + "datasources": [], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1741884755143, + "endTimeMs": 1741971155143 + }, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "rgba(0, 0, 0, 0)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "mapType": "geoMap", + "layers": [ + { + "label": "{i18n:widgets.maps.layer.roadmap}", + "provider": "openstreet", + "layerType": "OpenStreetMap.Mapnik" + }, + { + "label": "{i18n:widgets.maps.layer.satellite}", + "provider": "openstreet", + "layerType": "Esri.WorldImagery" + }, + { + "label": "{i18n:widgets.maps.layer.hybrid}", + "provider": "openstreet", + "layerType": "Esri.WorldImagery", + "referenceLayer": "openstreetmap_hybrid" + } + ], + "imageSource": null, + "markers": [ + { + "dsType": "entity", + "dsLabel": "", + "dsDeviceId": null, + "dsEntityAliasId": "68a058e1-fdda-8482-715b-3ae4a488568e", + "dsFilterId": null, + "additionalDataKeys": [ + { + "name": "temperature", + "type": "timeseries", + "label": "temperature", + "color": "#2196f3", + "settings": {}, + "_hash": 0.570889787682481, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return value || \"\";" + }, + { + "name": "humidity", + "type": "timeseries", + "label": "humidity", + "color": "#2196f3", + "settings": {}, + "_hash": 0.13597394595782442, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return value || \"\";" + }, + { + "name": "active", + "type": "attribute", + "label": "active", + "color": "#2196f3", + "settings": {}, + "_hash": 0.21080919932756603 + } + ], + "label": { + "show": true, + "type": "function", + "pattern": "${entityName}", + "patternFunction": "var color;\nif(data.active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '' + \n '${entityLabel}' + \n ''" + }, + "tooltip": { + "show": true, + "trigger": "click", + "autoclose": true, + "type": "pattern", + "pattern": "${entityName}

Temperature: ${temperature:1} °C
Humidity: ${humidity:0} %

Delete", + "offsetX": 0, + "offsetY": -1, + "patternFunction": null, + "tagActions": [ + { + "name": "delete", + "type": "custom", + "customFunction": "widgetContext.mapInstance.saveMarkerLocation(additionalParams, null, null).subscribe(function success() {\n widgetContext.updateAliases();\n});", + "openInSeparateDialog": false, + "openInPopover": false + } + ] + }, + "click": { + "type": "doNothing" + }, + "groups": null, + "edit": { + "enabledActions": [ + "add", + "move", + "remove" + ], + "attributeScope": "SERVER_SCOPE", + "snappable": false + }, + "xKey": { + "name": "latitude", + "label": "latitude", + "type": "attribute", + "settings": {}, + "color": "#2196f3" + }, + "yKey": { + "name": "longitude", + "label": "longitude", + "type": "attribute", + "settings": {}, + "color": "#2196f3" + }, + "markerType": "icon", + "markerShape": { + "shape": "markerShape1", + "size": 34, + "color": { + "type": "constant", + "color": "#307FE5" + } + }, + "markerIcon": { + "size": 48, + "color": { + "type": "function", + "color": "rgb(255, 0, 0)", + "range": null, + "colorFunction": "if (data.active !== \"true\"){\n return 'rgb(255, 0, 0)';\n} else {\n return 'rgb(39, 134, 34)';\n}" + }, + "iconContainer": "iconContainer1", + "icon": "thermostat" + }, + "markerImage": { + "type": "function", + "imageFunction": "return {};", + "images": [] + }, + "markerOffsetX": 0.5, + "markerOffsetY": 1, + "markerClustering": { + "enable": false, + "zoomOnClick": true, + "maxZoom": null, + "maxClusterRadius": 80, + "zoomAnimation": true, + "showCoverageOnHover": true, + "spiderfyOnMaxZoom": false, + "chunkedLoad": false, + "lazyLoad": true, + "useClusterMarkerColorFunction": false, + "clusterMarkerColorFunction": null + } + } + ], + "polygons": [], + "circles": [], + "additionalDataSources": [], + "controlsPosition": "topleft", + "zoomActions": [ + "scroll", + "doubleClick", + "controlButtons" + ], + "scales": [], + "dragModeButton": false, + "fitMapBounds": true, + "useDefaultCenterPosition": false, + "defaultCenterPosition": "0,0", + "defaultZoomLevel": null, + "mapPageSize": 16384, + "mapActionButtons": [], + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "padding": "8px" + }, + "title": "Map", + "useDashboardTimewindow": true, + "displayTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "dropShadow": true, + "enableFullscreen": false, + "widgetStyle": {}, + "widgetCss": "", + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "pageSize": 1024, + "noDataDisplayMessage": "", + "configMode": "advanced", + "titleFont": null, + "titleColor": null, + "margin": "0px", + "borderRadius": "0px", + "iconSize": "24px", + "titleIcon": "map", + "iconColor": "#1F6BDD", + "actions": {} + }, + "row": 0, + "col": 0, + "id": "1bbd4b8d-db42-ed7c-70c2-3d32c771843d" + }, + "c9da2d6b-4ba9-1ac8-63b8-68ab49b13c81": { + "typeFullFqn": "system.map", + "type": "latest", + "sizeX": 8.5, + "sizeY": 6, + "config": { + "datasources": [], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1741884755143, + "endTimeMs": 1741971155143 + }, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "rgba(0, 0, 0, 0)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "mapType": "geoMap", + "layers": [ + { + "label": "{i18n:widgets.maps.layer.roadmap}", + "provider": "openstreet", + "layerType": "OpenStreetMap.Mapnik" + }, + { + "label": "{i18n:widgets.maps.layer.satellite}", + "provider": "openstreet", + "layerType": "Esri.WorldImagery" + }, + { + "label": "{i18n:widgets.maps.layer.hybrid}", + "provider": "openstreet", + "layerType": "Esri.WorldImagery", + "referenceLayer": "openstreetmap_hybrid" + } + ], + "imageSource": null, + "markers": [ + { + "dsType": "entity", + "dsLabel": "", + "dsDeviceId": null, + "dsEntityAliasId": "12ae98c7-1ea2-52cf-64d5-763e9d993547", + "dsFilterId": null, + "additionalDataKeys": [ + { + "name": "temperature", + "type": "timeseries", + "label": "temperature", + "color": "#2196f3", + "settings": {}, + "_hash": 0.570889787682481, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return value || \"\";" + }, + { + "name": "humidity", + "type": "timeseries", + "label": "humidity", + "color": "#2196f3", + "settings": {}, + "_hash": 0.13597394595782442, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return value || \"\";" + }, + { + "name": "active", + "type": "attribute", + "label": "active", + "color": "#2196f3", + "settings": {}, + "_hash": 0.21080919932756603 + } + ], + "label": { + "show": true, + "type": "function", + "pattern": "${entityName}", + "patternFunction": "var color;\nif(data.active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '' + \n '${entityLabel}' + \n ''" + }, + "tooltip": { + "show": true, + "trigger": "click", + "autoclose": true, + "type": "pattern", + "pattern": "${entityName}

Temperature: ${temperature:1} °C
Humidity: ${humidity:0} %

Delete", + "offsetX": 0, + "offsetY": -1, + "patternFunction": null, + "tagActions": [ + { + "name": "delete", + "type": "custom", + "customFunction": "widgetContext.mapInstance.saveMarkerLocation(additionalParams, null, null).subscribe(function success() {\n widgetContext.updateAliases();\n});", + "openInSeparateDialog": false, + "openInPopover": false + } + ] + }, + "click": { + "type": "doNothing" + }, + "groups": null, + "edit": { + "enabledActions": [ + "add", + "move", + "remove" + ], + "attributeScope": "SERVER_SCOPE", + "snappable": false + }, + "xKey": { + "name": "latitude", + "label": "latitude", + "type": "attribute", + "settings": {}, + "color": "#2196f3" + }, + "yKey": { + "name": "longitude", + "label": "longitude", + "type": "attribute", + "settings": {}, + "color": "#2196f3" + }, + "markerType": "icon", + "markerShape": { + "shape": "markerShape1", + "size": 34, + "color": { + "type": "constant", + "color": "#307FE5" + } + }, + "markerIcon": { + "size": 48, + "color": { + "type": "function", + "color": "rgb(255, 0, 0)", + "range": null, + "colorFunction": "if (data.active !== \"true\"){\n return 'rgb(255, 0, 0)';\n} else {\n return 'rgb(39, 134, 34)';\n}" + }, + "iconContainer": "iconContainer1", + "icon": "thermostat" + }, + "markerImage": { + "type": "function", + "imageFunction": "return {};", + "images": [] + }, + "markerOffsetX": 0.5, + "markerOffsetY": 1, + "markerClustering": { + "enable": false, + "zoomOnClick": true, + "maxZoom": null, + "maxClusterRadius": 80, + "zoomAnimation": true, + "showCoverageOnHover": true, + "spiderfyOnMaxZoom": false, + "chunkedLoad": false, + "lazyLoad": true, + "useClusterMarkerColorFunction": false, + "clusterMarkerColorFunction": null + } + } + ], + "polygons": [], + "circles": [], + "additionalDataSources": [], + "controlsPosition": "topleft", + "zoomActions": [ + "scroll", + "doubleClick", + "controlButtons" + ], + "scales": [], + "dragModeButton": false, + "fitMapBounds": true, + "useDefaultCenterPosition": false, + "defaultCenterPosition": "0,0", + "defaultZoomLevel": 5, + "mapPageSize": 16384, + "mapActionButtons": [], + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "padding": "8px" + }, + "title": "Map", + "useDashboardTimewindow": true, + "displayTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "dropShadow": true, + "enableFullscreen": false, + "widgetStyle": {}, + "widgetCss": "", + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "pageSize": 1024, + "noDataDisplayMessage": "", + "configMode": "advanced", + "titleFont": null, + "titleColor": null, + "margin": "0px", + "borderRadius": "0px", + "iconSize": "24px", + "titleIcon": "map", + "iconColor": "#1F6BDD", + "actions": {} + }, + "row": 0, + "col": 0, + "id": "c9da2d6b-4ba9-1ac8-63b8-68ab49b13c81" + } + }, + "states": { + "default": { + "name": "Thermostats", + "root": true, + "layouts": { + "main": { + "widgets": { + "f33c746c-0dfc-c212-395b-b448c8a17209": { + "sizeX": 11, "sizeY": 11, "row": 0, "col": 0, @@ -1730,11 +2054,12 @@ "mobileOrder": 2, "mobileHeight": 5 }, - "3da9a9a1-0b9a-2e1f-0dcb-0ff34a695abb": { + "5186d1e3-f076-e062-5129-e4dd8e4adfb0": { "sizeX": 13, "sizeY": 6, "row": 5, "col": 11, + "resizable": true, "mobileOrder": 3, "mobileHeight": 5 } @@ -1748,7 +2073,8 @@ "mobileAutoFillHeight": false, "mobileRowHeight": 70, "margin": 10, - "outerMargin": true + "outerMargin": true, + "layoutType": "default" } } } @@ -1759,9 +2085,10 @@ "layouts": { "main": { "widgets": { - "00fb2742-ba1f-7e43-673f-d6c08b72ed06": { + "1bbd4b8d-db42-ed7c-70c2-3d32c771843d": { "sizeX": 24, - "sizeY": 12, + "sizeY": 11, + "resizable": true, "row": 0, "col": 0 } @@ -1775,7 +2102,8 @@ "mobileAutoFillHeight": false, "mobileRowHeight": 70, "margin": 10, - "outerMargin": true + "outerMargin": true, + "layoutType": "default" } } } @@ -1794,14 +2122,6 @@ "mobileOrder": 3, "mobileHeight": 5 }, - "0a430429-9078-9ae6-2b67-e4a15a2bf8bf": { - "sizeX": 6, - "sizeY": 6, - "row": 6, - "col": 0, - "mobileOrder": 4, - "mobileHeight": 6 - }, "eda8a397-0959-690c-405c-11e2c9b2bc7e": { "sizeX": 18, "sizeY": 6, @@ -1817,6 +2137,13 @@ "col": 6, "mobileOrder": 2, "mobileHeight": 6 + }, + "c9da2d6b-4ba9-1ac8-63b8-68ab49b13c81": { + "sizeX": 6, + "sizeY": 6, + "resizable": true, + "row": 6, + "col": 0 } }, "gridSettings": { @@ -1828,7 +2155,8 @@ "mobileAutoFillHeight": false, "mobileRowHeight": 70, "margin": 10, - "outerMargin": true + "outerMargin": true, + "layoutType": "default" } } } @@ -1861,7 +2189,6 @@ "timewindow": { "displayValue": "", "selectedTab": 0, - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, "realtime": { @@ -1889,33 +2216,17 @@ "showEntitiesSelect": true, "showDashboardTimewindow": true, "showDashboardExport": true, - "toolbarAlwaysOpen": true + "toolbarAlwaysOpen": true, + "titleColor": "rgba(0,0,0,0.870588)", + "showDashboardLogo": false, + "dashboardLogoUrl": null, + "hideToolbar": false, + "showFilters": true, + "showUpdateDashboardImage": true, + "dashboardCss": "" }, "filters": {} }, "name": "Thermostats", - "resources": [ - { - "link": "/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_0.svg", - "title": "\"Thermostats\" dashboard widget \"Thermostat maps\" marker image 0", - "type": "IMAGE", - "subType": "IMAGE", - "fileName": "thermostats_dashboard_widget_thermostat_maps_marker_image_0.svg", - "publicResourceKey": "DXvJsh8m6v4NOcO6AHXXm9kQzfrVgisT", - "mediaType": "image/svg+xml", - "data": "PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJzdmc0NDA4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjAiIHk9IjAiIHZpZXdCb3g9IjAgMCAxNTAgMTUwIiB4bWw6c3BhY2U9InByZXNlcnZlIj48c3R5bGU+LnN0MntmaWxsOiNmNDQzMzZ9PC9zdHlsZT48ZyBpZD0ibGF5ZXIxIj48ZyBpZD0icGF0aDY4ODEtMy01LTUtMS04LTQtNC03LTgiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xNDYuNDM4IC0yNzYuMDI4KSIgb3BhY2l0eT0iLjg5MiI+PHJhZGlhbEdyYWRpZW50IGlkPSJTVkdJRF8xXyIgY3g9IjMwODUuMjE1IiBjeT0iMzE3OC40NTgiIHI9IjQ5LjkwMSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjc5MyAuMDA3NiAtLjUwOSAuNTYxMiAtMjMyLjYyOSAtMTQxMS43MjUpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLW9wYWNpdHk9Ii4xODgiLz48L3JhZGlhbEdyYWRpZW50PjxwYXRoIGQ9Ik0yODUuNiAzODguNWMxMC4zLTEyLjQgNC40LTIyLjQtMTQuNC0yMi40LTE4LjkgMC00Mi40IDEwLTUzLjkgMjIuNC0xNi44IDE4IC40IDIzLjUtLjIgMzUtLjEgMS44IDMuOSAxLjggNyAwIDE5LjgtMTEuNSA0Ni41LTE3IDYxLjUtMzUiIGZpbGw9InVybCgjU1ZHSURfMV8pIi8+PC9nPjxwYXRoIGlkPSJwYXRoNjg4MS0zLTUtNS0xLTgtNC00IiBjbGFzcz0ic3QyIiBkPSJNMTI0LjcgNjkuMWMtLjktMjcuNS0yMi4zLTQ5LjgtNDkuOC00OS44cy00OSAyMi4zLTQ5LjggNDkuOGMtMS4zIDQwLjEgMzAuNyA1Mi4yIDQ0LjcgNzggMi4yIDQgOCA0IDEwLjEgMCAxNC4xLTI1LjggNDYuMS0zNy45IDQ0LjgtNzgiLz48L2c+PGcgaWQ9Imc0OTI4Ij48Y2lyY2xlIGlkPSJwYXRoNDk3OCIgY2xhc3M9InN0MiIgY3g9Ijc0LjkiIGN5PSI2OS4xIiByPSI0OS45Ii8+PGcgaWQ9Imc0OTE1Ij48cGF0aCBpZD0icGF0aDY4ODMtMi0zLTUtMi00LTktNC05IiBkPSJNNzQuOCAxMDYuNGMtMjAuNiAwLTM3LjQtMTYuNy0zNy40LTM3LjQgMC0yMC42IDE2LjctMzcuNCAzNy40LTM3LjQgMjAuNiAwIDM3LjQgMTYuNyAzNy40IDM3LjRzLTE2LjcgMzcuNC0zNy40IDM3LjQiIGZpbGw9IiNmZmYiLz48L2c+PC9nPjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik05NS45IDQ2LjZWNDloLTEwdi0yLjVsMTAgLjF6bS0yIDUuM2gtOHYyLjVoOHYtMi41em0tOCA3LjloNnYtMi41aC02djIuNXptNCAyLjloLTR2Mi41aDR2LTIuNXptLTQgNy44aDJWNjhoLTJ2Mi41em0xLjUgMTRjMCA2LjktNS41IDEyLjUtMTIuMyAxMi41cy0xMi4zLTUuNi0xMi4zLTEyLjVjMC00LjUgMi4zLTguNSA2LjEtMTAuN1Y0NS41YzAtMy41IDIuOC02LjMgNi4yLTYuM3M2LjIgMi44IDYuMiA2LjN2MjguM2MzLjggMi4yIDYuMSA2LjMgNi4xIDEwLjd6bS0yLjQgMGMwLTMuOC0yLjEtNy4yLTUuNC04LjlsLS43LS4zVjQ1LjVjMC0yLjEtMS43LTMuOC0zLjgtMy44LTIuMSAwLTMuOCAxLjctMy44IDMuOHYyOS44bC0uNy4zYy0zLjMgMS43LTUuNCA1LjEtNS40IDguOSAwIDUuNSA0LjQgMTAgOS45IDEwUzg1IDkwIDg1IDg0LjV6bS0yLjEgMGMwIDQuNC0zLjUgOC03LjggOHMtNy44LTMuNi03LjgtOGMwLTMuNiAyLjQtNi44IDUuOC03LjdsLjUtLjFWNjEuNWgzLjF2MTUuMmwuNS4xYzMuMyAxIDUuNyA0LjEgNS43IDcuN3ptLTcuNC01LjNjLS4yLS44LTEtMS40LTEuOS0xLjItMyAuNy01IDMuMy01IDYuNCAwIC45LjcgMS42IDEuNiAxLjZzMS42LS43IDEuNi0xLjZjMC0xLjYgMS4xLTMgMi42LTMuMy43LS4yIDEuMy0xIDEuMS0xLjl6Ii8+PC9zdmc+", - "public": true - }, - { - "link": "/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_1.svg", - "title": "\"Thermostats\" dashboard widget \"Thermostat maps\" marker image 1", - "type": "IMAGE", - "subType": "IMAGE", - "fileName": "thermostats_dashboard_widget_thermostat_maps_marker_image_1.svg", - "publicResourceKey": "HtfNoQ7FAZKdeH4m3WGodwajcsscKrTR", - "mediaType": "image/svg+xml", - "data": "PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJzdmc0NDA4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjAiIHk9IjAiIHZpZXdCb3g9IjAgMCAxNTAgMTUwIiB4bWw6c3BhY2U9InByZXNlcnZlIj48c3R5bGU+LnN0MntmaWxsOiMyNzg2MjJ9PC9zdHlsZT48ZyBpZD0ibGF5ZXIxIj48ZyBpZD0icGF0aDY4ODEtMy01LTUtMS04LTQtNC03LTgiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xNDYuNDM4IC0yNzYuMDI4KSIgb3BhY2l0eT0iLjg5MiI+PHJhZGlhbEdyYWRpZW50IGlkPSJTVkdJRF8xXyIgY3g9IjMwODUuMjE1IiBjeT0iMzE3OC40NTgiIHI9IjQ5LjkwMSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjc5MyAuMDA3NiAtLjUwOSAuNTYxMiAtMjMyLjYyOSAtMTQxMS43MjUpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLW9wYWNpdHk9Ii4xODgiLz48L3JhZGlhbEdyYWRpZW50PjxwYXRoIGQ9Ik0yODUuNiAzODguNWMxMC4zLTEyLjQgNC40LTIyLjQtMTQuNC0yMi40LTE4LjkgMC00Mi40IDEwLTUzLjkgMjIuNC0xNi44IDE4IC40IDIzLjUtLjIgMzUtLjEgMS44IDMuOSAxLjggNyAwIDE5LjgtMTEuNSA0Ni41LTE3IDYxLjUtMzUiIGZpbGw9InVybCgjU1ZHSURfMV8pIi8+PC9nPjxwYXRoIGlkPSJwYXRoNjg4MS0zLTUtNS0xLTgtNC00IiBjbGFzcz0ic3QyIiBkPSJNMTI0LjcgNjkuMWMtLjktMjcuNS0yMi4zLTQ5LjgtNDkuOC00OS44cy00OSAyMi4zLTQ5LjggNDkuOGMtMS4zIDQwLjEgMzAuNyA1Mi4yIDQ0LjcgNzggMi4yIDQgOCA0IDEwLjEgMCAxNC4xLTI1LjggNDYuMS0zNy45IDQ0LjgtNzgiLz48L2c+PGcgaWQ9Imc0OTI4Ij48Y2lyY2xlIGlkPSJwYXRoNDk3OCIgY2xhc3M9InN0MiIgY3g9Ijc0LjkiIGN5PSI2OS4xIiByPSI0OS45Ii8+PGcgaWQ9Imc0OTE1Ij48cGF0aCBpZD0icGF0aDY4ODMtMi0zLTUtMi00LTktNC05IiBkPSJNNzQuOCAxMDYuNGMtMjAuNiAwLTM3LjQtMTYuNy0zNy40LTM3LjQgMC0yMC42IDE2LjctMzcuNCAzNy40LTM3LjQgMjAuNiAwIDM3LjQgMTYuNyAzNy40IDM3LjRzLTE2LjcgMzcuNC0zNy40IDM3LjQiIGZpbGw9IiNmZmYiLz48L2c+PC9nPjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik05NS45IDQ2LjZWNDloLTEwdi0yLjVsMTAgLjF6bS0yIDUuM2gtOHYyLjVoOHYtMi41em0tOCA3LjloNnYtMi41aC02djIuNXptNCAyLjloLTR2Mi41aDR2LTIuNXptLTQgNy44aDJWNjhoLTJ2Mi41em0xLjUgMTRjMCA2LjktNS41IDEyLjUtMTIuMyAxMi41cy0xMi4zLTUuNi0xMi4zLTEyLjVjMC00LjUgMi4zLTguNSA2LjEtMTAuN1Y0NS41YzAtMy41IDIuOC02LjMgNi4yLTYuM3M2LjIgMi44IDYuMiA2LjN2MjguM2MzLjggMi4yIDYuMSA2LjMgNi4xIDEwLjd6bS0yLjQgMGMwLTMuOC0yLjEtNy4yLTUuNC04LjlsLS43LS4zVjQ1LjVjMC0yLjEtMS43LTMuOC0zLjgtMy44LTIuMSAwLTMuOCAxLjctMy44IDMuOHYyOS44bC0uNy4zYy0zLjMgMS43LTUuNCA1LjEtNS40IDguOSAwIDUuNSA0LjQgMTAgOS45IDEwUzg1IDkwIDg1IDg0LjV6bS0yLjEgMGMwIDQuNC0zLjUgOC03LjggOHMtNy44LTMuNi03LjgtOGMwLTMuNiAyLjQtNi44IDUuOC03LjdsLjUtLjFWNjEuNWgzLjF2MTUuMmwuNS4xYzMuMyAxIDUuNyA0LjEgNS43IDcuN3ptLTcuNC01LjNjLS4yLS44LTEtMS40LTEuOS0xLjItMyAuNy01IDMuMy01IDYuNCAwIC45LjcgMS42IDEuNiAxLjZzMS42LS43IDEuNi0xLjZjMC0xLjYgMS4xLTMgMi42LTMuMy43LS4yIDEuMy0xIDEuMS0xLjl6Ii8+PC9zdmc+Cg==", - "public": true - } - ] + "resources": [] } \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg index 513809cca5..acb895cd5a 100644 --- a/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg @@ -33,7 +33,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -278,80 +278,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueTextFont", @@ -364,80 +327,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -450,128 +411,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/elevated-tank.svg b/application/src/main/data/json/system/scada_symbols/elevated-tank.svg index 9db211457e..48668e9851 100644 --- a/application/src/main/data/json/system/scada_symbols/elevated-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/elevated-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 265;\n var majorIntervalLength = 895 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(825, y, 857, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 815, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(837, minorY, 857, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 265;\n var majorIntervalLength = 895 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(825, y, 857, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 815, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(837, minorY, 857, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -274,80 +274,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -360,80 +323,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "transparent", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ], + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -446,128 +407,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg b/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg index cff6a97eb9..18f194684b 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg @@ -38,7 +38,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 592 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(208, y, 240, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 198, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 198, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 198, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(220, minorY, 240, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 592 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(208, y, 240, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 198, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 198, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 198, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(220, minorY, 240, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -307,64 +307,67 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#EBEBEB", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#C8DFF7", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ], + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": false, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -377,96 +380,57 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks}", "type": "color", "default": "#00000061", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks}", "type": "color", "default": "#0000001F", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "warningColor", "name": "{i18n:scada.symbol.alarm-colors}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "criticalColor", "name": "{i18n:scada.symbol.alarm-colors}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg b/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg index 7875b5dde0..b6b6eede7b 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg @@ -33,7 +33,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -278,80 +278,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -364,80 +327,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -450,128 +411,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg index 772e34f0f7..69360fbe9c 100644 --- a/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg @@ -33,7 +33,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 60;\n var majorIntervalLength = 910 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(656, y, 688, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 646, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(668, minorY, 688, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 60;\n var majorIntervalLength = 910 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(656, y, 688, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 646, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(668, minorY, 688, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -278,80 +278,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -364,80 +327,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -450,128 +411,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg index dc9cb47268..11bd47916f 100644 --- a/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 60;\n var majorIntervalLength = 910 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(656, y, 688, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 646, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(668, minorY, 688, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 60;\n var majorIntervalLength = 910 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(656, y, 688, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 646, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(668, minorY, 688, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -279,80 +279,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -365,80 +328,76 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ] }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -451,128 +410,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg index b7cb67f2a0..d9c05bde40 100644 --- a/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 203;\n var majorIntervalLength = 763 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(676, y, 708, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 666, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(688, minorY, 708, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 203;\n var majorIntervalLength = 763 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(676, y, 708, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 666, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(688, minorY, 708, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -279,80 +279,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -365,80 +328,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -451,128 +412,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg index 3b9420e04d..cc168915ad 100644 --- a/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg @@ -33,7 +33,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 203;\n var majorIntervalLength = 763 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(676, y, 708, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 666, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(688, minorY, 708, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 203;\n var majorIntervalLength = 763 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(676, y, 708, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 666, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(688, minorY, 708, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -278,80 +278,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -364,80 +327,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -450,128 +411,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg b/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg index 689851529d..ed6eb25bf0 100644 --- a/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg +++ b/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg @@ -1,7 +1,7 @@ { - "title": "Long bottom filter with configurable click actions for custom operations, dashboard manipulation, etc.", - "description": "Long bottom filter", + "title": "Long bottom filter", + "description": "Long bottom filter with configurable click actions for custom operations, dashboard manipulation, etc.", "searchTags": [ "filter" ], diff --git a/application/src/main/data/json/system/scada_symbols/long-top-filter.svg b/application/src/main/data/json/system/scada_symbols/long-top-filter.svg index 9b287cbe46..d23059de37 100644 --- a/application/src/main/data/json/system/scada_symbols/long-top-filter.svg +++ b/application/src/main/data/json/system/scada_symbols/long-top-filter.svg @@ -1,7 +1,7 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="600" fill="none" version="1.1" viewBox="0 0 200 600"> <tb:metadata xmlns=""><![CDATA[{ - "title": "Long top filter with configurable click actions for custom operations, dashboard manipulation, etc.", - "description": "Title\nLong top filter\n", + "title": "Long top filter", + "description": "Long top filter with configurable click actions for custom operations, dashboard manipulation, etc.", "searchTags": [ "filter" ], diff --git a/application/src/main/data/json/system/scada_symbols/pool-hp.svg b/application/src/main/data/json/system/scada_symbols/pool-hp.svg index 357563a687..925ea8906a 100644 --- a/application/src/main/data/json/system/scada_symbols/pool-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/pool-hp.svg @@ -38,7 +38,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 792 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(298, y, 330, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 288, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 288, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 288, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(310, minorY, 330, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 792 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(298, y, 330, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 288, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 288, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 288, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(310, minorY, 330, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -307,64 +307,57 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#EBEBEB", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#C8DFF7", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ], + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": false, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -377,96 +370,67 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks}", "type": "color", "default": "#00000061", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks}", "type": "color", "default": "#0000001F", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "warningColor", "name": "{i18n:scada.symbol.alarm-colors}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "criticalColor", "name": "{i18n:scada.symbol.alarm-colors}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] } diff --git a/application/src/main/data/json/system/scada_symbols/short-top-filter.svg b/application/src/main/data/json/system/scada_symbols/short-top-filter.svg index 0a77c78f00..0cb6da475c 100644 --- a/application/src/main/data/json/system/scada_symbols/short-top-filter.svg +++ b/application/src/main/data/json/system/scada_symbols/short-top-filter.svg @@ -1,7 +1,7 @@ { - "title": "Short top filter with configurable click actions for custom operations, dashboard manipulation, etc.", - "description": "Short top filter", + "title": "Short top filter", + "description": "Short top filter with configurable click actions for custom operations, dashboard manipulation, etc.", "searchTags": [ "filter" ], diff --git a/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg b/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg index 1f635fc810..046c5c5802 100644 --- a/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg @@ -38,7 +38,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 594 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(170, y, 202, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 160, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 160, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 160, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(182, minorY, 202, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 594 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(170, y, 202, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 160, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 160, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 160, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(182, minorY, 202, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -307,64 +307,67 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#EBEBEB", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#C8DFF7", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ], + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": false, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -377,96 +380,57 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks}", "type": "color", "default": "#00000061", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks}", "type": "color", "default": "#0000001F", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "warningColor", "name": "{i18n:scada.symbol.alarm-colors}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "criticalColor", "name": "{i18n:scada.symbol.alarm-colors}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] } diff --git a/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg index 85bad5d9e0..d4ddda1895 100644 --- a/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 45;\n var majorIntervalLength = 525 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 45;\n var majorIntervalLength = 525 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -279,80 +279,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueTextFont", @@ -365,80 +328,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -451,128 +412,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg b/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg index 7e6e4fa8a6..b8e3987188 100644 --- a/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 560 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(268, y, 300, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 258, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(280, minorY, 300, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 560 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(268, y, 300, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 258, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(280, minorY, 300, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -279,80 +279,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueTextFont", @@ -365,80 +328,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -451,128 +412,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/spherical-tank.svg b/application/src/main/data/json/system/scada_symbols/spherical-tank.svg index 4184bcf593..cba49c63e0 100644 --- a/application/src/main/data/json/system/scada_symbols/spherical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/spherical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 960 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(458, y, 490, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 448, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(470, minorY, 490, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 960 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(458, y, 490, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 448, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(470, minorY, 490, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -279,80 +279,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueTextFont", @@ -365,80 +328,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -451,128 +412,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg index 2ad4ac9ec8..a78060dfaf 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -279,80 +279,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -365,80 +328,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -451,128 +412,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg index 8c3745ec3d..63a21bcf08 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -279,80 +279,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -365,80 +328,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -451,128 +412,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg index 43eac50d35..96df175589 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg @@ -35,7 +35,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 137;\n var majorIntervalLength = 442 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(523, y, 555, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 513, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(535, minorY, 555, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 137;\n var majorIntervalLength = 442 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(523, y, 555, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 513, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(535, minorY, 555, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -280,80 +280,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -366,80 +329,78 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -452,128 +413,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg index 07c3ea050c..0d0e368b9b 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -279,80 +279,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -365,80 +328,76 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ] }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -451,128 +410,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg b/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg index d4358e8fa7..57a79d72d7 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 137;\n var majorIntervalLength = 442 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(523, y, 555, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 513, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(535, minorY, 555, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 137;\n var majorIntervalLength = 442 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(523, y, 555, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 513, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(535, minorY, 555, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -279,80 +279,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -365,80 +328,79 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "required": false, + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -451,128 +413,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg b/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg index 3e29a4e657..491fb4477e 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg @@ -38,7 +38,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 994 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(160, y, 192, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 150, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 150, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 150, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(172, minorY, 192, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 994 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(160, y, 192, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 150, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 150, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 150, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(172, minorY, 192, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -307,64 +307,67 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#EBEBEB", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#C8DFF7", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ], + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": false, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -377,96 +380,57 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks}", "type": "color", "default": "#00000061", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks}", "type": "color", "default": "#0000001F", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "warningColor", "name": "{i18n:scada.symbol.alarm-colors}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "criticalColor", "name": "{i18n:scada.symbol.alarm-colors}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/vertical-tank.svg index de7f907e76..99fa9b649c 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-tank.svg @@ -33,7 +33,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -278,80 +278,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -364,80 +327,79 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "scaleDisplayFormat", + "name": "{i18n:scada.symbol.scale}", + "type": "select", + "default": true, + "subLabel": "{i18n:scada.symbol.display-format}", + "divider": false, + "disableOnProperty": "scale", + "items": [ + { + "value": true, + "label": "Percentage" + }, + { + "value": false, + "label": "Absolute" + } + ], + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", "type": "number", "default": 10, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.major-ticks}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": true, + "disableOnProperty": "scale", + "disabled": false, + "visible": true }, { "id": "majorFont", @@ -450,128 +412,84 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#00000061", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorWarningColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "majorCriticalColor", "name": "{i18n:scada.symbol.major-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorIntervals", "name": "{i18n:scada.symbol.minor-ticks}", "type": "number", "default": 5, - "required": null, "subLabel": "{i18n:scada.symbol.intervals}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", "min": 1, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#0000001F", - "required": null, "subLabel": "{i18n:scada.symbol.normal}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorWarningColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minorCriticalColor", "name": "{i18n:scada.symbol.minor-ticks-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/widget_bundles/maps.json b/application/src/main/data/json/system/widget_bundles/maps.json index d0d0741e21..74b4870de1 100644 --- a/application/src/main/data/json/system/widget_bundles/maps.json +++ b/application/src/main/data/json/system/widget_bundles/maps.json @@ -2,9 +2,9 @@ "widgetsBundle": { "alias": "maps_v2", "title": "Maps", - "image": "tb-image:bWFwc19zeXN0ZW1fYnVuZGxlX2ltYWdlLnBuZw==:Ik1hcHMiIHN5c3RlbSBidW5kbGUgaW1hZ2U=:SU1BR0U=;data:image/png;base64,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", + "image": "tb-image:bWFwcy13aWRnZXQtYnVuZGxlLnBuZw==:Ik1hcHMiIHN5c3RlbSBidW5kbGUgaW1hZ2U=:SU1BR0U=;data:image/png;base64,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", "scada": false, - "description": "Visualize the latest location or trip of the devices or other entities on the indoor or outdoor maps.", + "description": "Visualize the latest location or trip of devices and entities on indoor and outdoor maps using markers, polygons, and circles for enhanced spatial representation.", "order": 6000, "name": "Maps" }, diff --git a/application/src/main/data/json/system/widget_types/horizontal_ellipse_tank.json b/application/src/main/data/json/system/widget_types/horizontal_ellipse_tank.json index 6e7d86b847..441a5a520c 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_ellipse_tank.json +++ b/application/src/main/data/json/system/widget_types/horizontal_ellipse_tank.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-liquid-level-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-liquid-level-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"Static\",\"selectedShape\":\"Horizontal Ellipse\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"#FFFFFFC2\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Horizontal Ellipse\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"#FFFFFFC2\",\"rangeList\":[{\"from\":0,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" }, "tags": [ "reservoir", 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 2b7d56957b..2a4fe71696 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 @@ -2,8 +2,8 @@ "fqn": "image_map", "name": "Image Map", "deprecated": false, - "image": "tb-image;/api/images/system/image_map_system_widget_image.png", - "description": "Displays the indoor or relative location of the entities on the image map. Useful to display floor maps, smart parking, etc. Entity coordinates are expected to be in the range from 0 to 1. Highly customizable via custom markers, marker tooltips, and widget actions. ", + "image": "tb-image;/api/images/system/image-map-widget.png", + "description": "Displays the indoor or relative location of entities on an image map, making it ideal for floor plans, smart parking, and more. Entity coordinates are expected to range from 0 to 1. Supports markers, marker tooltips, widget actions, polygons, and circles for enhanced spatial representation.", "descriptor": { "type": "latest", "sizeX": 8.5, @@ -21,14 +21,14 @@ }, "resources": [ { - "link": "/api/images/system/image_map_system_widget_image.png", + "link": "/api/images/system/image-map-widget.png", "title": "\"Image Map\" system widget image", "type": "IMAGE", "subType": "IMAGE", - "fileName": "image_map_system_widget_image.png", - "publicResourceKey": "otJxNhSbraXccAZhnPzmfOIdEXra5Hf5", + "fileName": "image-map-widget.png", + "publicResourceKey": "UJKMEwPHCOL8rQk6qr9pRRHNnD90yeTA", "mediaType": "image/png", - "data": "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", + "data": "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", "public": true }, { @@ -45,6 +45,20 @@ ], "scada": false, "tags": [ + "plan", + "zone", + "parking", + "location", + "coordinates", + "indoor", + "image", + "marker", + "geofence", + "placement", + "polygon", + "circle", + "layer", + "tiles", "building", "interior", "venue", 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 9f246a215a..b9069d8af6 100644 --- a/application/src/main/data/json/system/widget_types/map.json +++ b/application/src/main/data/json/system/widget_types/map.json @@ -2,8 +2,8 @@ "fqn": "map", "name": "Map", "deprecated": false, - "image": "tb-image;/api/images/system/openstreet_map_system_widget_image.png", - "description": "Displays the location of the entities on Map. Allows to choose among existing tile providers or configure own server. Some providers require additional licenses. Highly customizable via custom markers, marker tooltips, and widget actions. ", + "image": "tb-image;/api/images/system/map-widget.png", + "description": "Displays the location of the entities on Map. Allows to choose among existing tile providers or configure own server. Supports markers, marker tooltips, widget actions, polygons, and circles for enhanced spatial representation.", "descriptor": { "type": "latest", "sizeX": 8.5, @@ -21,24 +21,36 @@ }, "resources": [ { - "link": "/api/images/system/openstreet_map_system_widget_image.png", - "title": "\"OpenStreet Map\" system widget image", + "link": "/api/images/system/map-widget.png", + "title": "\"Map\" system widget image", "type": "IMAGE", "subType": "IMAGE", - "fileName": "openstreet_map_system_widget_image.png", - "publicResourceKey": "Uyd9JmVKUcCF6PchzgNnAOjx9WvVN5ZE", + "fileName": "map-widget.png", + "publicResourceKey": "scAsnySDiQSGXiKpt69cZ9jxZh0zl3eL", "mediaType": "image/png", - "data": "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", + "data": "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", "public": true } ], "scada": false, "tags": [ - "mapping", - "gps", + "markers", + "polygon", + "circle", "navigation", + "position", + "sensor", "geolocation", "satellite", - "directions" + "roadmap", + "directions", + "placement", + "layer", + "openstreet", + "google", + "tiles", + "location", + "mapping", + "gps" ] } \ No newline at end of file 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 6d9590fe90..f1d2cd571c 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 @@ -2,8 +2,8 @@ "fqn": "route_map", "name": "Route Map", "deprecated": false, - "image": "tb-image;/api/images/system/route_map_openstreet_system_widget_image.png", - "description": "Displays the trip of the entity on the OpenStreetMap or other map providers. Highly customizable via custom markers, marker tooltips, and widget actions.", + "image": "tb-image;/api/images/system/route-map-widget.png", + "description": "Displays an entity's trip on various map providers. Supports custom markers, marker tooltips, widget actions, polygons, and circles for enhanced spatial representation.", "descriptor": { "type": "timeseries", "sizeX": 8.5, @@ -57,24 +57,40 @@ "public": true }, { - "link": "/api/images/system/route_map_openstreet_system_widget_image.png", - "title": "\"Route Map - OpenStreet\" system widget image", + "link": "/api/images/system/route-map-widget.png", + "title": "\"Route Map\" system widget image", "type": "IMAGE", "subType": "IMAGE", - "fileName": "route_map_openstreet_system_widget_image.png", - "publicResourceKey": "VTTAajdw2NRpCUbQLGVFCMBybEtJfwFK", + "fileName": "route-map-widget.png", + "publicResourceKey": "xHDxUSAefNkVjlpwj2OoNCHgKGGJLfbx", "mediaType": "image/png", - "data": "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", + "data": "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", "public": true } ], "scada": false, "tags": [ + "trip", + "route", + "movement", + "tracking", + "path", + "marker", + "location", + "point", + "satellite", + "directions", + "placement", + "polygon", + "circle", + "layer", + "openstreet", + "google", + "tiles", + "roadmap", "mapping", "gps", "navigation", - "geolocation", - "satellite", - "directions" + "geolocation" ] } \ No newline at end of file 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 09eba54978..a596e74c1f 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 @@ -2,8 +2,8 @@ "fqn": "trip_map", "name": "Trip Map", "deprecated": false, - "image": "tb-image;/api/images/system/trip_animation_system_widget_image.png", - "description": "Displays the trip of the entity on the OpenStreetMap or other map providers. Allows to scroll and animate the movement of the entity. Highly customizable via custom markers, marker tooltips, and widget actions.", + "image": "tb-image;/api/images/system/trip-map-widget.png", + "description": "Displays an entity's trip on various map providers, allowing scrolling and animated movement. Supports custom markers, marker tooltips, widget actions, polygons, and circles for enhanced spatial representation.", "descriptor": { "type": "timeseries", "sizeX": 8.5, @@ -24,24 +24,41 @@ }, "resources": [ { - "link": "/api/images/system/trip_animation_system_widget_image.png", - "title": "\"Trip Animation\" system widget image", + "link": "/api/images/system/trip-map-widget.png", + "title": "\"Trip Map\" system widget image", "type": "IMAGE", "subType": "IMAGE", - "fileName": "trip_animation_system_widget_image.png", - "publicResourceKey": "3UKAE6mZvW6bnIhr7NAtYTG8FbIDFY1e", + "fileName": "trip-map-widget.png", + "publicResourceKey": "nF3ox4p08cuECHLQYNdnhFUpkjK9Uw7P", "mediaType": "image/png", - "data": "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", + "data": "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", "public": true } ], "scada": false, "tags": [ + "trip", + "route", + "movement", + "tracking", + "path", + "point", + "timeline", + "marker", + "location", + "satellite", + "directions", + "placement", + "polygon", + "circle", + "layer", + "openstreet", + "google", + "tiles", + "roadmap", "mapping", "gps", "navigation", - "geolocation", - "satellite", - "directions" + "geolocation" ] } \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 9df613353d..1ed919e922 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -141,6 +141,7 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -552,7 +553,7 @@ public class ActorSystemContext { @Value("${actors.session.max_concurrent_sessions_per_device:1}") @Getter - private long maxConcurrentSessionsPerDevice; + private int maxConcurrentSessionsPerDevice; @Value("${actors.session.sync.timeout:10000}") @Getter @@ -856,9 +857,9 @@ public class ActorSystemContext { appActor.tellWithHighPriority(tbActorMsg); } - public void schedulePeriodicMsgWithDelay(TbActorRef ctx, TbActorMsg msg, long delayInMs, long periodInMs) { + public ScheduledFuture schedulePeriodicMsgWithDelay(TbActorRef ctx, TbActorMsg msg, long delayInMs, long periodInMs) { log.debug("Scheduling periodic msg {} every {} ms with delay {} ms", msg, periodInMs, delayInMs); - getScheduler().scheduleWithFixedDelay(() -> ctx.tell(msg), delayInMs, periodInMs, TimeUnit.MILLISECONDS); + return getScheduler().scheduleWithFixedDelay(() -> ctx.tell(msg), delayInMs, periodInMs, TimeUnit.MILLISECONDS); } public void scheduleMsgWithDelay(TbActorRef ctx, TbActorMsg msg, long delayInMs) { diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index 50fa7e2f8d..904547a2b6 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -116,7 +116,6 @@ public class AppActor extends ContextAwareActor { case CF_INIT_MSG: case CF_LINK_INIT_MSG: case CF_STATE_RESTORE_MSG: - case CF_ENTITY_LIFECYCLE_MSG: //TODO: use priority from the message body. For example, messages about CF lifecycle are important and Device lifecycle are not. // same for the Linked telemetry. onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, true); 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 31cb159229..a185b71d56 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 @@ -287,7 +287,11 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM state.checkStateSize(ctxId, ctx.getMaxStateSize()); stateSizeChecked = true; if (state.isSizeOk()) { - cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback); + if (!calculationResult.isEmpty()) { + cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback); + } else { + callback.onSuccess(); + } if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) { systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, JacksonUtil.writeValueAsString(calculationResult.getResult()), null); } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index b0185fd555..fc48e9ad3e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -22,6 +22,7 @@ import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.actors.TbCalculatedFieldEntityActorId; import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; @@ -35,6 +36,7 @@ import org.thingsboard.server.common.msg.cf.CalculatedFieldInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldLinkInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; +import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; @@ -121,7 +123,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware if (calculatedField != null) { msg.getState().setRequiredArguments(calculatedField.getArgNames()); - log.info("Pushing CF state restore msg to specific actor [{}]", msg.getId().entityId()); + log.debug("Pushing CF state restore msg to specific actor [{}]", msg.getId().entityId()); getOrCreateActor(msg.getId().entityId()).tell(msg); } else { cfStateService.removeState(msg.getId(), msg.getCallback()); @@ -178,6 +180,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware EntityId entityId = msg.getEntityId(); EntityId profileId = getProfileId(tenantId, entityId); cfEntityCache.add(tenantId, profileId, entityId); + if (!isMyPartition(entityId, callback)) { + return; + } var entityIdFields = getCalculatedFieldsByEntityId(entityId); var profileIdFields = getCalculatedFieldsByEntityId(profileId); var fieldsCount = entityIdFields.size() + profileIdFields.size(); @@ -191,8 +196,11 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } private void onEntityUpdated(ComponentLifecycleMsg msg, TbCallback callback) { - if (msg.getOldProfileId() != null && msg.getOldProfileId() != msg.getProfileId()) { + if (msg.getOldProfileId() != null && !msg.getOldProfileId().equals(msg.getProfileId())) { cfEntityCache.update(tenantId, msg.getOldProfileId(), msg.getProfileId(), msg.getEntityId()); + if (!isMyPartition(msg.getEntityId(), callback)) { + return; + } var oldProfileCfs = getCalculatedFieldsByEntityId(msg.getOldProfileId()); var newProfileCfs = getCalculatedFieldsByEntityId(msg.getProfileId()); var fieldsCount = oldProfileCfs.size() + newProfileCfs.size(); @@ -209,8 +217,10 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private void onEntityDeleted(ComponentLifecycleMsg msg, TbCallback callback) { cfEntityCache.evict(tenantId, msg.getEntityId()); - log.info("Pushing entity lifecycle msg to specific actor [{}]", msg.getEntityId()); - getOrCreateActor(msg.getEntityId()).tell(new CalculatedFieldEntityDeleteMsg(tenantId, msg.getEntityId(), callback)); + if (isMyPartition(msg.getEntityId(), callback)) { + log.debug("Pushing entity lifecycle msg to specific actor [{}]", msg.getEntityId()); + getOrCreateActor(msg.getEntityId()).tell(new CalculatedFieldEntityDeleteMsg(tenantId, msg.getEntityId(), callback)); + } } private void onCfCreated(ComponentLifecycleMsg msg, TbCallback callback) throws CalculatedFieldException { @@ -311,7 +321,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware callback.onSuccess(); } } else { - deleteCfForEntity(entityId, cfId, callback); + if (isMyPartition(entityId, callback)) { + deleteCfForEntity(entityId, cfId, callback); + } } } } @@ -418,20 +430,31 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware callback.onSuccess(); } } else { - initCfForEntity(entityId, cfCtx, forceStateReinit, callback); + if (isMyPartition(entityId, callback)) { + initCfForEntity(entityId, cfCtx, forceStateReinit, callback); + } } } private void deleteCfForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) { - log.info("Pushing delete CF msg to specific actor [{}]", entityId); + log.debug("Pushing delete CF msg to specific actor [{}]", entityId); getOrCreateActor(entityId).tell(new CalculatedFieldEntityDeleteMsg(tenantId, cfId, callback)); } private void initCfForEntity(EntityId entityId, CalculatedFieldCtx cfCtx, boolean forceStateReinit, TbCallback callback) { - log.info("Pushing entity init CF msg to specific actor [{}]", entityId); + log.debug("Pushing entity init CF msg to specific actor [{}]", entityId); getOrCreateActor(entityId).tell(new EntityInitCalculatedFieldMsg(tenantId, cfCtx, callback, forceStateReinit)); } + private boolean isMyPartition(EntityId entityId, TbCallback callback) { + if (!systemContext.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId).isMyPartition()) { + log.debug("[{}] Entity belongs to external partition.", entityId); + callback.onSuccess(); + return false; + } + return true; + } + private static boolean isProfileEntity(EntityType entityType) { return EntityType.DEVICE_PROFILE.equals(entityType) || EntityType.ASSET_PROFILE.equals(entityType); } diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java index 5778f02663..7b390cd9f3 100644 --- a/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java @@ -29,6 +29,9 @@ import org.thingsboard.server.common.msg.TbActorStopReason; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; +import java.util.Optional; +import java.util.concurrent.ScheduledFuture; + /** * @author Andrew Shvayka */ @@ -41,6 +44,7 @@ public abstract class ComponentActor statsScheduledFuture = null; public ComponentActor(ActorSystemContext systemContext, TenantId tenantId, T id) { super(systemContext); @@ -73,9 +77,9 @@ public abstract class ComponentActor x.cancel(false)); + statsScheduledFuture = null; } catch (Exception e) { log.warn("[{}][{}] Failed to stop {} processor: {}", tenantId, id, id.getEntityType(), e.getMessage()); logAndPersist("OnStop", e, true); diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java index 838bf0ea36..17f432eca5 100644 --- a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java @@ -21,6 +21,7 @@ import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.common.msg.TbActorMsg; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; @Slf4j public abstract class AbstractContextAwareMsgProcessor { @@ -36,8 +37,8 @@ public abstract class AbstractContextAwareMsgProcessor { return systemContext.getScheduler(); } - protected void schedulePeriodicMsgWithDelay(TbActorCtx ctx, TbActorMsg msg, long delayInMs, long periodInMs) { - systemContext.schedulePeriodicMsgWithDelay(ctx, msg, delayInMs, periodInMs); + protected ScheduledFuture schedulePeriodicMsgWithDelay(TbActorCtx ctx, TbActorMsg msg, long delayInMs, long periodInMs) { + return systemContext.schedulePeriodicMsgWithDelay(ctx, msg, delayInMs, periodInMs); } protected void scheduleMsgWithDelay(TbActorCtx ctx, TbActorMsg msg, long delayInMs) { diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java index d48a8f7684..76e5ce7899 100644 --- a/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java @@ -27,6 +27,8 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.RuleNodeException; +import java.util.concurrent.ScheduledFuture; + @Slf4j public abstract class ComponentMsgProcessor extends AbstractContextAwareMsgProcessor { @@ -77,8 +79,8 @@ public abstract class ComponentMsgProcessor extends Abstract start(context); } - public void scheduleStatsPersistTick(TbActorCtx context, long statsPersistFrequency) { - schedulePeriodicMsgWithDelay(context, StatsPersistTick.INSTANCE, statsPersistFrequency, statsPersistFrequency); + public ScheduledFuture scheduleStatsPersistTick(TbActorCtx context, long statsPersistFrequency) { + return schedulePeriodicMsgWithDelay(context, StatsPersistTick.INSTANCE, statsPersistFrequency, statsPersistFrequency); } protected boolean checkMsgValid(TbMsg tbMsg) { diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index f9eec324fc..846bde508d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -49,6 +49,7 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.aware.DeviceAwareMsg; import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg; +import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; @@ -112,7 +113,7 @@ public class TenantActor extends RuleChainManagerActor { cantFindTenant = true; } } else { - log.info("Tenant {} is not managed by current service, skipping rule chains init", tenantId); + log.info("Tenant {} is not managed by current service, skipping rule chains and cf actor init", tenantId); } } log.debug("[{}] Tenant actor started.", tenantId); @@ -175,7 +176,6 @@ public class TenantActor extends RuleChainManagerActor { case CF_LINK_INIT_MSG: case CF_STATE_RESTORE_MSG: case CF_PARTITIONS_CHANGE_MSG: - case CF_ENTITY_LIFECYCLE_MSG: onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, true); break; case CF_TELEMETRY_MSG: @@ -315,19 +315,26 @@ public class TenantActor extends RuleChainManagerActor { onToDeviceActorMsg(new DeviceDeleteMsg(tenantId, deviceId), true); deletedDevices.add(deviceId); } - if (isRuleEngine && ruleChainsInitialized) { - TbActorRef target = getEntityActorRef(msg.getEntityId()); - if (target != null) { - if (msg.getEntityId().getEntityType() == EntityType.RULE_CHAIN) { - RuleChain ruleChain = systemContext.getRuleChainService(). - findRuleChainById(tenantId, new RuleChainId(msg.getEntityId().getId())); - if (ruleChain != null && RuleChainType.CORE.equals(ruleChain.getType())) { - visit(ruleChain, target); + if (isRuleEngine) { + if (ruleChainsInitialized) { + TbActorRef target = getEntityActorRef(msg.getEntityId()); + if (target != null) { + if (msg.getEntityId().getEntityType() == EntityType.RULE_CHAIN) { + RuleChain ruleChain = systemContext.getRuleChainService(). + findRuleChainById(tenantId, new RuleChainId(msg.getEntityId().getId())); + if (ruleChain != null && RuleChainType.CORE.equals(ruleChain.getType())) { + visit(ruleChain, target); + } } + target.tellWithHighPriority(msg); + } else { + log.debug("[{}] Invalid component lifecycle msg: {}", tenantId, msg); + } + } + if (cfActor != null) { + if (msg.getEntityId().getEntityType().isOneOf(EntityType.CALCULATED_FIELD, EntityType.DEVICE, EntityType.ASSET)) { + cfActor.tellWithHighPriority(new CalculatedFieldEntityLifecycleMsg(tenantId, msg)); } - target.tellWithHighPriority(msg); - } else { - log.debug("[{}] Invalid component lifecycle msg: {}", tenantId, msg); } } } diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java index b0ab4edc27..3dedea999c 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java @@ -54,10 +54,12 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.tools.SchedulerUtils; +import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -144,18 +146,40 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService } @Override - public void process(TbProtoQueueMsg msg, TbCallback callback) { - ToUsageStatsServiceMsg statsMsg = msg.getValue(); - - TenantId tenantId = TenantId.fromUUID(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB())); - EntityId ownerId; - if (statsMsg.getCustomerIdMSB() != 0 && statsMsg.getCustomerIdLSB() != 0) { - ownerId = new CustomerId(new UUID(statsMsg.getCustomerIdMSB(), statsMsg.getCustomerIdLSB())); + public void process(TbProtoQueueMsg msgPack, TbCallback callback) { + ToUsageStatsServiceMsg serviceMsg = msgPack.getValue(); + String serviceId = serviceMsg.getServiceId(); + + List msgs; + + //For backward compatibility, remove after release + if (serviceMsg.getMsgsList().isEmpty()) { + TransportProtos.UsageStatsServiceMsg oldMsg = TransportProtos.UsageStatsServiceMsg.newBuilder() + .setTenantIdMSB(serviceMsg.getTenantIdMSB()) + .setTenantIdLSB(serviceMsg.getTenantIdLSB()) + .setCustomerIdMSB(serviceMsg.getCustomerIdMSB()) + .setCustomerIdLSB(serviceMsg.getCustomerIdLSB()) + .setEntityIdMSB(serviceMsg.getEntityIdMSB()) + .setEntityIdLSB(serviceMsg.getEntityIdLSB()) + .addAllValues(serviceMsg.getValuesList()) + .build(); + + msgs = List.of(oldMsg); } else { - ownerId = tenantId; + msgs = serviceMsg.getMsgsList(); } - processEntityUsageStats(tenantId, ownerId, statsMsg.getValuesList(), statsMsg.getServiceId()); + msgs.forEach(msg -> { + TenantId tenantId = TenantId.fromUUID(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB())); + EntityId ownerId; + if (msg.getCustomerIdMSB() != 0 && msg.getCustomerIdLSB() != 0) { + ownerId = new CustomerId(new UUID(msg.getCustomerIdMSB(), msg.getCustomerIdLSB())); + } else { + ownerId = tenantId; + } + + processEntityUsageStats(tenantId, ownerId, msg.getValuesList(), serviceId); + }); callback.onSuccess(); } @@ -181,7 +205,14 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService updatedEntries = new ArrayList<>(ApiUsageRecordKey.values().length); Set apiFeatures = new HashSet<>(); for (UsageStatsKVProto statsItem : values) { - ApiUsageRecordKey recordKey = ApiUsageRecordKey.valueOf(statsItem.getKey()); + ApiUsageRecordKey recordKey; + + //For backward compatibility, remove after release + if (StringUtils.isNotEmpty(statsItem.getKey())) { + recordKey = ApiUsageRecordKey.valueOf(statsItem.getKey()); + } else { + recordKey = ProtoUtils.fromProto(statsItem.getRecordKey()); + } StatsCalculationResult calculationResult = usageState.calculate(recordKey, statsItem.getValue(), serviceId); if (calculationResult.isValueChanged()) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java index 0163d65d98..91c08ab6e0 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java @@ -19,14 +19,20 @@ import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.calculatedField.CalculatedFieldStateRestoreMsg; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.exception.CalculatedFieldStateException; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; +import org.thingsboard.server.queue.common.state.QueueStateService; +import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; @@ -35,12 +41,7 @@ public abstract class AbstractCalculatedFieldStateService implements CalculatedF @Autowired private ActorSystemContext actorSystemContext; - protected PartitionedQueueConsumerManager> eventConsumer; - - @Override - public void init(PartitionedQueueConsumerManager> eventConsumer) { - this.eventConsumer = eventConsumer; - } + protected QueueStateService, TbProtoQueueMsg> stateService; @Override public final void persistState(CalculatedFieldEntityCtxId stateId, CalculatedFieldState state, TbCallback callback) { @@ -69,4 +70,24 @@ public abstract class AbstractCalculatedFieldStateService implements CalculatedF actorSystemContext.tell(new CalculatedFieldStateRestoreMsg(id, state)); } + @Override + public void restore(QueueKey queueKey, Set partitions) { + stateService.update(queueKey, partitions); + } + + @Override + public void delete(Set partitions) { + stateService.delete(partitions); + } + + @Override + public Set getPartitions() { + return stateService.getPartitions().values().stream().flatMap(Collection::stream).collect(Collectors.toSet()); + } + + @Override + public void stop() { + stateService.stop(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java index 8eb27395c1..49acf6917c 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java @@ -27,4 +27,11 @@ public final class CalculatedFieldResult { private final AttributeScope scope; private final JsonNode result; + public boolean isEmpty() { + return result == null || result.isMissingNode() || result.isNull() || + (result.isObject() && result.isEmpty()) || + (result.isArray() && result.isEmpty()) || + (result.isTextual() && result.asText().isEmpty()); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java index 109f13f183..d0b34f18e8 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java @@ -21,6 +21,7 @@ import org.thingsboard.server.exception.CalculatedFieldStateException; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; +import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; @@ -34,7 +35,11 @@ public interface CalculatedFieldStateService { void removeState(CalculatedFieldEntityCtxId stateId, TbCallback callback); - void restore(Set partitions); + void restore(QueueKey queueKey, Set partitions); + + void delete(Set partitions); + + Set getPartitions(); void stop(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldInitService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldInitService.java index 9442329edb..cc3022fa29 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldInitService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldInitService.java @@ -47,12 +47,20 @@ public class DefaultCalculatedFieldInitService implements CalculatedFieldInitSer PageDataIterable deviceIdInfos = new PageDataIterable<>(deviceService::findProfileEntityIdInfos, initFetchPackSize); for (ProfileEntityIdInfo idInfo : deviceIdInfos) { log.trace("Processing device record: {}", idInfo); - entityProfileCache.add(idInfo.getTenantId(), idInfo.getProfileId(), idInfo.getEntityId()); + try { + entityProfileCache.add(idInfo.getTenantId(), idInfo.getProfileId(), idInfo.getEntityId()); + } catch (Exception e) { + log.error("Failed to process device record: {}", idInfo, e); + } } PageDataIterable assetIdInfos = new PageDataIterable<>(assetService::findProfileEntityIdInfos, initFetchPackSize); for (ProfileEntityIdInfo idInfo : assetIdInfos) { log.trace("Processing asset record: {}", idInfo); - entityProfileCache.add(idInfo.getTenantId(), idInfo.getProfileId(), idInfo.getEntityId()); + try { + entityProfileCache.add(idInfo.getTenantId(), idInfo.getProfileId(), idInfo.getEntityId()); + } catch (Exception e) { + log.error("Failed to process asset record: {}", idInfo, e); + } } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java index ba4be6ace6..8289e4db42 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java @@ -128,6 +128,9 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS private void checkEntityAndPushToQueue(TenantId tenantId, EntityId entityId, Predicate mainEntityFilter, Predicate linkedEntityFilter, Supplier msg, FutureCallback callback) { + if (EntityType.TENANT.equals(entityId.getEntityType())) { + tenantId = (TenantId) entityId; + } boolean send = checkEntityForCalculatedFields(tenantId, entityId, mainEntityFilter, linkedEntityFilter); if (send) { clusterService.pushMsgToCalculatedFields(tenantId, entityId, msg.get(), wrap(callback)); @@ -221,6 +224,10 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS private CalculatedFieldTelemetryMsgProto.Builder buildTelemetryMsgProto(TenantId tenantId, EntityId entityId, List calculatedFieldIds, UUID tbMsgId, TbMsgType tbMsgType) { CalculatedFieldTelemetryMsgProto.Builder telemetryMsg = CalculatedFieldTelemetryMsgProto.newBuilder(); + if (EntityType.TENANT.equals(entityId.getEntityType())) { + tenantId = (TenantId) entityId; + } + telemetryMsg.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); telemetryMsg.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index c51aaa2e72..0c4352dcea 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -83,7 +83,7 @@ public class CalculatedFieldCtx { for (Map.Entry entry : arguments.entrySet()) { var refId = entry.getValue().getRefEntityId(); var refKey = entry.getValue().getRefEntityKey(); - if (refId == null) { + if (refId == null || refId.equals(calculatedField.getEntityId())) { mainEntityArguments.put(refKey, entry.getKey()); } else { linkedEntityArguments.computeIfAbsent(refId, key -> new HashMap<>()).put(refKey, entry.getKey()); 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 557768e9c9..90d4056afc 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 @@ -35,7 +35,7 @@ import org.thingsboard.server.queue.TbQueueMsgHeaders; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; -import org.thingsboard.server.queue.common.consumer.QueueStateService; +import org.thingsboard.server.queue.common.state.KafkaQueueStateService; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; @@ -43,10 +43,12 @@ import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory; import org.thingsboard.server.service.cf.AbstractCalculatedFieldStateService; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; -import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; -import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.*; +import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.bytesToString; +import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.bytesToUuid; +import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.stringToBytes; +import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.uuidToBytes; @Service @RequiredArgsConstructor @@ -60,18 +62,14 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta @Value("${queue.calculated_fields.poll_interval:25}") private long pollInterval; - private PartitionedQueueConsumerManager> stateConsumer; private TbKafkaProducerTemplate> stateProducer; - private QueueStateService, TbProtoQueueMsg> queueStateService; private final AtomicInteger counter = new AtomicInteger(); @Override public void init(PartitionedQueueConsumerManager> eventConsumer) { - super.init(eventConsumer); - var queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.CF_STATES_QUEUE_NAME); - this.stateConsumer = PartitionedQueueConsumerManager.>create() + PartitionedQueueConsumerManager> stateConsumer = PartitionedQueueConsumerManager.>create() .queueKey(queueKey) .topic(partitionService.getTopic(queueKey)) .pollInterval(pollInterval) @@ -94,13 +92,13 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta } }) .consumerCreator((config, partitionId) -> queueFactory.createCalculatedFieldStateConsumer()) + .queueAdmin(queueFactory.getCalculatedFieldQueueAdmin()) .consumerExecutor(eventConsumer.getConsumerExecutor()) .scheduler(eventConsumer.getScheduler()) .taskExecutor(eventConsumer.getTaskExecutor()) .build(); + super.stateService = new KafkaQueueStateService<>(eventConsumer, stateConsumer); this.stateProducer = (TbKafkaProducerTemplate>) queueFactory.createCalculatedFieldStateProducer(); - this.queueStateService = new QueueStateService<>(); - this.queueStateService.init(stateConsumer, super.eventConsumer); } @Override @@ -132,11 +130,6 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta doPersist(stateId, null, callback); } - @Override - public void restore(Set partitions) { - queueStateService.update(partitions); - } - private void putStateId(TbQueueMsgHeaders headers, CalculatedFieldEntityCtxId stateId) { headers.put("tenantId", uuidToBytes(stateId.tenantId().getId())); headers.put("cfId", uuidToBytes(stateId.cfId().getId())); @@ -153,8 +146,7 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta @Override public void stop() { - stateConsumer.stop(); - stateConsumer.awaitStop(); + super.stop(); stateProducer.stop(); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java index a508eecada..0eaa506dfd 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java @@ -22,7 +22,12 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.queue.common.state.DefaultQueueStateService; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; +import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; +import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.service.cf.AbstractCalculatedFieldStateService; import org.thingsboard.server.service.cf.CfRocksDb; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; @@ -37,7 +42,10 @@ public class RocksDBCalculatedFieldStateService extends AbstractCalculatedFieldS private final CfRocksDb cfRocksDb; - private boolean initialized; + @Override + public void init(PartitionedQueueConsumerManager> eventConsumer) { + super.stateService = new DefaultQueueStateService<>(eventConsumer); + } @Override protected void doPersist(CalculatedFieldEntityCtxId stateId, CalculatedFieldStateProto stateMsgProto, TbCallback callback) { @@ -52,8 +60,8 @@ public class RocksDBCalculatedFieldStateService extends AbstractCalculatedFieldS } @Override - public void restore(Set partitions) { - if (!this.initialized) { + public void restore(QueueKey queueKey, Set partitions) { + if (stateService.getPartitions().isEmpty()) { cfRocksDb.forEach((key, value) -> { try { processRestoredState(CalculatedFieldStateProto.parseFrom(value)); @@ -61,13 +69,8 @@ public class RocksDBCalculatedFieldStateService extends AbstractCalculatedFieldS log.error("[{}] Failed to process restored state", key, e); } }); - this.initialized = true; } - eventConsumer.update(partitions); - } - - @Override - public void stop() { + super.restore(queueKey, partitions); } } 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 ad248cc3d4..ac62576714 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java @@ -15,31 +15,31 @@ */ package org.thingsboard.server.service.queue; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; -import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.calculatedField.CalculatedFieldLinkedTelemetryMsg; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.queue.QueueConfig; -import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; -import org.thingsboard.server.gen.transport.TransportProtos.ComponentLifecycleMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; import org.thingsboard.server.queue.TbQueueConsumer; @@ -69,8 +69,6 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.thingsboard.server.common.util.ProtoUtils.fromProto; - @Service @TbRuleEngineComponent @Slf4j @@ -84,8 +82,6 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer private final TbRuleEngineQueueFactory queueFactory; private final CalculatedFieldStateService stateService; - private PartitionedQueueConsumerManager> eventConsumer; - public DefaultTbCalculatedFieldConsumerService(TbRuleEngineQueueFactory tbQueueFactory, ActorSystemContext actorContext, TbDeviceProfileCache deviceProfileCache, @@ -108,12 +104,13 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer super.init("tb-cf"); var queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME); - this.eventConsumer = PartitionedQueueConsumerManager.>create() + PartitionedQueueConsumerManager> eventConsumer = PartitionedQueueConsumerManager.>create() .queueKey(queueKey) .topic(partitionService.getTopic(queueKey)) .pollInterval(pollInterval) .msgPackProcessor(this::processMsgs) .consumerCreator((config, partitionId) -> queueFactory.createToCalculatedFieldMsgConsumer()) + .queueAdmin(queueFactory.getCalculatedFieldQueueAdmin()) .consumerExecutor(consumersExecutor) .scheduler(scheduler) .taskExecutor(mgmtExecutor) @@ -133,9 +130,12 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer @Override protected void onTbApplicationEvent(PartitionChangeEvent event) { - var partitions = event.getCfPartitions(); try { - stateService.restore(partitions); + event.getNewPartitions().forEach((queueKey, partitions) -> { + if (queueKey.getQueueName().equals(DataConstants.CF_QUEUE_NAME)) { + stateService.restore(queueKey, partitions); + } + }); // eventConsumer's partitions will be updated by stateService // Cleanup old entities after corresponding consumers are stopped. @@ -168,9 +168,6 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer forwardToActorSystem(toCfMsg.getTelemetryMsg(), callback); } else if (toCfMsg.hasLinkedTelemetryMsg()) { forwardToActorSystem(toCfMsg.getLinkedTelemetryMsg(), callback); - } else if (toCfMsg.hasComponentLifecycleMsg()) { - log.trace("[{}] Forwarding component lifecycle message for processing {}", id, toCfMsg.getComponentLifecycleMsg()); - forwardToActorSystem(toCfMsg.getComponentLifecycleMsg(), callback); } } catch (Throwable e) { log.warn("[{}] Failed to process message: {}", id, msg, e); @@ -219,15 +216,26 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer @Override protected void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) { ToCalculatedFieldNotificationMsg toCfNotification = msg.getValue(); - if (toCfNotification.hasComponentLifecycleMsg()) { - // from upstream (maybe removed since we don't need to init state for each partition) - log.trace("[{}] Forwarding component lifecycle message for processing {}", id, toCfNotification.getComponentLifecycleMsg()); - forwardToActorSystem(toCfNotification.getComponentLifecycleMsg(), callback); - } else if (toCfNotification.hasLinkedTelemetryMsg()) { + if (toCfNotification.hasLinkedTelemetryMsg()) { forwardToActorSystem(toCfNotification.getLinkedTelemetryMsg(), callback); } } + @EventListener + public void handleComponentLifecycleEvent(ComponentLifecycleMsg event) { + if (event.getEntityId().getEntityType() == EntityType.TENANT) { + if (event.getEvent() == ComponentLifecycleEvent.DELETED) { + Set partitions = stateService.getPartitions(); + if (CollectionUtils.isEmpty(partitions)) { + return; + } + stateService.delete(partitions.stream() + .filter(tpi -> tpi.getTenantId().isPresent() && tpi.getTenantId().get().equals(event.getTenantId())) + .collect(Collectors.toSet())); + } + } + } + private void forwardToActorSystem(CalculatedFieldTelemetryMsgProto msg, TbCallback callback) { var tenantId = toTenantId(msg.getTenantIdMSB(), msg.getTenantIdLSB()); var entityId = EntityIdFactory.getByTypeAndUuid(msg.getEntityType(), new UUID(msg.getEntityIdMSB(), msg.getEntityIdLSB())); @@ -241,11 +249,6 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer actorContext.tell(new CalculatedFieldLinkedTelemetryMsg(tenantId, entityId, linkedMsg, callback)); } - private void forwardToActorSystem(ComponentLifecycleMsgProto proto, TbCallback callback) { - var msg = fromProto(proto); - actorContext.tell(new CalculatedFieldEntityLifecycleMsg(msg.getTenantId(), msg, callback)); - } - private TenantId toTenantId(long tenantIdMSB, long tenantIdLSB) { return TenantId.fromUUID(new UUID(tenantIdMSB, tenantIdLSB)); } @@ -253,9 +256,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer @Override protected void stopConsumers() { super.stopConsumers(); - eventConsumer.stop(); - eventConsumer.awaitStop(); - stateService.stop(); + stateService.stop(); // eventConsumer will be stopped by stateService } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index c7174469b0..92c48a5fed 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -94,10 +94,8 @@ import org.thingsboard.server.queue.common.MultipleTbQueueCallbackWrapper; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbRuleEngineProducerService; import org.thingsboard.server.queue.discovery.PartitionService; -import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; -import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; @@ -146,10 +144,6 @@ public class DefaultTbClusterService implements TbClusterService { @Lazy private OtaPackageStateService otaPackageStateService; - @Autowired - @Lazy - private CalculatedFieldProcessingService calculatedFieldProcessingService; - private final TopicService topicService; private final TbDeviceProfileCache deviceProfileCache; private final TbAssetProfileCache assetProfileCache; @@ -369,13 +363,6 @@ public class DefaultTbClusterService implements TbClusterService { toRuleEngineMsgs.incrementAndGet(); // TODO: add separate counter when we will have new ServiceType.CALCULATED_FIELDS } - @Override - public void pushNotificationToCalculatedFields(TenantId tenantId, EntityId entityId, ToCalculatedFieldNotificationMsg msg, TbQueueCallback callback) { - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId); - producerProvider.getCalculatedFieldsNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), callback); - toRuleEngineNfs.incrementAndGet(); - } - @Override public void broadcastEntityStateChangeEvent(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state) { log.trace("[{}] Processing {} state change event: {}", tenantId, entityId.getEntityType(), state); @@ -431,7 +418,6 @@ public class DefaultTbClusterService implements TbClusterService { public void onDeviceDeleted(TenantId tenantId, Device device, TbQueueCallback callback) { DeviceId deviceId = device.getId(); gatewayNotificationsService.onDeviceDeleted(device); - handleCalculatedFieldEntityDeleted(tenantId, deviceId); broadcastEntityDeleteToTransport(tenantId, deviceId, device.getName(), callback); sendDeviceStateServiceEvent(tenantId, deviceId, false, false, true); broadcastEntityStateChangeEvent(tenantId, deviceId, ComponentLifecycleEvent.DELETED); @@ -440,7 +426,6 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void onAssetDeleted(TenantId tenantId, Asset asset, TbQueueCallback callback) { AssetId assetId = asset.getId(); - handleCalculatedFieldEntityDeleted(tenantId, assetId); broadcastEntityStateChangeEvent(tenantId, assetId, ComponentLifecycleEvent.DELETED); } @@ -604,6 +589,7 @@ public class DefaultTbClusterService implements TbClusterService { || (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED) || entityType.equals(EntityType.ENTITY_VIEW) || entityType.equals(EntityType.NOTIFICATION_RULE) + || entityType.equals(EntityType.CALCULATED_FIELD) ) { TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); @@ -658,38 +644,28 @@ public class DefaultTbClusterService implements TbClusterService { public void onDeviceUpdated(Device entity, Device old) { var created = old == null; broadcastEntityChangeToTransport(entity.getTenantId(), entity.getId(), entity, null); - if (old != null) { + + var msg = ComponentLifecycleMsg.builder() + .tenantId(entity.getTenantId()) + .entityId(entity.getId()) + .profileId(entity.getDeviceProfileId()) + .name(entity.getName()); + if (created) { + msg.event(ComponentLifecycleEvent.CREATED); + } else { boolean deviceNameChanged = !entity.getName().equals(old.getName()); if (deviceNameChanged) { gatewayNotificationsService.onDeviceUpdated(entity, old); } boolean deviceProfileChanged = !entity.getDeviceProfileId().equals(old.getDeviceProfileId()); - if (deviceProfileChanged) { - ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() - .tenantId(entity.getTenantId()) - .entityId(entity.getId()) - .event(ComponentLifecycleEvent.UPDATED) - .oldProfileId(old.getDeviceProfileId()) - .profileId(entity.getDeviceProfileId()) - .oldName(old.getName()) - .name(entity.getName()) - .build(); - broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), TbQueueCallback.EMPTY); - } if (deviceNameChanged || deviceProfileChanged) { pushMsgToCore(new DeviceNameOrTypeUpdateMsg(entity.getTenantId(), entity.getId(), entity.getName(), entity.getType()), null); } - } else { - ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() - .tenantId(entity.getTenantId()) - .entityId(entity.getId()) - .event(ComponentLifecycleEvent.CREATED) - .profileId(entity.getDeviceProfileId()) - .name(entity.getName()) - .build(); - broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), TbQueueCallback.EMPTY); + msg.event(ComponentLifecycleEvent.UPDATED) + .oldProfileId(old.getDeviceProfileId()) + .oldName(old.getName()); } - broadcastEntityStateChangeEvent(entity.getTenantId(), entity.getId(), created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + broadcast(msg.build()); sendDeviceStateServiceEvent(entity.getTenantId(), entity.getId(), created, !created, false); otaPackageStateService.update(entity, old); } @@ -697,48 +673,29 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void onAssetUpdated(Asset entity, Asset old) { var created = old == null; - if (old != null) { - boolean assetTypeChanged = !entity.getAssetProfileId().equals(old.getAssetProfileId()); - if (assetTypeChanged) { - ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() - .tenantId(entity.getTenantId()) - .entityId(entity.getId()) - .event(ComponentLifecycleEvent.UPDATED) - .oldProfileId(old.getAssetProfileId()) - .profileId(entity.getAssetProfileId()) - .oldName(old.getName()) - .name(entity.getName()) - .build(); - broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), TbQueueCallback.EMPTY); - } + var msg = ComponentLifecycleMsg.builder() + .tenantId(entity.getTenantId()) + .entityId(entity.getId()) + .profileId(entity.getAssetProfileId()) + .name(entity.getName()); + if (created) { + msg.event(ComponentLifecycleEvent.CREATED); } else { - ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() - .tenantId(entity.getTenantId()) - .entityId(entity.getId()) - .event(ComponentLifecycleEvent.CREATED) - .profileId(entity.getAssetProfileId()) - .name(entity.getName()) - .build(); - broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), TbQueueCallback.EMPTY); + msg.event(ComponentLifecycleEvent.UPDATED) + .oldProfileId(old.getAssetProfileId()) + .oldName(old.getName()); } - broadcastEntityStateChangeEvent(entity.getTenantId(), entity.getId(), created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + broadcast(msg.build()); } @Override public void onCalculatedFieldUpdated(CalculatedField calculatedField, CalculatedField oldCalculatedField, TbQueueCallback callback) { - var msg = toProto(new ComponentLifecycleMsg(calculatedField.getTenantId(), calculatedField.getId(), oldCalculatedField == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED)); - onCalculatedFieldLifecycleMsg(msg, callback); + broadcastEntityStateChangeEvent(calculatedField.getTenantId(), calculatedField.getId(), oldCalculatedField == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); } @Override public void onCalculatedFieldDeleted(CalculatedField calculatedField, TbQueueCallback callback) { - var msg = toProto(new ComponentLifecycleMsg(calculatedField.getTenantId(), calculatedField.getId(), ComponentLifecycleEvent.DELETED)); - onCalculatedFieldLifecycleMsg(msg, callback); - } - - private void onCalculatedFieldLifecycleMsg(ComponentLifecycleMsgProto msg, TbQueueCallback callback) { - broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(msg).build(), callback); - broadcastToCore(ToCoreNotificationMsg.newBuilder().setComponentLifecycle(msg).build()); + broadcastEntityStateChangeEvent(calculatedField.getTenantId(), calculatedField.getId(), ComponentLifecycleEvent.DELETED); } @Override @@ -868,8 +825,4 @@ public class DefaultTbClusterService implements TbClusterService { } } - private void handleCalculatedFieldEntityDeleted(TenantId tenantId, EntityId entityId) { - ComponentLifecycleMsg msg = new ComponentLifecycleMsg(tenantId, entityId, ComponentLifecycleEvent.DELETED); - broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), TbQueueCallback.EMPTY); - } } 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 d522f11f7b..9cc743e510 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 @@ -29,7 +29,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.rpc.RpcError; -import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; @@ -234,7 +233,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< if (event.getEvent() == ComponentLifecycleEvent.DELETED) { List toRemove = consumers.keySet().stream() .filter(queueKey -> queueKey.getTenantId().equals(event.getTenantId())) - .collect(Collectors.toList()); + .toList(); toRemove.forEach(queueKey -> { removeConsumer(queueKey).ifPresent(consumer -> consumer.delete(false)); }); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java index 8ef6465d4b..3029e0be70 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java @@ -500,10 +500,9 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont @Override public ListenableFuture getEntityDataInfo(User user, EntityId entityId, String versionId) { return Futures.transform(gitServiceQueue.getEntity(user.getTenantId(), versionId, entityId), - entity -> new EntityDataInfo(entity.hasRelations(), entity.hasAttributes(), entity.hasCredentials()), MoreExecutors.directExecutor()); + entity -> new EntityDataInfo(entity.hasRelations(), entity.hasAttributes(), entity.hasCredentials(), entity.hasCalculatedFields()), MoreExecutors.directExecutor()); } - @Override public ListenableFuture> listBranches(TenantId tenantId) { return gitServiceQueue.listBranches(tenantId); diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index 2302446b6b..30f2885e78 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -177,8 +177,8 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer if (strategy.sendWsUpdate()) { addWsCallback(resultFuture, success -> onTimeSeriesUpdate(tenantId, entityId, request.getEntries())); } - if (strategy.saveLatest()) { - copyLatestToEntityViews(tenantId, entityId, request.getEntries()); + if (strategy.saveLatest() && entityId.getEntityType().isOneOf(EntityType.DEVICE, EntityType.ASSET)) { + addMainCallback(resultFuture, __ -> copyLatestToEntityViews(tenantId, entityId, request.getEntries())); } return resultFuture; } @@ -333,58 +333,56 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer } private void copyLatestToEntityViews(TenantId tenantId, EntityId entityId, List ts) { - if (EntityType.DEVICE.equals(entityId.getEntityType()) || EntityType.ASSET.equals(entityId.getEntityType())) { - Futures.addCallback(this.tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId), - new FutureCallback<>() { - @Override - public void onSuccess(@Nullable List result) { - if (result != null && !result.isEmpty()) { - Map> tsMap = new HashMap<>(); - for (TsKvEntry entry : ts) { - tsMap.computeIfAbsent(entry.getKey(), s -> new ArrayList<>()).add(entry); - } - for (EntityView entityView : result) { - List keys = entityView.getKeys() != null && entityView.getKeys().getTimeseries() != null ? - entityView.getKeys().getTimeseries() : new ArrayList<>(tsMap.keySet()); - List entityViewLatest = new ArrayList<>(); - long startTs = entityView.getStartTimeMs(); - long endTs = entityView.getEndTimeMs() == 0 ? Long.MAX_VALUE : entityView.getEndTimeMs(); - for (String key : keys) { - List entries = tsMap.get(key); - if (entries != null) { - Optional tsKvEntry = entries.stream() - .filter(entry -> entry.getTs() > startTs && entry.getTs() <= endTs) - .max(comparingLong(TsKvEntry::getTs)); - tsKvEntry.ifPresent(entityViewLatest::add); - } - } - if (!entityViewLatest.isEmpty()) { - saveTimeseries(TimeseriesSaveRequest.builder() - .tenantId(tenantId) - .entityId(entityView.getId()) - .entries(entityViewLatest) - .strategy(TimeseriesSaveRequest.Strategy.LATEST_AND_WS) - .callback(new FutureCallback<>() { - @Override - public void onSuccess(@Nullable Void tmp) {} - - @Override - public void onFailure(Throwable t) { - log.error("[{}][{}] Failed to save entity view latest timeseries: {}", tenantId, entityView.getId(), entityViewLatest, t); - } - }) - .build()); + Futures.addCallback(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable List result) { + if (result != null && !result.isEmpty()) { + Map> tsMap = new HashMap<>(); + for (TsKvEntry entry : ts) { + tsMap.computeIfAbsent(entry.getKey(), s -> new ArrayList<>()).add(entry); + } + for (EntityView entityView : result) { + List keys = entityView.getKeys() != null && entityView.getKeys().getTimeseries() != null ? + entityView.getKeys().getTimeseries() : new ArrayList<>(tsMap.keySet()); + List entityViewLatest = new ArrayList<>(); + long startTs = entityView.getStartTimeMs(); + long endTs = entityView.getEndTimeMs() == 0 ? Long.MAX_VALUE : entityView.getEndTimeMs(); + for (String key : keys) { + List entries = tsMap.get(key); + if (entries != null) { + Optional tsKvEntry = entries.stream() + .filter(entry -> entry.getTs() > startTs && entry.getTs() <= endTs) + .max(comparingLong(TsKvEntry::getTs)); + tsKvEntry.ifPresent(entityViewLatest::add); } } + if (!entityViewLatest.isEmpty()) { + saveTimeseries(TimeseriesSaveRequest.builder() + .tenantId(tenantId) + .entityId(entityView.getId()) + .entries(entityViewLatest) + .strategy(TimeseriesSaveRequest.Strategy.LATEST_AND_WS) + .callback(new FutureCallback<>() { + @Override + public void onSuccess(@Nullable Void tmp) {} + + @Override + public void onFailure(Throwable t) { + log.error("[{}][{}] Failed to save entity view latest timeseries: {}", tenantId, entityView.getId(), entityViewLatest, t); + } + }) + .build()); + } } } + } - @Override - public void onFailure(Throwable t) { - log.error("Error while finding entity views by tenantId and entityId", t); - } - }, MoreExecutors.directExecutor()); - } + @Override + public void onFailure(Throwable t) { + log.error("Error while finding entity views by tenantId and entityId", t); + } + }, MoreExecutors.directExecutor()); } private void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes) { diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 2c8c942ba3..7fee411704 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -351,7 +351,7 @@ public class DefaultTransportApiService implements TransportApiService { device.setAdditionalInfo(additionalInfo); device = deviceService.saveDevice(device); - relationService.saveRelation(TenantId.SYS_TENANT_ID, new EntityRelation(gateway.getId(), device.getId(), "Created")); + relationService.saveRelation(tenantId, new EntityRelation(gateway.getId(), device.getId(), "Created")); TbMsgMetaData metaData = new TbMsgMetaData(); CustomerId customerId = gateway.getCustomerId(); diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java index 5a74f9f69a..73712542fd 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java @@ -53,8 +53,6 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS @ConditionalOnExpression("'${queue.type:null}'=='kafka' && ${edges.enabled:true} && ${sql.ttl.edge_events.edge_events_ttl:0} > 0") public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService { - private static final String EDGE_EVENT_TOPIC_NAME = "tb_edge_event.notifications."; - private final TopicService topicService; private final TenantService tenantService; private final EdgeService edgeService; @@ -64,6 +62,9 @@ public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService { @Value("${sql.ttl.edge_events.edge_events_ttl:2628000}") private long ttlSeconds; + @Value("${queue.edge.event-notifications-topic:tb_edge_event.notifications}") + private String tbEdgeEventNotificationsTopic; + public KafkaEdgeTopicsCleanUpService(PartitionService partitionService, EdgeService edgeService, TenantService tenantService, AttributesService attributesService, TopicService topicService, TbKafkaSettings kafkaSettings, TbKafkaTopicConfigs kafkaTopicConfigs) { @@ -86,7 +87,7 @@ public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService { return; } - String edgeTopicPrefix = topicService.buildTopicName(EDGE_EVENT_TOPIC_NAME); + String edgeTopicPrefix = topicService.buildTopicName(tbEdgeEventNotificationsTopic); List matchingTopics = topics.stream().filter(topic -> topic.startsWith(edgeTopicPrefix)).toList(); if (matchingTopics.isEmpty()) { log.debug("No matching topics found with prefix [{}]. Skipping cleanup.", edgeTopicPrefix); @@ -147,7 +148,7 @@ public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService { try { String remaining = topic.substring(prefix.length()); String[] parts = remaining.split("\\."); - TenantId tenantId = new TenantId(UUID.fromString(parts[0])); + TenantId tenantId = TenantId.fromUUID(UUID.fromString(parts[0])); EdgeId edgeId = new EdgeId(UUID.fromString(parts[1])); tenantEdgeMap.computeIfAbsent(tenantId, id -> new ArrayList<>()).add(edgeId); } catch (Exception e) { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index b3022fb9d3..c61a993d56 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -187,7 +187,9 @@ usage: # Enable/Disable the collection of API usage statistics on a customer level enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Statistics reporting interval, set to send summarized data every 10 seconds by default - interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + interval: "${USAGE_STATS_REPORT_INTERVAL:60}" + # Amount of statistic messages in pack + pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" check: # Interval of checking the start of the next cycle and re-enabling the blocked tenants/customers cycle: "${USAGE_STATS_CHECK_CYCLE:60000}" @@ -1417,6 +1419,8 @@ device: host: "${DEVICE_CONNECTIVITY_COAPS_HOST:}" # Port of coap transport service. If empty, the default port for coaps will be used. port: "${DEVICE_CONNECTIVITY_COAPS_PORT:5684}" + # Path to the COAP CA root certificate file + pem_cert_file: "${DEVICE_CONNECTIVITY_COAPS_CA_ROOT_CERT:cafile.pem}" # Edges parameters edges: diff --git a/application/src/test/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessorTest.java b/application/src/test/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessorTest.java index 00019c07fa..9243e295bc 100644 --- a/application/src/test/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessorTest.java +++ b/application/src/test/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessorTest.java @@ -49,7 +49,7 @@ public class DeviceActorMessageProcessorTest { public void setUp() { systemContext = mock(ActorSystemContext.class); deviceService = mock(DeviceService.class); - willReturn((long)MAX_CONCURRENT_SESSIONS_PER_DEVICE).given(systemContext).getMaxConcurrentSessionsPerDevice(); + willReturn(MAX_CONCURRENT_SESSIONS_PER_DEVICE).given(systemContext).getMaxConcurrentSessionsPerDevice(); willReturn(deviceService).given(systemContext).getDeviceService(); processor = new DeviceActorMessageProcessor(systemContext, tenantId, deviceId); willReturn(mock(TbCoreToTransportService.class)).given(systemContext).getTbCoreToTransportService(); @@ -58,7 +58,7 @@ public class DeviceActorMessageProcessorTest { @Test public void givenSystemContext_whenNewInstance_thenVerifySessionMapMaxSize() { assertThat(processor.sessions, instanceOf(LinkedHashMapRemoveEldest.class)); - assertThat(processor.sessions.getMaxEntries(), is((long)MAX_CONCURRENT_SESSIONS_PER_DEVICE)); + assertThat(processor.sessions.getMaxEntries(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE)); assertThat(processor.sessions.getRemovalConsumer(), notNullValue()); } diff --git a/application/src/test/java/org/thingsboard/server/actors/service/ComponentActorTest.java b/application/src/test/java/org/thingsboard/server/actors/service/ComponentActorTest.java new file mode 100644 index 0000000000..49bf3c8670 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/actors/service/ComponentActorTest.java @@ -0,0 +1,74 @@ +/** + * 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.actors.service; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.actors.shared.ComponentMsgProcessor; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.msg.TbActorStopReason; + +import java.util.concurrent.ScheduledFuture; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; + +class ComponentActorTest { + ComponentActor componentActor; + + @BeforeEach + void setUp() { + componentActor = Mockito.mock(ComponentActor.class); + } + + @Test + void scheduleStatsPersistTickTest() { + Assertions.assertNull(componentActor.statsScheduledFuture); + ScheduledFuture statsScheduledFuture = Mockito.mock(ScheduledFuture.class); + ActorSystemContext systemContext = Mockito.mock(ActorSystemContext.class); + ReflectionTestUtils.setField(componentActor, "systemContext", systemContext); + ComponentMsgProcessor processor = Mockito.mock(ComponentMsgProcessor.class); + componentActor.processor = processor; + BDDMockito.willReturn(statsScheduledFuture).given(processor).scheduleStatsPersistTick(any(), anyLong()); + BDDMockito.willCallRealMethod().given(componentActor).scheduleStatsPersistTick(); + + componentActor.scheduleStatsPersistTick(); + + Assertions.assertNotNull(componentActor.statsScheduledFuture); + } + + @Test + void destroyTest() { + ScheduledFuture statsScheduledFuture = Mockito.mock(ScheduledFuture.class); + componentActor.statsScheduledFuture = statsScheduledFuture; + Assertions.assertNotNull(componentActor.statsScheduledFuture); + Throwable cause = new Throwable(); + EntityId id = Mockito.mock(EntityId.class); + ReflectionTestUtils.setField(componentActor, "id", id); + BDDMockito.willCallRealMethod().given(componentActor).destroy(any(), any()); + + componentActor.destroy(TbActorStopReason.STOPPED, cause); + + Mockito.verify(statsScheduledFuture).cancel(false); + Assertions.assertNull(componentActor.statsScheduledFuture); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index 9ddf0f0a04..002f668fdc 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -48,6 +48,9 @@ import static org.awaitility.Awaitility.await; @DaoSqlTest public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTest { + public static final int TIMEOUT = 60; + public static final int POLL_INTERVAL = 1; + @BeforeEach void setUp() throws Exception { loginTenantAdmin(); @@ -86,6 +89,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp"); assertThat(fahrenheitTemp).isNotNull(); @@ -95,6 +99,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}")); await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp"); assertThat(fahrenheitTemp).isNotNull(); @@ -108,6 +113,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); await().alias("update CF output -> perform calculation with updated output").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { ArrayNode temperatureF = getServerAttributes(testDevice.getId(), "temperatureF"); assertThat(temperatureF).isNotNull(); @@ -119,6 +125,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); await().alias("update CF argument -> perform calculation with new argument").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { ArrayNode temperatureF = getServerAttributes(testDevice.getId(), "temperatureF"); assertThat(temperatureF).isNotNull(); @@ -129,6 +136,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); await().alias("update CF expression -> perform calculation with new expression").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { ArrayNode temperatureF = getServerAttributes(testDevice.getId(), "temperatureF"); assertThat(temperatureF).isNotNull(); @@ -166,6 +174,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); await().alias("create CF -> state is not ready -> no calculation performed").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp"); assertThat(fahrenheitTemp).isNotNull(); @@ -175,6 +184,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}")); await().alias("update telemetry -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp"); assertThat(fahrenheitTemp).isNotNull(); @@ -213,6 +223,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); await().alias("create CF -> perform initial calculation with default value").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp"); assertThat(fahrenheitTemp).isNotNull(); @@ -222,6 +233,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}")); await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp"); assertThat(fahrenheitTemp).isNotNull(); @@ -277,6 +289,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes doPost("/api/calculatedField", calculatedField, CalculatedField.class); await().alias("create CF and perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { // result of asset 1 ArrayNode z1 = getServerAttributes(asset1.getId(), "z"); @@ -292,6 +305,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"x\":25}")); await().alias("update device telemetry -> recalculate state for all assets").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { // result of asset 1 ArrayNode z1 = getServerAttributes(asset1.getId(), "z"); @@ -307,6 +321,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes doPost("/api/plugins/telemetry/ASSET/" + asset1.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"y\":15}")); await().alias("update asset 1 telemetry -> recalculate state only for asset 1").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { // result of asset 1 ArrayNode z1 = getServerAttributes(asset1.getId(), "z"); @@ -322,6 +337,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes doPost("/api/plugins/telemetry/ASSET/" + asset2.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"y\":5}")); await().alias("update asset 2 telemetry -> recalculate state only for asset 2").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { // result of asset 1 (no changes) ArrayNode z1 = getServerAttributes(asset1.getId(), "z"); @@ -339,6 +355,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes Asset finalAsset3 = asset3; await().alias("add new entity to profile -> calculate state for new entity").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { // result of asset 3 ArrayNode z3 = getServerAttributes(finalAsset3.getId(), "z"); @@ -349,6 +366,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"x\":20}")); await().alias("update device telemetry -> recalculate state for all assets").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { // result of asset 1 ArrayNode z1 = getServerAttributes(asset1.getId(), "z"); @@ -375,6 +393,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes Asset updatedAsset3 = asset3; await().alias("update device telemetry -> recalculate state for asset 1 and asset 2").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { // result of asset 1 ArrayNode z1 = getServerAttributes(asset1.getId(), "z"); @@ -425,6 +444,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); await().alias("create CF -> ctx is not initialized -> no calculation perform").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp"); assertThat(fahrenheitTemp).isNotNull(); @@ -434,6 +454,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}")); await().alias("update telemetry -> ctx is not initialized -> no calculation perform").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp"); assertThat(fahrenheitTemp).isNotNull(); diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index e90aecb41e..33f2209eca 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -38,6 +38,7 @@ import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; @@ -143,6 +144,7 @@ import org.thingsboard.server.dao.device.ClaimDevicesService; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.memory.InMemoryStorage; +import org.thingsboard.server.service.cf.CfRocksDb; import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest; @@ -276,6 +278,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { @Autowired protected InMemoryStorage storage; + @MockBean + protected CfRocksDb cfRocksDb; + @Rule public TestRule watcher = new TestWatcher() { protected void starting(Description description) { diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java index 77002cef8e..b0a3518e60 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java @@ -28,6 +28,7 @@ import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -73,6 +74,7 @@ import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.containsString; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.argThat; @@ -325,6 +327,59 @@ public class BaseQueueControllerTest extends AbstractControllerTest { doDelete("/api/queues/" + queue.getUuidId()).andExpect(status().isOk()); } + @Test + public void testQueueWithReservedName() throws Exception { + loginSysAdmin(); + + // create queue + Queue queue = new Queue(); + queue.setName(DataConstants.CF_QUEUE_NAME); + queue.setTopic("tb_rule_engine.calculated_fields"); + queue.setPollInterval(25); + queue.setPartitions(10); + queue.setTenantId(TenantId.SYS_TENANT_ID); + queue.setConsumerPerPartition(false); + queue.setPackProcessingTimeout(2000); + SubmitStrategy submitStrategy = new SubmitStrategy(); + submitStrategy.setType(SubmitStrategyType.SEQUENTIAL_BY_ORIGINATOR); + queue.setSubmitStrategy(submitStrategy); + ProcessingStrategy processingStrategy = new ProcessingStrategy(); + processingStrategy.setType(ProcessingStrategyType.RETRY_ALL); + processingStrategy.setRetries(3); + processingStrategy.setFailurePercentage(0.7); + processingStrategy.setPauseBetweenRetries(3); + processingStrategy.setMaxPauseBetweenRetries(5); + queue.setProcessingStrategy(processingStrategy); + + doPost("/api/queues?serviceType=" + "TB-RULE-ENGINE", queue) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString(String.format("The queue name '%s' is not allowed. This name is reserved for internal use. Please choose a different name.", DataConstants.CF_QUEUE_NAME)))); + + // create queue + Queue queue2 = new Queue(); + queue2.setName(DataConstants.CF_STATES_QUEUE_NAME); + queue2.setTopic("tb_rule_engine.calculated_fields"); + queue2.setPollInterval(25); + queue2.setPartitions(10); + queue2.setTenantId(TenantId.SYS_TENANT_ID); + queue2.setConsumerPerPartition(false); + queue2.setPackProcessingTimeout(2000); + SubmitStrategy submitStrategy2 = new SubmitStrategy(); + submitStrategy2.setType(SubmitStrategyType.SEQUENTIAL_BY_ORIGINATOR); + queue2.setSubmitStrategy(submitStrategy); + ProcessingStrategy processingStrategy2 = new ProcessingStrategy(); + processingStrategy2.setType(ProcessingStrategyType.RETRY_ALL); + processingStrategy2.setRetries(3); + processingStrategy2.setFailurePercentage(0.7); + processingStrategy2.setPauseBetweenRetries(3); + processingStrategy2.setMaxPauseBetweenRetries(5); + queue2.setProcessingStrategy(processingStrategy); + + doPost("/api/queues?serviceType=" + "TB-RULE-ENGINE", queue2) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString(String.format("The queue name '%s' is not allowed. This name is reserved for internal use. Please choose a different name.", DataConstants.CF_STATES_QUEUE_NAME)))); + } + private Queue saveQueue(Queue queue) { return doPost("/api/queues?serviceType=TB_RULE_ENGINE", queue, Queue.class); } diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java index 04d71b890f..2a6c847591 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java @@ -231,16 +231,19 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { credentials.getCredentialsId())); JsonNode linuxCoapCommands = commands.get(COAP); - assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST coap://localhost:5683/api/v1/%s/telemetry " + - "-t json -e \"{temperature:25}\"", credentials.getCredentialsId())); - assertThat(linuxCoapCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST coaps://localhost:5684/api/v1/%s/telemetry" + - " -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST " + + "-t \"application/json\" -e \"{temperature:25}\" coap://localhost:5683/api/v1/%s/telemetry", credentials.getCredentialsId())); + + assertThat(linuxCoapCommands.get(COAPS).get(1).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST " + + "-R " + CA_ROOT_CERT_PEM + " -t \"application/json\" -e \"{temperature:25}\" coaps://localhost:5684/api/v1/%s/telemetry", credentials.getCredentialsId())); JsonNode dockerCoapCommands = commands.get(COAP).get(DOCKER); assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway" + - " thingsboard/coap-clients coap-client -v 6 -m POST coap://host.docker.internal:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + " thingsboard/coap-clients coap-client -v 6 -m POST -t \"application/json\" -e \"{temperature:25}\" coap://host.docker.internal:5683/api/v1/%s/telemetry", credentials.getCredentialsId())); assertThat(dockerCoapCommands.get(COAPS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway" + - " thingsboard/coap-clients coap-client-openssl -v 6 -m POST coaps://host.docker.internal:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + " thingsboard/coap-clients " + + "/bin/sh -c \"curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/coaps/certificate/download && " + + "coap-client-openssl -v 6 -m POST -R " + CA_ROOT_CERT_PEM + " -t \"application/json\" -e \"{temperature:25}\" coaps://host.docker.internal:5684/api/v1/%s/telemetry\"", credentials.getCredentialsId())); } @Test @@ -376,16 +379,19 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { credentials.getCredentialsId())); JsonNode linuxCoapCommands = commands.get(COAP); - assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST coap://[::1]:5683/api/v1/%s/telemetry " + - "-t json -e \"{temperature:25}\"", credentials.getCredentialsId())); - assertThat(linuxCoapCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST coaps://[::1]:5684/api/v1/%s/telemetry" + - " -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST " + + "-t \"application/json\" -e \"{temperature:25}\" coap://[::1]:5683/api/v1/%s/telemetry", credentials.getCredentialsId())); + assertThat(linuxCoapCommands.get(COAPS).get(0).asText()).isEqualTo("curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/coaps/certificate/download"); + assertThat(linuxCoapCommands.get(COAPS).get(1).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST " + + "-R " + CA_ROOT_CERT_PEM + " -t \"application/json\" -e \"{temperature:25}\" coaps://[::1]:5684/api/v1/%s/telemetry", credentials.getCredentialsId())); JsonNode dockerCoapCommands = commands.get(COAP).get(DOCKER); assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway" + - " thingsboard/coap-clients coap-client -v 6 -m POST coap://host.docker.internal:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + " thingsboard/coap-clients coap-client -v 6 -m POST -t \"application/json\" -e \"{temperature:25}\" coap://host.docker.internal:5683/api/v1/%s/telemetry", credentials.getCredentialsId())); assertThat(dockerCoapCommands.get(COAPS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway" + - " thingsboard/coap-clients coap-client-openssl -v 6 -m POST coaps://host.docker.internal:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + " thingsboard/coap-clients " + + "/bin/sh -c \"curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/coaps/certificate/download && " + + "coap-client-openssl -v 6 -m POST -R " + CA_ROOT_CERT_PEM + " -t \"application/json\" -e \"{temperature:25}\" coaps://host.docker.internal:5684/api/v1/%s/telemetry\"", credentials.getCredentialsId())); } @Test @@ -430,16 +436,19 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { credentials.getCredentialsId())); JsonNode linuxCoapCommands = commands.get(COAP); - assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST coap://[1:1:1:1:1:1:1:1]:5683/api/v1/%s/telemetry " + - "-t json -e \"{temperature:25}\"", credentials.getCredentialsId())); - assertThat(linuxCoapCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST coaps://[1:1:1:1:1:1:1:1]:5684/api/v1/%s/telemetry" + - " -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST " + + "-t \"application/json\" -e \"{temperature:25}\" coap://[1:1:1:1:1:1:1:1]:5683/api/v1/%s/telemetry", credentials.getCredentialsId())); + assertThat(linuxCoapCommands.get(COAPS).get(0).asText()).isEqualTo("curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/coaps/certificate/download"); + assertThat(linuxCoapCommands.get(COAPS).get(1).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST -R " + CA_ROOT_CERT_PEM + + " -t \"application/json\" -e \"{temperature:25}\" coaps://[1:1:1:1:1:1:1:1]:5684/api/v1/%s/telemetry", credentials.getCredentialsId())); JsonNode dockerCoapCommands = commands.get(COAP).get(DOCKER); assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it" + - " thingsboard/coap-clients coap-client -v 6 -m POST coap://[1:1:1:1:1:1:1:1]:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + " thingsboard/coap-clients coap-client -v 6 -m POST -t \"application/json\" -e \"{temperature:25}\" coap://[1:1:1:1:1:1:1:1]:5683/api/v1/%s/telemetry", credentials.getCredentialsId())); assertThat(dockerCoapCommands.get(COAPS).asText()).isEqualTo(String.format("docker run --rm -it" + - " thingsboard/coap-clients coap-client-openssl -v 6 -m POST coaps://[1:1:1:1:1:1:1:1]:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + " thingsboard/coap-clients " + + "/bin/sh -c \"curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/coaps/certificate/download && " + + "coap-client-openssl -v 6 -m POST -R " + CA_ROOT_CERT_PEM + " -t \"application/json\" -e \"{temperature:25}\" coaps://[1:1:1:1:1:1:1:1]:5684/api/v1/%s/telemetry\"", credentials.getCredentialsId())); } @@ -552,9 +561,10 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { assertThat(commands).hasSize(1); JsonNode linuxCommands = commands.get(COAP); - assertThat(linuxCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST coap://localhost:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", + assertThat(linuxCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST -t \"application/json\" -e \"{temperature:25}\" coap://localhost:5683/api/v1/%s/telemetry", credentials.getCredentialsId())); - assertThat(linuxCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST coaps://localhost:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", + assertThat(linuxCommands.get(COAPS).get(0).asText()).isEqualTo("curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/coaps/certificate/download"); + assertThat(linuxCommands.get(COAPS).get(1).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST -R " + CA_ROOT_CERT_PEM + " -t \"application/json\" -e \"{temperature:25}\" coaps://localhost:5684/api/v1/%s/telemetry", credentials.getCredentialsId())); } @@ -772,16 +782,18 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { "mosquitto_pub -d -q 1 --cafile " + CA_ROOT_CERT_PEM + " -h host.docker.internal -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"", credentials.getCredentialsId())); JsonNode linuxCoapCommands = commands.get(COAP); - assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST coap://localhost/api/v1/%s/telemetry " + - "-t json -e \"{temperature:25}\"", credentials.getCredentialsId())); - assertThat(linuxCoapCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST coaps://localhost/api/v1/%s/telemetry" + - " -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST " + + "-t \"application/json\" -e \"{temperature:25}\" coap://localhost/api/v1/%s/telemetry", credentials.getCredentialsId())); + assertThat(linuxCoapCommands.get(COAPS).get(0).asText()).isEqualTo("curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/coaps/certificate/download"); + assertThat(linuxCoapCommands.get(COAPS).get(1).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST " + + "-R " + CA_ROOT_CERT_PEM + " -t \"application/json\" -e \"{temperature:25}\" coaps://localhost/api/v1/%s/telemetry", credentials.getCredentialsId())); JsonNode dockerCoapCommands = commands.get(COAP).get(DOCKER); assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway" + - " thingsboard/coap-clients coap-client -v 6 -m POST coap://host.docker.internal/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + " thingsboard/coap-clients coap-client -v 6 -m POST -t \"application/json\" -e \"{temperature:25}\" coap://host.docker.internal/api/v1/%s/telemetry", credentials.getCredentialsId())); assertThat(dockerCoapCommands.get(COAPS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway" + - " thingsboard/coap-clients coap-client-openssl -v 6 -m POST coaps://host.docker.internal/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + " thingsboard/coap-clients /bin/sh -c \"curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/coaps/certificate/download && " + + "coap-client-openssl -v 6 -m POST -R " + CA_ROOT_CERT_PEM + " -t \"application/json\" -e \"{temperature:25}\" coaps://host.docker.internal/api/v1/%s/telemetry\"", credentials.getCredentialsId())); } @Test @@ -831,16 +843,18 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { credentials.getCredentialsId())); JsonNode linuxCoapCommands = commands.get(COAP); - assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST coap://test.domain:5683/api/v1/%s/telemetry " + - "-t json -e \"{temperature:25}\"", credentials.getCredentialsId())); - assertThat(linuxCoapCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST coaps://test.domain:5684/api/v1/%s/telemetry" + - " -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST " + + "-t \"application/json\" -e \"{temperature:25}\" coap://test.domain:5683/api/v1/%s/telemetry", credentials.getCredentialsId())); + assertThat(linuxCoapCommands.get(COAPS).get(0).asText()).isEqualTo("curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/coaps/certificate/download"); + assertThat(linuxCoapCommands.get(COAPS).get(1).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST " + + "-R "+ CA_ROOT_CERT_PEM + " -t \"application/json\" -e \"{temperature:25}\" coaps://test.domain:5684/api/v1/%s/telemetry", credentials.getCredentialsId())); JsonNode dockerCoapCommands = commands.get(COAP).get(DOCKER); assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it " + - "thingsboard/coap-clients coap-client -v 6 -m POST coap://test.domain:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + "thingsboard/coap-clients coap-client -v 6 -m POST -t \"application/json\" -e \"{temperature:25}\" coap://test.domain:5683/api/v1/%s/telemetry", credentials.getCredentialsId())); assertThat(dockerCoapCommands.get(COAPS).asText()).isEqualTo(String.format("docker run --rm -it " + - "thingsboard/coap-clients coap-client-openssl -v 6 -m POST coaps://test.domain:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + "thingsboard/coap-clients /bin/sh -c \"curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/coaps/certificate/download && " + + "coap-client-openssl -v 6 -m POST -R " + CA_ROOT_CERT_PEM + " -t \"application/json\" -e \"{temperature:25}\" coaps://test.domain:5684/api/v1/%s/telemetry\"", credentials.getCredentialsId())); } @Test @@ -917,12 +931,17 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients /bin/sh -c \"curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/mqtts/certificate/download && mosquitto_pub -d -q 1 --cafile " + CA_ROOT_CERT_PEM + " -h host.docker.internal -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"", credentials.getCredentialsId())); JsonNode coapCommands = commands.get(COAP); - assertThat(coapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST coap://localhost/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); - assertThat(coapCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST coaps://localhost/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + assertThat(coapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST -t \"application/json\" -e \"{temperature:25}\" coap://localhost/api/v1/%s/telemetry", credentials.getCredentialsId())); + assertThat(coapCommands.get(COAPS).get(0).asText()).isEqualTo("curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/coaps/certificate/download"); + assertThat(coapCommands.get(COAPS).get(1).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST " + + "-R " + CA_ROOT_CERT_PEM + " -t \"application/json\" -e \"{temperature:25}\" coaps://localhost/api/v1/%s/telemetry", credentials.getCredentialsId())); JsonNode dockerCoapCommands = coapCommands.get(DOCKER); - assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/coap-clients coap-client -v 6 -m POST coap://host.docker.internal/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); - assertThat(dockerCoapCommands.get(COAPS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/coap-clients coap-client-openssl -v 6 -m POST coaps://host.docker.internal/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway " + + "thingsboard/coap-clients coap-client -v 6 -m POST -t \"application/json\" -e \"{temperature:25}\" coap://host.docker.internal/api/v1/%s/telemetry", credentials.getCredentialsId())); + assertThat(dockerCoapCommands.get(COAPS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway " + + "thingsboard/coap-clients /bin/sh -c \"curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/coaps/certificate/download && " + + "coap-client-openssl -v 6 -m POST -R " + CA_ROOT_CERT_PEM + " -t \"application/json\" -e \"{temperature:25}\" coaps://host.docker.internal/api/v1/%s/telemetry\"", credentials.getCredentialsId())); } diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java new file mode 100644 index 0000000000..91be3f4744 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java @@ -0,0 +1,130 @@ +/** + * 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.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.edqs.EdqsSyncRequest; +import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.DeviceTypeFilter; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; + +@DaoSqlTest +@TestPropertySource(properties = { + "queue.edqs.sync.enabled=true", + "queue.edqs.api.supported=true", + "queue.edqs.api.auto_enable=true", + "queue.edqs.mode=local" +}) +public class EdqsControllerTest extends AbstractControllerTest { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Before + public void beforeEdqsControllerTest() throws Exception { + loginTenantAdmin(); + } + + @Test + public void testEdqsSync() throws Exception { + List devices = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + Device device = new Device(); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + ObjectNode additionalInfo = JacksonUtil.newObjectNode(); + additionalInfo.put("gateway", true); + device.setAdditionalInfo(additionalInfo); + devices.add(doPost("/api/device", device, Device.class)); + Thread.sleep(1); + } + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("default")); + filter.setDeviceNameFilter(""); + + List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null); + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, Collections.singletonList(getGatewayFilter())); + await().atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { + }), result -> result.getTotalElements() == 3); + + // update db + Device device1 = devices.get(0); + device1.setAdditionalInfo(JacksonUtil.newObjectNode()); + jdbcTemplate.execute("update device set additional_info = '{}' where id = '" + device1.getId().getId().toString() + "'"); + + // do edqs sync + loginSysAdmin(); + ToCoreEdqsRequest syncRequest = new ToCoreEdqsRequest(new EdqsSyncRequest(), null); + doPost("/api/edqs/system/request", syncRequest); + + //check sync is finished + await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> { + Optional attribute = attributesService.find(TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID, AttributeScope.SERVER_SCOPE, "edqsSyncState").get(); + return attribute.isPresent() && attribute.get().getJsonValue().isPresent() && + attribute.get().getJsonValue().get().contains("\"status\":\"FINISHED\""); + }); + + // check if the count is updated + loginTenantAdmin(); + await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { + }), result -> result.getTotalElements() == 2); + } + + private KeyFilter getGatewayFilter() { + KeyFilter additionalInfoFilter = new KeyFilter(); + additionalInfoFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "additionalInfo")); + additionalInfoFilter.setValueType(EntityKeyValueType.STRING); + StringFilterPredicate predicate = new StringFilterPredicate(); + predicate.setValue(FilterPredicateValue.fromString("\"gateway\":true")); + predicate.setOperation(StringFilterPredicate.StringOperation.CONTAINS); + additionalInfoFilter.setPredicate(predicate); + return additionalInfoFilter; + } +} diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index ce5221ab89..153ec2d26f 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -35,7 +35,7 @@ import static org.awaitility.Awaitility.await; @DaoSqlTest @TestPropertySource(properties = { // "queue.type=kafka", // uncomment to use Kafka -// "queue.kafka.bootstrap.servers=10.7.1.254:9092", +// "queue.kafka.bootstrap.servers=10.7.2.107:9092", "queue.edqs.sync.enabled=true", "queue.edqs.api.supported=true", "queue.edqs.api.auto_enable=true", diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java index 3f2434c4ec..4fd613dc1d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java @@ -664,34 +664,38 @@ public class TenantControllerTest extends AbstractControllerTest { savedDifferentTenant.setTenantProfileId(tenantProfile.getId()); savedDifferentTenant = saveTenant(savedDifferentTenant); TenantId tenantId = differentTenantId; - await().atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> { - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId); - return !tpi.getTenantId().get().isSysTenantId(); - }); - TopicPartitionInfo tpi = new TopicPartitionInfo(MAIN_QUEUE_TOPIC, tenantId, 0, false); - String isolatedTopic = tpi.getFullTopicName(); - TbMsg expectedMsg = publishTbMsg(tenantId, tpi); + List isolatedTpis = await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> { + List newTpis = new ArrayList<>(); + newTpis.add(partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId)); + newTpis.add(partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, tenantId)); + return newTpis; + }, newTpis -> newTpis.stream().allMatch(newTpi -> newTpi.getTenantId().get().equals(tenantId))); + TbMsg expectedMsg = publishTbMsg(tenantId, isolatedTpis.get(0)); awaitTbMsg(tbMsg -> tbMsg.getId().equals(expectedMsg.getId()), 10000); // to wait for consumer start loginSysAdmin(); tenantProfile.setIsolatedTbRuleEngine(false); tenantProfile.getProfileData().setQueueConfiguration(Collections.emptyList()); tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); - await().atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId) - .getTenantId().get().isSysTenantId()); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + TopicPartitionInfo newTpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId); + assertThat(newTpi.getTenantId()).hasValue(TenantId.SYS_TENANT_ID); + newTpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, tenantId); + assertThat(newTpi.getTenantId()).hasValue(TenantId.SYS_TENANT_ID); + }); List submittedMsgs = new ArrayList<>(); long timeLeft = TimeUnit.SECONDS.toMillis(7); // based on topic-deletion-delay int msgs = 100; for (int i = 1; i <= msgs; i++) { - TbMsg tbMsg = publishTbMsg(tenantId, tpi); + TbMsg tbMsg = publishTbMsg(tenantId, isolatedTpis.get(0)); submittedMsgs.add(tbMsg.getId()); Thread.sleep(timeLeft / msgs); } await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { - verify(queueAdmin, times(1)).deleteTopic(eq(isolatedTopic)); + TopicPartitionInfo tpi = isolatedTpis.get(0); + // we only expect deletion of Rule Engine topic. for CF - the topic is left as is because queue draining is not supported + verify(queueAdmin, times(1)).deleteTopic(eq(tpi.getFullTopicName())); }); await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { @@ -719,12 +723,16 @@ public class TenantControllerTest extends AbstractControllerTest { savedDifferentTenant.setTenantProfileId(tenantProfile.getId()); savedDifferentTenant = saveTenant(savedDifferentTenant); TenantId tenantId = differentTenantId; - await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { - assertThat(partitionService.getMyPartitions(new QueueKey(ServiceType.TB_RULE_ENGINE, tenantId))).isNotNull(); - }); - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tenantId); - assertThat(tpi.getTenantId()).hasValue(tenantId); - TbMsg tbMsg = publishTbMsg(tenantId, tpi); + List isolatedTpis = await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> { + List newTpis = new ArrayList<>(); + newTpis.add(partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId)); + newTpis.add(partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, tenantId)); + return newTpis; + }, newTpis -> newTpis.stream().allMatch(newTpi -> { + return newTpi.getTenantId().get().equals(tenantId) && + newTpi.isMyPartition(); + })); + TbMsg tbMsg = publishTbMsg(tenantId, isolatedTpis.get(0)); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { verify(actorContext).tell(argThat(msg -> { return msg instanceof QueueToRuleEngineMsg && ((QueueToRuleEngineMsg) msg).getMsg().getId().equals(tbMsg.getId()); @@ -738,7 +746,9 @@ public class TenantControllerTest extends AbstractControllerTest { assertThatThrownBy(() -> partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tenantId)) .isInstanceOf(TenantNotFoundException.class); - verify(queueAdmin).deleteTopic(eq(tpi.getFullTopicName())); + isolatedTpis.forEach(tpi -> { + verify(queueAdmin).deleteTopic(eq(tpi.getFullTopicName())); + }); }); } diff --git a/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java index 71fe987813..4df305b2c5 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java @@ -121,14 +121,14 @@ public class AssetProfileEdgeTest extends AbstractEdgeTest { Assert.assertNull(assetProfile.getDefaultRuleChainId()); Assert.assertEquals(edgeRuleChainId, assetProfile.getDefaultEdgeRuleChainId()); - // delete profile - edgeImitator.expectMessageAmount(1); + // delete profile and delete relation messages + edgeImitator.expectMessageAmount(2); doDelete("/api/assetProfile/" + assetProfile.getUuidId()) .andExpect(status().isOk()); Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof AssetProfileUpdateMsg); - AssetProfileUpdateMsg assetProfileUpdateMsg = (AssetProfileUpdateMsg) latestMessage; + Optional assetDeleteMsgOpt = edgeImitator.findMessageByType(AssetProfileUpdateMsg.class); + Assert.assertTrue(assetDeleteMsgOpt.isPresent()); + AssetProfileUpdateMsg assetProfileUpdateMsg = assetDeleteMsgOpt.get(); Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, assetProfileUpdateMsg.getMsgType()); Assert.assertEquals(assetProfile.getUuidId().getMostSignificantBits(), assetProfileUpdateMsg.getIdMSB()); Assert.assertEquals(assetProfile.getUuidId().getLeastSignificantBits(), assetProfileUpdateMsg.getIdLSB()); diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java index f1c4f31f65..819dec5c0d 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java @@ -327,14 +327,14 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { Assert.assertNotNull(deviceProfile); Assert.assertEquals("Device Profile On Edge", deviceProfile.getName()); - // delete profile - edgeImitator.expectMessageAmount(1); + // delete profile and delete relation messages + edgeImitator.expectMessageAmount(2); doDelete("/api/deviceProfile/" + deviceProfile.getUuidId()) .andExpect(status().isOk()); Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg); - DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; + Optional deviceDeleteMsgOpt = edgeImitator.findMessageByType(DeviceProfileUpdateMsg.class); + Assert.assertTrue(deviceDeleteMsgOpt.isPresent()); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = deviceDeleteMsgOpt.get(); Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); Assert.assertEquals(deviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB()); Assert.assertEquals(deviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB()); diff --git a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java index 4a303e5253..dace159774 100644 --- a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java @@ -428,7 +428,6 @@ public class HashPartitionServiceTest { ReflectionTestUtils.setField(partitionService, "corePartitions", 10); ReflectionTestUtils.setField(partitionService, "cfEventTopic", "tb_cf_event"); ReflectionTestUtils.setField(partitionService, "cfStateTopic", "tb_cf_state"); - ReflectionTestUtils.setField(partitionService, "cfPartitions", 10); ReflectionTestUtils.setField(partitionService, "vcTopic", "tb.vc"); ReflectionTestUtils.setField(partitionService, "vcPartitions", 10); ReflectionTestUtils.setField(partitionService, "hashFunctionName", hashFunctionName); 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 18687cedba..1e7f5384b5 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 @@ -282,6 +282,10 @@ public class EntityServiceTest extends AbstractControllerTest { data.forEach(entityData -> Assert.assertNotNull(entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("phone"))); countByQueryAndCheck(query, 5); + + // delete user + userService.deleteUser(tenantId, users.get(0)); + countByQueryAndCheck(query, 4); } private void createTestUserRelations(TenantId tenantId, List users) { diff --git a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java index 64845a12cd..2b4d9f38e5 100644 --- a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java @@ -359,6 +359,45 @@ class DefaultTelemetrySubscriptionServiceTest { then(subscriptionManagerService).shouldHaveNoInteractions(); } + @Test + void shouldNotCopyLatestToEntityViewWhenTimeseriesSaveFailedOnMainEntity() { + // GIVEN + var entityView = new EntityView(new EntityViewId(UUID.randomUUID())); + entityView.setTenantId(tenantId); + entityView.setCustomerId(customerId); + entityView.setEntityId(entityId); + entityView.setKeys(new TelemetryEntityView(sampleTimeseries.stream().map(KvEntry::getKey).toList(), new AttributesEntityView())); + + // mock that there is one entity view + lenient().when(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).thenReturn(immediateFuture(List.of(entityView))); + // mock that save latest call for entity view is successful + lenient().when(tsService.saveLatest(tenantId, entityView.getId(), sampleTimeseries)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size())))); + // mock TPI for entity view + lenient().when(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityView.getId())).thenReturn(tpi); + + var request = TimeseriesSaveRequest.builder() + .tenantId(tenantId) + .customerId(customerId) + .entityId(entityId) + .entries(sampleTimeseries) + .ttl(sampleTtl) + .strategy(new TimeseriesSaveRequest.Strategy(true, true, false, false)) + .build(); + + given(tsService.save(tenantId, entityId, sampleTimeseries, sampleTtl)).willReturn(immediateFailedFuture(new RuntimeException("failed to save data on main entity"))); + + // WHEN + telemetryService.saveTimeseries(request); + + // THEN + // should save only time series for the main entity + then(tsService).should().save(tenantId, entityId, sampleTimeseries, sampleTtl); + then(tsService).shouldHaveNoMoreInteractions(); + + // should not send any WS updates + then(subscriptionManagerService).shouldHaveNoInteractions(); + } + @ParameterizedTest @MethodSource("allCombinationsOfFourBooleans") void shouldCallCorrectSaveTimeseriesApiBasedOnBooleanFlagsInTheSaveRequest(boolean saveTimeseries, boolean saveLatest, boolean sendWsUpdate, boolean processCalculatedFields) { diff --git a/common/actor/pom.xml b/common/actor/pom.xml index 71b7e2eb6e..76d000724b 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC common org.thingsboard.common diff --git a/common/cache/pom.xml b/common/cache/pom.xml index 27a536dc28..3d549da8e6 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC common org.thingsboard.common diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index adc77dce22..722645a0da 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC common org.thingsboard.common diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java index 2a2d04e0fd..aed6eb4cf5 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java @@ -85,8 +85,6 @@ public interface TbClusterService extends TbQueueClusterService { void pushMsgToCalculatedFields(TopicPartitionInfo tpi, UUID msgId, ToCalculatedFieldMsg msg, TbQueueCallback callback); - void pushNotificationToCalculatedFields(TenantId tenantId, EntityId entityId, TransportProtos.ToCalculatedFieldNotificationMsg msg, TbQueueCallback callback); - void broadcastEntityStateChangeEvent(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state); void onDeviceProfileChange(DeviceProfile deviceProfile, DeviceProfile oldDeviceProfile, TbQueueCallback callback); diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml index 3f020b289a..ffe1811b62 100644 --- a/common/coap-server/pom.xml +++ b/common/coap-server/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC common org.thingsboard.common diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index b1b55a9461..4a11a8a574 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC common org.thingsboard.common diff --git a/common/data/pom.xml b/common/data/pom.xml index 5895974d82..2ece9779b5 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC common org.thingsboard.common diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 4ea66be1b4..93e754eb2c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -86,4 +86,16 @@ public enum EntityType { this.tableName = tableName; } + public boolean isOneOf(EntityType... types) { + if (types == null) { + return false; + } + for (EntityType type : types) { + if (this == type) { + return true; + } + } + return false; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java index 532c4a92ac..1b0975542c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java @@ -148,6 +148,7 @@ public interface EntityFields { default String getAsString(String key) { return switch (key) { case "createdTime" -> Long.toString(getCreatedTime()); + case "title" -> getName(); case "type" -> getType(); case "label" -> getLabel(); case "additionalInfo" -> getAdditionalInfo(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityDataInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityDataInfo.java index 0b8f392472..d55a575a75 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityDataInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityDataInfo.java @@ -26,4 +26,5 @@ public class EntityDataInfo { boolean hasRelations; boolean hasAttributes; boolean hasCredentials; + boolean hasCalculatedFields; } diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml index f992486149..096dd911be 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC common org.thingsboard.common diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml index f7106e56fd..4667b5cbe6 100644 --- a/common/edqs/pom.xml +++ b/common/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC common org.thingsboard.common diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/ApiUsageStateData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/ApiUsageStateData.java index f7cd51fc38..8f0a865744 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/ApiUsageStateData.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/ApiUsageStateData.java @@ -35,12 +35,12 @@ public class ApiUsageStateData extends BaseEntityData { @Override public String getEntityName() { - return getEntityOwnerName(); + return getOwnerName(); } @Override - public String getEntityOwnerName() { - return repo.getOwnerName(fields.getEntityId()); + public String getOwnerName() { + return repo.getOwnerEntityName(fields.getEntityId()); } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java index 10ee17fc75..33f32b9781 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java @@ -98,8 +98,13 @@ public abstract class BaseEntityData implements EntityDa } @Override - public EntityType getOwnerType() { - return customerId != null ? EntityType.CUSTOMER : EntityType.TENANT; + public String getOwnerName() { + return repo.getOwnerEntityName(isTenantEntity() ? repo.getTenantId() : new CustomerId(getCustomerId())); + } + + @Override + public String getOwnerType() { + return isTenantEntity() ? EntityType.TENANT.name() : EntityType.CUSTOMER.name(); } @Override @@ -132,22 +137,21 @@ public abstract class BaseEntityData implements EntityDa } return switch (name) { case "name" -> getEntityName(); - case "ownerName" -> getEntityOwnerName(); - case "ownerType" -> customerId != null ? EntityType.CUSTOMER.name() : EntityType.TENANT.name(); + case "ownerName" -> getOwnerName(); + case "ownerType" -> getOwnerType(); case "entityType" -> Optional.ofNullable(getEntityType()).map(EntityType::name).orElse(""); default -> fields.getAsString(name); }; } - public String getEntityOwnerName() { - return repo.getOwnerName(getCustomerId() == null || CustomerId.NULL_UUID.equals(getCustomerId()) ? null : - new CustomerId(getCustomerId())); - } - public String getEntityName() { return getFields().getName(); } + private boolean isTenantEntity() { + return getCustomerId() == null || CustomerId.NULL_UUID.equals(getCustomerId()); + } + private String getRelatedParentId(QueryContext ctx) { return Optional.ofNullable(ctx.getRelatedParentIdMap().get(getId())) .map(UUID::toString) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityData.java index 53ee73f638..31ba7b7134 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityData.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/EntityData.java @@ -54,7 +54,9 @@ public interface EntityData { boolean removeTs(Integer keyId); - EntityType getOwnerType(); + String getOwnerName(); + + String getOwnerType(); DataPoint getDataPoint(DataKey key, QueryContext queryContext); 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 c5696c08f7..7ddc9147df 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 @@ -141,6 +141,7 @@ public class EdqsProcessor implements TbQueueHandler, consumer.commit(); }) .consumerCreator((config, partitionId) -> queueFactory.createEdqsMsgConsumer(EdqsQueue.EVENTS)) + .queueAdmin(queueFactory.getEdqsQueueAdmin()) .consumerExecutor(consumersExecutor) .taskExecutor(taskExecutor) .scheduler(scheduler) 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 59255b11cc..870574a786 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 @@ -421,20 +421,13 @@ public class TenantRepo { return relations.computeIfAbsent(relationTypeGroup, type -> new RelationsRepo()); } - public String getOwnerName(EntityId ownerId) { - if (ownerId == null || (EntityType.CUSTOMER.equals(ownerId.getEntityType()) && CustomerId.NULL_UUID.equals(ownerId.getId()))) { - ownerId = tenantId; - } - return getEntityName(ownerId); - } - - private String getEntityName(EntityId entityId) { + public String getOwnerEntityName(EntityId entityId) { EntityType entityType = entityId.getEntityType(); - if (entityType == EntityType.TENANT && entityId.getId().equals(TenantId.NULL_UUID)) { - return ""; - } return switch (entityType) { - case CUSTOMER, TENANT -> getEntityMap(entityType).get(entityId.getId()).getFields().getName(); + case CUSTOMER, TENANT -> { + EntityFields fields = getEntityMap(entityType).get(entityId.getId()).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 80b6eebf5c..85b8e92387 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 @@ -33,7 +33,8 @@ 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.common.consumer.QueueConsumerManager; -import org.thingsboard.server.queue.common.consumer.QueueStateService; +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; @@ -89,13 +90,13 @@ public class KafkaEdqsStateService implements EdqsStateService { consumer.commit(); }) .consumerCreator((config, partitionId) -> queueFactory.createEdqsMsgConsumer(EdqsQueue.STATE)) + .queueAdmin(queueFactory.getEdqsQueueAdmin()) .consumerExecutor(eventConsumer.getConsumerExecutor()) .taskExecutor(eventConsumer.getTaskExecutor()) .scheduler(eventConsumer.getScheduler()) .uncaughtErrorHandler(edqsProcessor.getErrorHandler()) .build(); - queueStateService = new QueueStateService<>(); - queueStateService.init(stateConsumer, eventConsumer); + queueStateService = new KafkaQueueStateService<>(eventConsumer, stateConsumer); eventsToBackupConsumer = QueueConsumerManager.>builder() .name("edqs-events-to-backup-consumer") @@ -149,11 +150,11 @@ public class KafkaEdqsStateService implements EdqsStateService { @Override public void process(Set partitions) { - if (queueStateService.getPartitions() == null) { + if (queueStateService.getPartitions().isEmpty()) { eventsToBackupConsumer.subscribe(); eventsToBackupConsumer.launch(); } - queueStateService.update(partitions); + queueStateService.update(new QueueKey(ServiceType.EDQS), partitions); } @Override 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 970f8585dd..3bf12752a0 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("")) + public static final Comparator SORT_ASC = Comparator.comparing((SortableEntityData sed) -> Optional.ofNullable(sed.getSortValue()).orElse(""), String.CASE_INSENSITIVE_ORDER) .thenComparing(sp -> sp.getId().toString()); - public static final Comparator SORT_DESC = Comparator.comparing((SortableEntityData sed) -> Optional.ofNullable(sed.getSortValue()).orElse("")) + public static final Comparator SORT_DESC = Comparator.comparing((SortableEntityData sed) -> Optional.ofNullable(sed.getSortValue()).orElse(""), String.CASE_INSENSITIVE_ORDER) .thenComparing(sp -> sp.getId().toString()).reversed(); public static EntityType resolveEntityType(EntityFilter entityFilter) { diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/VersionsStore.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/VersionsStore.java index e52b1bbac9..798ac0603d 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/VersionsStore.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/VersionsStore.java @@ -26,19 +26,17 @@ import java.util.concurrent.atomic.AtomicBoolean; public class VersionsStore { private final Cache versions = Caffeine.newBuilder() - .expireAfterWrite(1, TimeUnit.HOURS) + .expireAfterWrite(24, TimeUnit.HOURS) .build(); public boolean isNew(String key, Long version) { AtomicBoolean isNew = new AtomicBoolean(false); versions.asMap().compute(key, (k, prevVersion) -> { - if (prevVersion == null || prevVersion < version) { + if (prevVersion == null || prevVersion <= version) { isNew.set(true); return version; } else { - if (version < prevVersion) { - log.info("[{}] Version {} is outdated, the latest is {}", key, version, prevVersion); - } + log.info("[{}] Version {} is outdated, the latest is {}", key, version, prevVersion); return prevVersion; } }); diff --git a/common/message/pom.xml b/common/message/pom.xml index 380621a5ad..d8127f6327 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC common org.thingsboard.common diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldEntityLifecycleMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldEntityLifecycleMsg.java index 099240f54d..1eb8f425ab 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldEntityLifecycleMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldEntityLifecycleMsg.java @@ -16,19 +16,16 @@ package org.thingsboard.server.common.msg.cf; import lombok.Data; -import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; -import org.thingsboard.server.common.msg.queue.TbCallback; @Data public class CalculatedFieldEntityLifecycleMsg implements ToCalculatedFieldSystemMsg { private final TenantId tenantId; private final ComponentLifecycleMsg data; - private final TbCallback callback; @Override public MsgType getMsgType() { diff --git a/common/pom.xml b/common/pom.xml index 9a7a836ba5..aecd6ba841 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC thingsboard common diff --git a/common/proto/pom.xml b/common/proto/pom.xml index bcfb3dfc85..712807dddb 100644 --- a/common/proto/pom.xml +++ b/common/proto/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC common org.thingsboard.common diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index f1da00d943..1ebd753f3c 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageStateValue; import org.thingsboard.server.common.data.Device; @@ -93,6 +94,7 @@ import org.thingsboard.server.common.msg.rule.engine.DeviceEdgeUpdateMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceNameOrTypeUpdateMsg; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.KeyValueProto; +import org.thingsboard.server.gen.transport.TransportProtos.ApiUsageRecordKeyProto; import java.util.ArrayList; import java.util.Arrays; @@ -432,6 +434,39 @@ public class ProtoUtils { return builder.build(); } + public static ApiUsageRecordKeyProto toProto(ApiUsageRecordKey apiUsageRecordKey) { + return switch (apiUsageRecordKey) { + case TRANSPORT_MSG_COUNT -> ApiUsageRecordKeyProto.TRANSPORT_MSG_COUNT; + case TRANSPORT_DP_COUNT -> ApiUsageRecordKeyProto.TRANSPORT_DP_COUNT; + case STORAGE_DP_COUNT -> ApiUsageRecordKeyProto.STORAGE_DP_COUNT; + case RE_EXEC_COUNT -> ApiUsageRecordKeyProto.RE_EXEC_COUNT; + case JS_EXEC_COUNT -> ApiUsageRecordKeyProto.JS_EXEC_COUNT; + case TBEL_EXEC_COUNT -> ApiUsageRecordKeyProto.TBEL_EXEC_COUNT; + case EMAIL_EXEC_COUNT -> ApiUsageRecordKeyProto.EMAIL_EXEC_COUNT; + case SMS_EXEC_COUNT -> ApiUsageRecordKeyProto.SMS_EXEC_COUNT; + case CREATED_ALARMS_COUNT -> ApiUsageRecordKeyProto.CREATED_ALARMS_COUNT; + case ACTIVE_DEVICES -> ApiUsageRecordKeyProto.ACTIVE_DEVICES; + case INACTIVE_DEVICES -> ApiUsageRecordKeyProto.INACTIVE_DEVICES; + }; + } + + public static ApiUsageRecordKey fromProto(ApiUsageRecordKeyProto proto) { + return switch (proto) { + case UNRECOGNIZED -> null; + case TRANSPORT_MSG_COUNT -> ApiUsageRecordKey.TRANSPORT_MSG_COUNT; + case TRANSPORT_DP_COUNT -> ApiUsageRecordKey.TRANSPORT_DP_COUNT; + case STORAGE_DP_COUNT -> ApiUsageRecordKey.STORAGE_DP_COUNT; + case RE_EXEC_COUNT -> ApiUsageRecordKey.RE_EXEC_COUNT; + case JS_EXEC_COUNT -> ApiUsageRecordKey.JS_EXEC_COUNT; + case TBEL_EXEC_COUNT -> ApiUsageRecordKey.TBEL_EXEC_COUNT; + case EMAIL_EXEC_COUNT -> ApiUsageRecordKey.EMAIL_EXEC_COUNT; + case SMS_EXEC_COUNT -> ApiUsageRecordKey.SMS_EXEC_COUNT; + case CREATED_ALARMS_COUNT -> ApiUsageRecordKey.CREATED_ALARMS_COUNT; + case ACTIVE_DEVICES -> ApiUsageRecordKey.ACTIVE_DEVICES; + case INACTIVE_DEVICES -> ApiUsageRecordKey.INACTIVE_DEVICES; + }; + } + private static ToDeviceActorNotificationMsg fromProto(TransportProtos.DeviceAttributesEventMsgProto proto) { return new DeviceAttributesEventNotificationMsg( TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 6ecdf13981..938a1692ae 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -63,6 +63,20 @@ enum EntityTypeProto { CALCULATED_FIELD_LINK = 40; } +enum ApiUsageRecordKeyProto { + TRANSPORT_MSG_COUNT = 0; + TRANSPORT_DP_COUNT = 1; + STORAGE_DP_COUNT = 2; + RE_EXEC_COUNT = 3; + JS_EXEC_COUNT = 4; + TBEL_EXEC_COUNT = 5; + EMAIL_EXEC_COUNT = 6; + SMS_EXEC_COUNT = 7; + CREATED_ALARMS_COUNT = 8; + ACTIVE_DEVICES = 9; + INACTIVE_DEVICES = 10; +} + /** * Service Discovery Data Structures; */ @@ -1674,14 +1688,12 @@ message ToEdgeEventNotificationMsg { } message ToCalculatedFieldMsg { - ComponentLifecycleMsgProto componentLifecycleMsg = 1; - CalculatedFieldTelemetryMsgProto telemetryMsg = 2; - CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 3; + CalculatedFieldTelemetryMsgProto telemetryMsg = 1; + CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 2; } message ToCalculatedFieldNotificationMsg { - ComponentLifecycleMsgProto componentLifecycleMsg = 1; - CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 2; + CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 1; } /* Messages that are handled by ThingsBoard RuleEngine Service */ @@ -1720,12 +1732,25 @@ message ToTransportMsg { repeated QueueDeleteMsg queueDeleteMsgs = 16; } -message UsageStatsKVProto{ - string key = 1; +message UsageStatsKVProto { + string key = 1 [deprecated=true]; int64 value = 2; + ApiUsageRecordKeyProto recordKey = 3; } message ToUsageStatsServiceMsg { + int64 tenantIdMSB = 1 [deprecated=true]; + int64 tenantIdLSB = 2 [deprecated=true]; + int64 entityIdMSB = 3 [deprecated=true]; + int64 entityIdLSB = 4 [deprecated=true]; + repeated UsageStatsKVProto values = 5 [deprecated=true]; + int64 customerIdMSB = 6 [deprecated=true]; + int64 customerIdLSB = 7 [deprecated=true]; + string serviceId = 8; + repeated UsageStatsServiceMsg msgs = 9; +} + +message UsageStatsServiceMsg { int64 tenantIdMSB = 1; int64 tenantIdLSB = 2; int64 entityIdMSB = 3; @@ -1733,7 +1758,6 @@ message ToUsageStatsServiceMsg { repeated UsageStatsKVProto values = 5; int64 customerIdMSB = 6; int64 customerIdLSB = 7; - string serviceId = 8; } message ToOtaPackageStateServiceMsg { diff --git a/common/queue/pom.xml b/common/queue/pom.xml index e28ebfbcf0..0275d71b86 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC common org.thingsboard.common 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 1d68cc9c4e..f25a98adf4 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 @@ -20,9 +20,11 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.queue.QueueConfig; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueMsg; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.AddPartitionsTask; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.DeletePartitionsTask; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.RemovePartitionsTask; import org.thingsboard.server.queue.discovery.QueueKey; @@ -36,17 +38,19 @@ import java.util.function.Consumer; public class PartitionedQueueConsumerManager extends MainQueueConsumerManager { private final ConsumerPerPartitionWrapper consumerWrapper; + private final TbQueueAdmin queueAdmin; @Getter private final String topic; @Builder(builderMethodName = "create") // not to conflict with super.builder() public PartitionedQueueConsumerManager(QueueKey queueKey, String topic, long pollInterval, MsgPackProcessor msgPackProcessor, - BiFunction> consumerCreator, + BiFunction> consumerCreator, TbQueueAdmin queueAdmin, ExecutorService consumerExecutor, ScheduledExecutorService scheduler, ExecutorService taskExecutor, Consumer uncaughtErrorHandler) { super(queueKey, QueueConfig.of(true, pollInterval), msgPackProcessor, consumerCreator, consumerExecutor, scheduler, taskExecutor, uncaughtErrorHandler); this.topic = topic; this.consumerWrapper = (ConsumerPerPartitionWrapper) super.consumerWrapper; + this.queueAdmin = queueAdmin; } @Override @@ -57,6 +61,17 @@ public class PartitionedQueueConsumerManager extends MainQ } else if (task instanceof RemovePartitionsTask removePartitionsTask) { log.info("[{}] Removed partitions: {}", queueKey, removePartitionsTask.partitions()); consumerWrapper.removePartitions(removePartitionsTask.partitions()); + } else if (task instanceof DeletePartitionsTask deletePartitionsTask) { + log.info("[{}] Removing partitions and deleting topics: {}", queueKey, deletePartitionsTask.partitions()); + consumerWrapper.removePartitions(deletePartitionsTask.partitions()); + deletePartitionsTask.partitions().forEach(tpi -> { + String topic = tpi.getFullTopicName(); + try { + queueAdmin.deleteTopic(topic); + } catch (Throwable t) { + log.error("Failed to delete topic {}", topic, t); + } + }); } } @@ -72,4 +87,8 @@ public class PartitionedQueueConsumerManager extends MainQ addTask(new RemovePartitionsTask(partitions)); } + public void delete(Set partitions) { + addTask(new DeletePartitionsTask(partitions)); + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java deleted file mode 100644 index 8870ff2a2c..0000000000 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java +++ /dev/null @@ -1,98 +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.common.consumer; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.queue.TbQueueMsg; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTopic; - -@Slf4j -public class QueueStateService { - - private PartitionedQueueConsumerManager stateConsumer; - private PartitionedQueueConsumerManager eventConsumer; - - @Getter - private Set partitions; - private final Set partitionsInProgress = ConcurrentHashMap.newKeySet(); - private boolean initialized; - - private final ReadWriteLock partitionsLock = new ReentrantReadWriteLock(); - - public void init(PartitionedQueueConsumerManager stateConsumer, PartitionedQueueConsumerManager eventConsumer) { - this.stateConsumer = stateConsumer; - this.eventConsumer = eventConsumer; - } - - public void update(Set newPartitions) { - newPartitions = withTopic(newPartitions, stateConsumer.getTopic()); - var writeLock = partitionsLock.writeLock(); - writeLock.lock(); - Set oldPartitions = this.partitions != null ? this.partitions : Collections.emptySet(); - Set addedPartitions; - Set removedPartitions; - try { - addedPartitions = new HashSet<>(newPartitions); - addedPartitions.removeAll(oldPartitions); - removedPartitions = new HashSet<>(oldPartitions); - removedPartitions.removeAll(newPartitions); - this.partitions = newPartitions; - } finally { - writeLock.unlock(); - } - - if (!removedPartitions.isEmpty()) { - stateConsumer.removePartitions(removedPartitions); - eventConsumer.removePartitions(withTopic(removedPartitions, eventConsumer.getTopic())); - } - - if (!addedPartitions.isEmpty()) { - partitionsInProgress.addAll(addedPartitions); - stateConsumer.addPartitions(addedPartitions, partition -> { - var readLock = partitionsLock.readLock(); - readLock.lock(); - try { - partitionsInProgress.remove(partition); - log.info("Finished partition {} (still in progress: {})", partition, partitionsInProgress); - if (partitionsInProgress.isEmpty()) { - log.info("All partitions processed"); - } - if (this.partitions.contains(partition)) { - eventConsumer.addPartitions(Set.of(partition.withTopic(eventConsumer.getTopic()))); - } - } finally { - readLock.unlock(); - } - }); - } - initialized = true; - } - - public Set getPartitionsInProgress() { - return initialized ? partitionsInProgress : null; - } - -} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueTaskType.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueTaskType.java index 93601146e9..84cb3c9382 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueTaskType.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueTaskType.java @@ -20,6 +20,6 @@ import java.io.Serializable; public enum QueueTaskType implements Serializable { UPDATE_PARTITIONS, UPDATE_CONFIG, DELETE, - ADD_PARTITIONS, REMOVE_PARTITIONS + ADD_PARTITIONS, REMOVE_PARTITIONS, DELETE_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 3380dd7e31..e0dd9b808b 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 @@ -60,4 +60,11 @@ public interface TbQueueConsumerManagerTask { } } + record DeletePartitionsTask(Set partitions) implements TbQueueConsumerManagerTask { + @Override + public QueueTaskType getType() { + return QueueTaskType.REMOVE_PARTITIONS; + } + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/DefaultQueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/DefaultQueueStateService.java new file mode 100644 index 0000000000..be019caaa7 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/DefaultQueueStateService.java @@ -0,0 +1,27 @@ +/** + * 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.common.state; + +import org.thingsboard.server.queue.TbQueueMsg; +import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; + +public class DefaultQueueStateService extends QueueStateService { + + public DefaultQueueStateService(PartitionedQueueConsumerManager eventConsumer) { + super(eventConsumer); + } + +} 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 new file mode 100644 index 0000000000..9adc6bb996 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java @@ -0,0 +1,81 @@ +/** + * 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.common.state; + +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.Set; + +import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTopic; + +@Slf4j +public class KafkaQueueStateService extends QueueStateService { + + private final PartitionedQueueConsumerManager stateConsumer; + + public KafkaQueueStateService(PartitionedQueueConsumerManager eventConsumer, PartitionedQueueConsumerManager stateConsumer) { + super(eventConsumer); + this.stateConsumer = stateConsumer; + } + + @Override + protected void addPartitions(QueueKey queueKey, Set partitions) { + Set statePartitions = withTopic(partitions, stateConsumer.getTopic()); + partitionsInProgress.addAll(statePartitions); + stateConsumer.addPartitions(statePartitions, statePartition -> { + var readLock = partitionsLock.readLock(); + readLock.lock(); + try { + partitionsInProgress.remove(statePartition); + log.info("Finished partition {} (still in progress: {})", statePartition, partitionsInProgress); + if (partitionsInProgress.isEmpty()) { + log.info("All partitions processed"); + } + + TopicPartitionInfo eventPartition = statePartition.withTopic(eventConsumer.getTopic()); + if (this.partitions.get(queueKey).contains(eventPartition)) { + eventConsumer.addPartitions(Set.of(eventPartition)); + } + } finally { + readLock.unlock(); + } + }); + } + + @Override + protected void removePartitions(QueueKey queueKey, Set partitions) { + super.removePartitions(queueKey, partitions); + stateConsumer.removePartitions(withTopic(partitions, stateConsumer.getTopic())); + } + + @Override + protected void deletePartitions(Set partitions) { + super.deletePartitions(partitions); + stateConsumer.delete(withTopic(partitions, stateConsumer.getTopic())); + } + + @Override + public void stop() { + super.stop(); + stateConsumer.stop(); + stateConsumer.awaitStop(); + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/QueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/QueueStateService.java new file mode 100644 index 0000000000..29426fab63 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/QueueStateService.java @@ -0,0 +1,114 @@ +/** + * 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.common.state; + +import lombok.Getter; +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.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTopic; + +@Slf4j +public abstract class QueueStateService { + + protected final PartitionedQueueConsumerManager eventConsumer; + + @Getter + protected final Map> partitions = new HashMap<>(); + protected final Set partitionsInProgress = ConcurrentHashMap.newKeySet(); + protected boolean initialized; + + protected final ReadWriteLock partitionsLock = new ReentrantReadWriteLock(); + + protected QueueStateService(PartitionedQueueConsumerManager eventConsumer) { + this.eventConsumer = eventConsumer; + } + + public void update(QueueKey queueKey, Set newPartitions) { + newPartitions = withTopic(newPartitions, eventConsumer.getTopic()); + var writeLock = partitionsLock.writeLock(); + writeLock.lock(); + Set oldPartitions = this.partitions.getOrDefault(queueKey, Collections.emptySet()); + Set addedPartitions; + Set removedPartitions; + try { + addedPartitions = new HashSet<>(newPartitions); + addedPartitions.removeAll(oldPartitions); + removedPartitions = new HashSet<>(oldPartitions); + removedPartitions.removeAll(newPartitions); + this.partitions.put(queueKey, newPartitions); + } finally { + writeLock.unlock(); + } + + if (!removedPartitions.isEmpty()) { + removePartitions(queueKey, removedPartitions); + } + + if (!addedPartitions.isEmpty()) { + addPartitions(queueKey, addedPartitions); + } + initialized = true; + } + + protected void addPartitions(QueueKey queueKey, Set partitions) { + eventConsumer.addPartitions(partitions); + } + + protected void removePartitions(QueueKey queueKey, Set partitions) { + eventConsumer.removePartitions(partitions); + } + + public void delete(Set partitions) { + if (partitions.isEmpty()) { + return; + } + var writeLock = partitionsLock.writeLock(); + writeLock.lock(); + try { + this.partitions.values().forEach(tpis -> tpis.removeAll(partitions)); + } finally { + writeLock.unlock(); + } + deletePartitions(partitions); + } + + protected void deletePartitions(Set partitions) { + eventConsumer.delete(withTopic(partitions, eventConsumer.getTopic())); + } + + public Set getPartitionsInProgress() { + return initialized ? partitionsInProgress : null; + } + + public void stop() { + eventConsumer.stop(); + eventConsumer.awaitStop(); + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 345b44e764..3a76d50825 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -52,7 +52,10 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; +import java.util.stream.Stream; +import static org.thingsboard.server.common.data.DataConstants.CF_QUEUE_NAME; +import static org.thingsboard.server.common.data.DataConstants.CF_STATES_QUEUE_NAME; import static org.thingsboard.server.common.data.DataConstants.EDGE_QUEUE_NAME; import static org.thingsboard.server.common.data.DataConstants.MAIN_QUEUE_NAME; @@ -159,16 +162,7 @@ public class HashPartitionService implements PartitionService { List queueRoutingInfoList = getQueueRoutingInfos(); queueRoutingInfoList.forEach(queue -> { QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queue); - if (DataConstants.MAIN_QUEUE_NAME.equals(queueKey.getQueueName())) { - QueueKey cfQueueKey = queueKey.withQueueName(DataConstants.CF_QUEUE_NAME); - partitionSizesMap.put(cfQueueKey, queue.getPartitions()); - partitionTopicsMap.put(cfQueueKey, cfEventTopic); - QueueKey cfQueueStatesKey = queueKey.withQueueName(DataConstants.CF_STATES_QUEUE_NAME); - partitionSizesMap.put(cfQueueStatesKey, queue.getPartitions()); - partitionTopicsMap.put(cfQueueStatesKey, cfStateTopic); - } - partitionTopicsMap.put(queueKey, queue.getQueueTopic()); - partitionSizesMap.put(queueKey, queue.getPartitions()); + updateQueue(queueKey, queue.getQueueTopic(), queue.getPartitions()); queueConfigs.put(queueKey, new QueueConfig(queue)); }); } @@ -215,16 +209,7 @@ public class HashPartitionService implements PartitionService { QueueRoutingInfo queueRoutingInfo = new QueueRoutingInfo(queueUpdateMsg); TenantId tenantId = queueRoutingInfo.getTenantId(); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueRoutingInfo.getQueueName(), tenantId); - if (DataConstants.MAIN_QUEUE_NAME.equals(queueKey.getQueueName())) { - QueueKey cfQueueKey = queueKey.withQueueName(DataConstants.CF_QUEUE_NAME); - partitionSizesMap.put(cfQueueKey, queueRoutingInfo.getPartitions()); - partitionTopicsMap.put(cfQueueKey, cfEventTopic); - QueueKey cfQueueStatesKey = queueKey.withQueueName(DataConstants.CF_STATES_QUEUE_NAME); - partitionSizesMap.put(cfQueueStatesKey, queueRoutingInfo.getPartitions()); - partitionTopicsMap.put(cfQueueStatesKey, cfStateTopic); - } - partitionTopicsMap.put(queueKey, queueRoutingInfo.getQueueTopic()); - partitionSizesMap.put(queueKey, queueRoutingInfo.getPartitions()); + updateQueue(queueKey, queueRoutingInfo.getQueueTopic(), queueRoutingInfo.getPartitions()); queueConfigs.put(queueKey, new QueueConfig(queueRoutingInfo)); if (!tenantId.isSysTenantId()) { tenantRoutingInfoMap.remove(tenantId); @@ -235,9 +220,15 @@ public class HashPartitionService implements PartitionService { @Override public void removeQueues(List queueDeleteMsgs) { List queueKeys = queueDeleteMsgs.stream() - .map(queueDeleteMsg -> { + .flatMap(queueDeleteMsg -> { TenantId tenantId = TenantId.fromUUID(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB())); - return new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId); + QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId); + if (queueKey.getQueueName().equals(MAIN_QUEUE_NAME)) { + return Stream.of(queueKey, queueKey.withQueueName(CF_QUEUE_NAME), + queueKey.withQueueName(CF_STATES_QUEUE_NAME)); + } else { + return Stream.of(queueKey); + } }).toList(); queueKeys.forEach(queueKey -> { removeQueue(queueKey); @@ -252,25 +243,38 @@ public class HashPartitionService implements PartitionService { @Override public void removeTenant(TenantId tenantId) { List queueKeys = partitionSizesMap.keySet().stream() - .filter(queueKey -> tenantId.equals(queueKey.getTenantId())).toList(); + .filter(queueKey -> tenantId.equals(queueKey.getTenantId())) + .flatMap(queueKey -> { + if (queueKey.getQueueName().equals(MAIN_QUEUE_NAME)) { + return Stream.of(queueKey, queueKey.withQueueName(CF_QUEUE_NAME), + queueKey.withQueueName(CF_STATES_QUEUE_NAME)); + } else { + return Stream.of(queueKey); + } + }) + .toList(); queueKeys.forEach(this::removeQueue); evictTenantInfo(tenantId); } + private void updateQueue(QueueKey queueKey, String topic, int partitions) { + partitionTopicsMap.put(queueKey, topic); + partitionSizesMap.put(queueKey, partitions); + if (DataConstants.MAIN_QUEUE_NAME.equals(queueKey.getQueueName())) { + QueueKey cfQueueKey = queueKey.withQueueName(DataConstants.CF_QUEUE_NAME); + partitionTopicsMap.put(cfQueueKey, cfEventTopic); + partitionSizesMap.put(cfQueueKey, partitions); + QueueKey cfStatesQueueKey = queueKey.withQueueName(DataConstants.CF_STATES_QUEUE_NAME); + partitionTopicsMap.put(cfStatesQueueKey, cfStateTopic); + partitionSizesMap.put(cfStatesQueueKey, partitions); + } + } + private void removeQueue(QueueKey queueKey) { myPartitions.remove(queueKey); partitionTopicsMap.remove(queueKey); partitionSizesMap.remove(queueKey); queueConfigs.remove(queueKey); - - if (DataConstants.MAIN_QUEUE_NAME.equals(queueKey.getQueueName())) { - QueueKey cfQueueKey = queueKey.withQueueName(DataConstants.CF_QUEUE_NAME); - partitionSizesMap.remove(cfQueueKey); - partitionTopicsMap.remove(cfQueueKey); - QueueKey cfQueueStatesKey = queueKey.withQueueName(DataConstants.CF_STATES_QUEUE_NAME); - partitionSizesMap.remove(cfQueueStatesKey); - partitionTopicsMap.remove(cfQueueStatesKey); - } } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java index 6720a9d71e..1709003ada 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java @@ -23,9 +23,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; -import static org.thingsboard.server.common.data.DataConstants.CF_QUEUE_NAME; -import static org.thingsboard.server.common.data.DataConstants.CF_STATES_QUEUE_NAME; - @Data @AllArgsConstructor public class QueueKey { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java index f165f60be7..32537edba5 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java @@ -24,7 +24,6 @@ import org.thingsboard.server.queue.discovery.QueueKey; import java.io.Serial; import java.util.Collection; -import java.util.Collections; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; 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 fed786e120..b5541c740b 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 @@ -17,6 +17,7 @@ package org.thingsboard.server.queue.edqs; 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; @@ -32,4 +33,6 @@ public interface EdqsQueueFactory { TbQueueResponseTemplate, TbProtoQueueMsg> createEdqsResponseTemplate(); + TbQueueAdmin getEdqsQueueAdmin(); + } 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 0b6cc1909d..0801399c14 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 @@ -22,6 +22,7 @@ import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsType; 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; @@ -39,6 +40,7 @@ public class InMemoryEdqsQueueFactory implements EdqsQueueFactory { private final InMemoryStorage storage; private final EdqsConfig edqsConfig; private final StatsFactory statsFactory; + private final TbQueueAdmin queueAdmin; @Override public TbQueueConsumer> createEdqsMsgConsumer(EdqsQueue queue) { @@ -76,4 +78,9 @@ public class InMemoryEdqsQueueFactory implements EdqsQueueFactory { .build(); } + @Override + public TbQueueAdmin getEdqsQueueAdmin() { + return queueAdmin; + } + } 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 42ca604841..e985696040 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 @@ -22,6 +22,7 @@ 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; @@ -126,4 +127,9 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory { .build(); } + @Override + public TbQueueAdmin 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 f393a27ddf..4ae744be67 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 @@ -91,7 +91,7 @@ public class TbKafkaAdmin implements TbQueueAdmin { @Override public void deleteTopic(String topic) { Set topics = getTopics(); - if (topics.contains(topic)) { + if (topics.remove(topic)) { settings.getAdminClient().deleteTopics(Collections.singletonList(topic)); } else { try { 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 d219428941..4bd3bf0fe6 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 @@ -160,7 +160,7 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue int partition = record.partition(); Long endOffset = endOffsets.get(partition); if (endOffset == null) { - log.warn("End offset not found for {} [{}]", record.topic(), partition); + log.debug("End offset not found for {} [{}]", record.topic(), partition); return; } log.trace("[{}-{}] Got record offset {}, expected end offset: {}", record.topic(), partition, record.offset(), endOffset - 1); 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 e97af10ecc..fac2cf5d5e 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 @@ -138,6 +138,11 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE return new InMemoryTbQueueConsumer<>(storage, topicService.buildTopicName(calculatedFieldSettings.getEventTopic())); } + @Override + public TbQueueAdmin getCalculatedFieldQueueAdmin() { + return queueAdmin; + } + @Override public TbQueueProducer> createToCalculatedFieldMsgProducer() { return new InMemoryTbQueueProducer<>(storage, topicService.buildTopicName(calculatedFieldSettings.getEventTopic())); 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 f3d1e2d158..f07bd9dcbb 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 @@ -526,6 +526,11 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi return consumerBuilder.build(); } + @Override + public TbQueueAdmin getCalculatedFieldQueueAdmin() { + return cfAdmin; + } + @Override public TbQueueProducer> createToCalculatedFieldMsgProducer() { TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); 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 43fbb5efeb..d0e6c2f123 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 @@ -321,6 +321,11 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { return consumerBuilder.build(); } + @Override + public TbQueueAdmin getCalculatedFieldQueueAdmin() { + return cfAdmin; + } + @Override public TbQueueProducer> createToCalculatedFieldMsgProducer() { TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java index 767fea9f0c..03f662d8bb 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java @@ -29,6 +29,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueRequestTemplate; @@ -122,6 +123,8 @@ public interface TbRuleEngineQueueFactory extends TbUsageStatsClientQueueFactory TbQueueConsumer> createToCalculatedFieldMsgConsumer(); + TbQueueAdmin getCalculatedFieldQueueAdmin(); + TbQueueProducer> createToCalculatedFieldMsgProducer(); TbQueueConsumer> createToCalculatedFieldNotificationsMsgConsumer(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java index bc90a8c392..715020dc7c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.queue.usagestats; +import com.google.common.collect.Lists; import jakarta.annotation.PostConstruct; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -29,6 +30,9 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.stats.TbApiUsageReportClient; +import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto; import org.thingsboard.server.queue.TbQueueProducer; @@ -38,7 +42,11 @@ import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.scheduler.SchedulerComponent; +import java.util.ArrayList; import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Random; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -57,6 +65,8 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { private boolean enabledPerCustomer; @Value("${usage.stats.report.interval:10}") private int interval; + @Value("${usage.stats.report.pack_size:1024}") + private int packSize; private final EnumMap> stats = new EnumMap<>(ApiUsageRecordKey.class); @@ -64,7 +74,7 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { private final TbServiceInfoProvider serviceInfoProvider; private final SchedulerComponent scheduler; private final TbQueueProducerProvider producerProvider; - private TbQueueProducer> msgProducer; + private TbQueueProducer> msgProducer; @PostConstruct private void init() { @@ -84,7 +94,7 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { } private void reportStats() { - ConcurrentMap report = new ConcurrentHashMap<>(); + ConcurrentMap report = new ConcurrentHashMap<>(); for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { ConcurrentMap statsForKey = stats.get(key); @@ -92,8 +102,8 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { long value = statsValue.get(); if (value == 0 && key.isCounter()) return; - ToUsageStatsServiceMsg.Builder statsMsg = report.computeIfAbsent(reportLevel.getParentEntity(), parent -> { - ToUsageStatsServiceMsg.Builder newStatsMsg = ToUsageStatsServiceMsg.newBuilder(); + UsageStatsServiceMsg.Builder statsMsg = report.computeIfAbsent(reportLevel.getParentEntity(), parent -> { + UsageStatsServiceMsg.Builder newStatsMsg = UsageStatsServiceMsg.newBuilder(); TenantId tenantId = parent.getTenantId(); newStatsMsg.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); @@ -105,36 +115,55 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { newStatsMsg.setCustomerIdLSB(customerId.getId().getLeastSignificantBits()); } - newStatsMsg.setServiceId(serviceInfoProvider.getServiceId()); return newStatsMsg; }); UsageStatsKVProto.Builder statsItem = UsageStatsKVProto.newBuilder() - .setKey(key.name()) + .setRecordKey(ProtoUtils.toProto(key)) .setValue(value); statsMsg.addValues(statsItem.build()); }); statsForKey.clear(); } - report.forEach(((parent, statsMsg) -> { - //TODO: figure out how to minimize messages into the queue. Maybe group by 100s of messages? + Map> reportStatsPerTpi = new HashMap<>(); + + report.forEach((parent, statsMsg) -> { try { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, parent.getTenantId(), parent.getId()) .newByTopic(msgProducer.getDefaultTopic()); - msgProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg.build()), null); + reportStatsPerTpi.computeIfAbsent(tpi, k -> new ArrayList<>()).add(statsMsg.build()); } catch (TenantNotFoundException e) { log.debug("Couldn't report usage stats for non-existing tenant: {}", e.getTenantId()); - } catch (Exception e) { - log.warn("Failed to report usage stats for tenant {}", parent.getTenantId(), e); } - })); + }); + + reportStatsPerTpi.forEach((tpi, statsList) -> { + toMsgPack(statsList).forEach(pack -> { + try { + msgProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), pack), null); + } catch (Exception e) { + log.warn("Failed to report usage stats pack to TPI {}", tpi, e); + } + }); + }); if (!report.isEmpty()) { log.debug("Reporting API usage statistics for {} tenants and customers", report.size()); } } + private List toMsgPack(List list) { + return Lists.partition(list, packSize) + .stream() + .map(partition -> + ToUsageStatsServiceMsg.newBuilder() + .addAllMsgs(partition) + .setServiceId(serviceInfoProvider.getServiceId()) + .build()) + .toList(); + } + @Override public void report(TenantId tenantId, CustomerId customerId, ApiUsageRecordKey key, long value) { if (!enabled) return; diff --git a/common/script/pom.xml b/common/script/pom.xml index 80a8c4664a..681750024d 100644 --- a/common/script/pom.xml +++ b/common/script/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC common org.thingsboard.common diff --git a/common/script/remote-js-client/pom.xml b/common/script/remote-js-client/pom.xml index fc15448316..c398444fa6 100644 --- a/common/script/remote-js-client/pom.xml +++ b/common/script/remote-js-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.0.0-SNAPSHOT + 4.0.0-RC script org.thingsboard.common.script diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml index 3a51b99f35..3655692217 100644 --- a/common/script/script-api/pom.xml +++ b/common/script/script-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.0.0-SNAPSHOT + 4.0.0-RC script org.thingsboard.common.script diff --git a/common/stats/pom.xml b/common/stats/pom.xml index db8367180a..5319a40ac1 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index 887dc014e8..dbfe72c515 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.0.0-SNAPSHOT + 4.0.0-RC transport org.thingsboard.common.transport diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index 4354d14a1c..358ecb72e2 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.0.0-SNAPSHOT + 4.0.0-RC transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index f918cdd167..2100d5db22 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.0.0-SNAPSHOT + 4.0.0-RC transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index 34eeed396d..735fe9a2d2 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.0.0-SNAPSHOT + 4.0.0-RC transport org.thingsboard.common.transport diff --git a/common/transport/pom.xml b/common/transport/pom.xml index 27b9bcbc9e..2ee0f4f3c8 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC common org.thingsboard.common diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index b971de293c..48a23b4e91 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 4.0.0-SNAPSHOT + 4.0.0-RC transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 423fc5dc97..a671358581 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.0.0-SNAPSHOT + 4.0.0-RC transport org.thingsboard.common.transport diff --git a/common/util/pom.xml b/common/util/pom.xml index 83302c8c80..430a6e7df9 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC common org.thingsboard.common diff --git a/common/util/src/main/java/org/thingsboard/common/util/LinkedHashMapRemoveEldest.java b/common/util/src/main/java/org/thingsboard/common/util/LinkedHashMapRemoveEldest.java index cd902e3cdb..f570017e31 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/LinkedHashMapRemoveEldest.java +++ b/common/util/src/main/java/org/thingsboard/common/util/LinkedHashMapRemoveEldest.java @@ -34,10 +34,10 @@ import java.util.function.BiConsumer; @ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) public class LinkedHashMapRemoveEldest extends LinkedHashMap { - final long maxEntries; + final int maxEntries; final BiConsumer removalConsumer; - public LinkedHashMapRemoveEldest(long maxEntries, BiConsumer removalConsumer) { + public LinkedHashMapRemoveEldest(int maxEntries, BiConsumer removalConsumer) { this.maxEntries = maxEntries; this.removalConsumer = removalConsumer; } diff --git a/common/util/src/test/java/org/thingsboard/common/util/LinkedHashMapRemoveEldestTest.java b/common/util/src/test/java/org/thingsboard/common/util/LinkedHashMapRemoveEldestTest.java index 0fb12081c9..9ddc658fa5 100644 --- a/common/util/src/test/java/org/thingsboard/common/util/LinkedHashMapRemoveEldestTest.java +++ b/common/util/src/test/java/org/thingsboard/common/util/LinkedHashMapRemoveEldestTest.java @@ -27,10 +27,10 @@ import static org.hamcrest.MatcherAssert.assertThat; public class LinkedHashMapRemoveEldestTest { - public static final long MAX_ENTRIES = 10L; - long removeCount = 0; + public static final int MAX_ENTRIES = 10; + int removeCount = 0; - void removalConsumer(Long id, String name) { + void removalConsumer(Integer id, String name) { removeCount++; assertThat(id, is(Matchers.lessThan(MAX_ENTRIES))); assertThat(name, is(id.toString())); @@ -39,7 +39,7 @@ public class LinkedHashMapRemoveEldestTest { @Test public void givenMap_whenOverSized_thenVerifyRemovedEldest() { //given - LinkedHashMapRemoveEldest map = + LinkedHashMapRemoveEldest map = new LinkedHashMapRemoveEldest<>(MAX_ENTRIES, this::removalConsumer); assertThat(map.getMaxEntries(), is(MAX_ENTRIES)); @@ -49,14 +49,14 @@ public class LinkedHashMapRemoveEldestTest { assertThat(map.size(), is(0)); //when - for (long i = 0; i < MAX_ENTRIES * 2; i++) { + for (int i = 0; i < MAX_ENTRIES * 2; i++) { map.put(i, String.valueOf(i)); } //then - assertThat((long) map.size(), is(MAX_ENTRIES)); + assertThat( map.size(), is(MAX_ENTRIES)); assertThat(removeCount, is(MAX_ENTRIES)); - for (long i = MAX_ENTRIES; i < MAX_ENTRIES * 2; i++) { + for (int i = MAX_ENTRIES; i < MAX_ENTRIES * 2; i++) { assertThat(map.get(i), is(String.valueOf(i))); } } diff --git a/common/version-control/pom.xml b/common/version-control/pom.xml index 22beed80d9..8fc4cb2566 100644 --- a/common/version-control/pom.xml +++ b/common/version-control/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC common org.thingsboard.common diff --git a/dao/pom.xml b/dao/pom.xml index 35baa9b13b..0bc6541ac1 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC thingsboard dao diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java index b7f350ba72..606388e465 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java @@ -41,6 +41,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.util.DeviceConnectivityUtil; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URISyntaxException; @@ -83,6 +84,8 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService @Value("${device.connectivity.mqtts.pem_cert_file:}") private String mqttsPemCertFile; + @Value("${device.connectivity.coaps.pem_cert_file:}") + private String coapsPemCertFile; @Override public JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException { @@ -133,22 +136,19 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService public Resource getPemCertFile(String protocol) { return certs.computeIfAbsent(protocol, key -> { DeviceConnectivityInfo connectivity = getConnectivity(protocol); - if (!MQTTS.equals(protocol) || connectivity == null) { + if (connectivity == null) { log.warn("Unknown connectivity protocol: {}", protocol); return null; } - if (StringUtils.isNotBlank(mqttsPemCertFile) && ResourceUtils.resourceExists(this, mqttsPemCertFile)) { - try { - return getCert(mqttsPemCertFile); - } catch (Exception e) { - String msg = String.format("Failed to read %s server certificate!", protocol); - log.warn(msg); - throw new RuntimeException(msg, e); + return switch (protocol) { + case COAPS -> getCert(coapsPemCertFile); + case MQTTS -> getCert(mqttsPemCertFile); + default -> { + log.warn("Unsupported secure protocol: {}", protocol); + yield null; } - } else { - return null; - } + }; }); } @@ -174,7 +174,11 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService return info != null && info.isEnabled(); } - private Resource getCert(String path) throws Exception { + private Resource getCert(String path) { + if (StringUtils.isBlank(path) || !ResourceUtils.resourceExists(this, path)) { + return null; + } + StringBuilder pemContentBuilder = new StringBuilder(); try (InputStream inStream = ResourceUtils.getInputStream(this, path); @@ -197,6 +201,10 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService pemContentBuilder.append("-----END CERTIFICATE-----\n"); } } + } catch (Exception e) { + String msg = String.format("Failed to read %s server certificate!", path); + log.warn(msg); + throw new RuntimeException(msg, e); } return new ByteArrayResource(pemContentBuilder.toString().getBytes(StandardCharsets.UTF_8)); @@ -311,8 +319,11 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService } if (isEnabled(COAPS)) { + ArrayNode coapsCommands = coapCommands.putArray(COAPS); + Optional.ofNullable(DeviceConnectivityUtil.getCurlPemCertCommand(baseUrl, COAPS)) + .ifPresent(coapsCommands::add); Optional.ofNullable(getCoapPublishCommand(COAPS, baseUrl, deviceCredentials)) - .ifPresent(v -> coapCommands.put(COAPS, v)); + .ifPresent(coapsCommands::add); Optional.ofNullable(getDockerCoapPublishCommand(COAPS, baseUrl, deviceCredentials)) .ifPresent(v -> dockerCoapCommands.put(COAPS, v)); @@ -336,7 +347,7 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService DeviceConnectivityInfo properties = getConnectivity(protocol); String host = getHost(baseUrl, properties, protocol); String port = StringUtils.isBlank(properties.getPort()) ? "" : ":" + properties.getPort(); - return DeviceConnectivityUtil.getDockerCoapPublishCommand(protocol, host, port, deviceCredentials); + return DeviceConnectivityUtil.getDockerCoapPublishCommand(protocol, baseUrl, host, port, deviceCredentials); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index a0e75e062c..87f7e44a1f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -236,6 +236,7 @@ public class BaseRelationService implements RelationService { return Futures.transform(future, deletedEvent -> { if (deletedEvent != null) { handleEvictEvent(EntityRelationEvent.from(deletedEvent)); + eventPublisher.publishEvent(new RelationActionEvent(tenantId, deletedEvent, ActionType.RELATION_DELETED)); } return deletedEvent != null; }, MoreExecutors.directExecutor()); @@ -267,6 +268,7 @@ public class BaseRelationService implements RelationService { for (EntityRelation relation : inboundRelations) { eventPublisher.publishEvent(EntityRelationEvent.from(relation)); + eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_DELETED)); } List outboundRelations; @@ -278,6 +280,7 @@ public class BaseRelationService implements RelationService { for (EntityRelation relation : outboundRelations) { eventPublisher.publishEvent(EntityRelationEvent.from(relation)); + eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_DELETED)); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java index ba88d446d9..9f1f583108 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java @@ -21,6 +21,7 @@ import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.EntityId; @@ -158,6 +159,9 @@ public abstract class DataValidator> { protected static void validateQueueName(String name) { validateQueueNameOrTopic(name, NAME); + if (DataConstants.CF_QUEUE_NAME.equals(name) || DataConstants.CF_STATES_QUEUE_NAME.equals(name)) { + throw new DataValidationException(String.format("The queue name '%s' is not allowed. This name is reserved for internal use. Please choose a different name.", name)); + } } protected static void validateQueueTopic(String topic) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index 19ce8e8993..a1137853ab 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -195,7 +195,9 @@ public class BaseTimeseriesService implements TimeseriesService { } if (saveLatest) { latestFutures.add(Futures.transform(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry), version -> { - edqsService.onUpdate(tenantId, ObjectType.LATEST_TS_KV, new LatestTsKv(entityId, tsKvEntry, version)); + if (version != null) { + edqsService.onUpdate(tenantId, ObjectType.LATEST_TS_KV, new LatestTsKv(entityId, tsKvEntry, version)); + } return version; }, MoreExecutors.directExecutor())); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java index 7a0c122186..56e0e44778 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java @@ -199,19 +199,39 @@ public class DeviceConnectivityUtil { switch (deviceCredentials.getCredentialsType()) { case ACCESS_TOKEN: String client = COAPS.equals(protocol) ? "coap-client-openssl" : "coap-client"; - return String.format("%s -v 6 -m POST %s://%s%s/api/v1/%s/telemetry -t json -e %s", - client, protocol, host, port, deviceCredentials.getCredentialsId(), JSON_EXAMPLE_PAYLOAD); + String certificate = COAPS.equals(protocol) ? " -R " + CA_ROOT_CERT_PEM : ""; + return String.format("%s -v 6 -m POST%s -t \"application/json\" -e %s %s://%s%s/api/v1/%s/telemetry", + client, certificate, JSON_EXAMPLE_PAYLOAD, protocol, host, port, deviceCredentials.getCredentialsId()); default: return null; } } - public static String getDockerCoapPublishCommand(String protocol, String host, String port, DeviceCredentials deviceCredentials) { + public static String getDockerCoapPublishCommand(String protocol, String baseUrl, String host, String port, DeviceCredentials deviceCredentials) { String coapCommand = getCoapPublishCommand(protocol, host, port, deviceCredentials); - if (coapCommand != null && isLocalhost(host)) { + + if (coapCommand == null) { + return null; + } + + StringBuilder coapDockerCommand = new StringBuilder(); + coapDockerCommand.append(DOCKER_RUN).append(isLocalhost(host) ? ADD_DOCKER_INTERNAL_HOST : "").append(COAP_IMAGE); + + if (isLocalhost(host)) { coapCommand = coapCommand.replace(host, HOST_DOCKER_INTERNAL); } - return coapCommand != null ? String.format("%s%s%s", DOCKER_RUN + (isLocalhost(host) ? ADD_DOCKER_INTERNAL_HOST : ""), COAP_IMAGE, coapCommand) : null; + + if (COAPS.equals(protocol)) { + coapDockerCommand.append("/bin/sh -c \"") + .append(getCurlPemCertCommand(baseUrl, protocol)) + .append(" && ") + .append(coapCommand) + .append("\""); + } else { + coapDockerCommand.append(coapCommand); + } + + return coapDockerCommand.toString(); } public static String getHost(String baseUrl, DeviceConnectivityInfo properties, String protocol) throws URISyntaxException { diff --git a/docker/compose-utils.sh b/docker/compose-utils.sh index 5767026b11..3862024786 100755 --- a/docker/compose-utils.sh +++ b/docker/compose-utils.sh @@ -143,7 +143,6 @@ function additionalComposeEdqsArgs() { function permissionList() { PERMISSION_LIST=" 799 799 tb-node/log - 799 799 tb-transports/coap/log 799 799 tb-transports/lwm2m/log 799 799 tb-transports/http/log 799 799 tb-transports/mqtt/log @@ -200,29 +199,77 @@ function permissionList() { } function checkFolders() { + CREATE=false + SKIP_CHOWN=false + for i in "$@" + do + case $i in + --create) + CREATE=true + shift + ;; + --skipChown) + SKIP_CHOWN=true + shift + ;; + *) + # unknown option + ;; + esac + done EXIT_CODE=0 PERMISSION_LIST=$(permissionList) || exit $? set -e while read -r USR GRP DIR do - if [ -z "$DIR" ]; then # skip empty lines + IS_EXIST_CHECK_PASSED=false + IS_OWNER_CHECK_PASSED=false + + # skip empty lines + if [ -z "$DIR" ]; then continue fi - MESSAGE="Checking user ${USR} group ${GRP} dir ${DIR}" - if [[ -d "$DIR" ]] && - [[ $(ls -ldn "$DIR" | awk '{print $3}') -eq "$USR" ]] && - [[ $(ls -ldn "$DIR" | awk '{print $4}') -eq "$GRP" ]] - then - MESSAGE="$MESSAGE OK" + + # checks section + echo "Checking if dir ${DIR} exists..." + if [[ -d "$DIR" ]]; then + echo "> OK" + IS_EXIST_CHECK_PASSED=true + if [ "$SKIP_CHOWN" = false ]; then + echo "Checking user ${USR} group ${GRP} ownership for dir ${DIR}..." + if [[ $(ls -ldn "$DIR" | awk '{print $3}') -eq "$USR" ]] && [[ $(ls -ldn "$DIR" | awk '{print $4}') -eq "$GRP" ]]; then + echo "> OK" + IS_OWNER_CHECK_PASSED=true + else + echo "...ownership check failed" + if [ "$CREATE" = false ]; then + EXIT_CODE=1 + fi + fi + fi else - if [ "$1" = "--create" ]; then - echo "Create and chown: user ${USR} group ${GRP} dir ${DIR}" - mkdir -p "$DIR" && sudo chown -R "$USR":"$GRP" "$DIR" - else - echo "$MESSAGE FAILED" + echo "...does not exist" + if [ "$CREATE" = false ]; then EXIT_CODE=1 fi fi + + # create/chown section + if [ "$CREATE" = true ]; then + if [ "$IS_EXIST_CHECK_PASSED" = false ]; then + echo "...will create dir ${DIR}" + if [ "$SKIP_CHOWN" = false ]; then + echo "...will change ownership to user ${USR} group ${GRP} for dir ${DIR}" + mkdir -p "$DIR" && sudo chown -R "$USR":"$GRP" "$DIR" && echo "> OK" + else + mkdir -p "$DIR" && echo "> OK" + fi + elif [ "$IS_OWNER_CHECK_PASSED" = false ] && [ "$SKIP_CHOWN" = false ]; then + echo "...will change ownership to user ${USR} group ${GRP} for dir ${DIR}" + sudo chown -R "$USR":"$GRP" "$DIR" && echo "> OK" + fi + fi + done < <(echo "$PERMISSION_LIST") return $EXIT_CODE } diff --git a/docker/docker-check-log-folders.sh b/docker/docker-check-log-folders.sh index e293968a69..6122f3d2c1 100755 --- a/docker/docker-check-log-folders.sh +++ b/docker/docker-check-log-folders.sh @@ -17,5 +17,12 @@ set -e source compose-utils.sh -checkFolders || exit $? -echo "OK" +if checkFolders "$@" ; then + echo "------" + echo "All checks have passed" +else + CHECK_EXIT_CODE=$? + echo "------" + echo "Some checks did not pass - check the output" + exit $CHECK_EXIT_CODE +fi \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 69b7722459..1cee5ad5ad 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -20,7 +20,7 @@ version: '3.0' services: zookeeper: restart: always - image: "zookeeper:3.8.0" + image: "zookeeper:3.8.1" ports: - "2181" environment: diff --git a/docker/docker-create-log-folders.sh b/docker/docker-create-log-folders.sh index 098ffabb31..ed66d4e156 100755 --- a/docker/docker-create-log-folders.sh +++ b/docker/docker-create-log-folders.sh @@ -17,4 +17,4 @@ set -e source compose-utils.sh -checkFolders --create +checkFolders --create "$@" diff --git a/docker/docker-install-tb.sh b/docker/docker-install-tb.sh index da09684d4f..aa68a2252f 100755 --- a/docker/docker-install-tb.sh +++ b/docker/docker-install-tb.sh @@ -53,8 +53,6 @@ ADDITIONAL_COMPOSE_EDQS_ARGS=$(additionalComposeEdqsArgs) || exit $? ADDITIONAL_STARTUP_SERVICES=$(additionalStartupServices) || exit $? -checkFolders --create || exit $? - if [ ! -z "${ADDITIONAL_STARTUP_SERVICES// }" ]; then COMPOSE_ARGS="\ diff --git a/docker/docker-start-services.sh b/docker/docker-start-services.sh index 8b380f199b..0d256abcf6 100755 --- a/docker/docker-start-services.sh +++ b/docker/docker-start-services.sh @@ -31,8 +31,6 @@ ADDITIONAL_COMPOSE_MONITORING_ARGS=$(additionalComposeMonitoringArgs) || exit $? ADDITIONAL_COMPOSE_EDQS_ARGS=$(additionalComposeEdqsArgs) || exit $? -checkFolders --create || exit $? - COMPOSE_ARGS="\ -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} ${ADDITIONAL_COMPOSE_MONITORING_ARGS} ${ADDITIONAL_COMPOSE_EDQS_ARGS} \ up -d" diff --git a/docker/docker-upgrade-tb.sh b/docker/docker-upgrade-tb.sh index eca5d34957..05293e475e 100755 --- a/docker/docker-upgrade-tb.sh +++ b/docker/docker-upgrade-tb.sh @@ -46,8 +46,6 @@ ADDITIONAL_COMPOSE_EDQS_ARGS=$(additionalComposeEdqsArgs) || exit $? ADDITIONAL_STARTUP_SERVICES=$(additionalStartupServices) || exit $? -checkFolders --create || exit $? - COMPOSE_ARGS_PULL="\ -f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} ${ADDITIONAL_COMPOSE_EDQS_ARGS} \ diff --git a/edqs/pom.xml b/edqs/pom.xml index 07b25102d4..5a70f8d0b0 100644 --- a/edqs/pom.xml +++ b/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC thingsboard edqs @@ -36,7 +36,7 @@ false process-resources package - edqs + tb-edqs ${project.build.directory}/windows true ThingsBoard Entity Data Query Service diff --git a/edqs/src/main/conf/edqs.conf b/edqs/src/main/conf/tb-edqs.conf similarity index 100% rename from edqs/src/main/conf/edqs.conf rename to edqs/src/main/conf/tb-edqs.conf diff --git a/monitoring/pom.xml b/monitoring/pom.xml index b7735aed4c..f89a57e477 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -21,7 +21,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC thingsboard diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index c3379cf37a..424891328e 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC msa org.thingsboard.msa diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java index d74438a428..aafe93f9f3 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java @@ -28,6 +28,7 @@ import io.restassured.internal.ValidatableResponseImpl; import io.restassured.path.json.JsonPath; import io.restassured.response.ValidatableResponse; import io.restassured.specification.RequestSpecification; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Device; @@ -40,10 +41,12 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; @@ -146,6 +149,14 @@ public class TestRestClient { .as(ObjectNode.class); } + public CalculatedField postCalculatedField(CalculatedField calculatedField) { + return given().spec(requestSpec).body(calculatedField) + .post("/api/calculatedField") + .then() + .statusCode(HTTP_OK) + .extract() + .as(CalculatedField.class); + } public Device getDeviceByName(String deviceName) { return given().spec(requestSpec).pathParam("deviceName", deviceName) @@ -212,9 +223,16 @@ public class TestRestClient { .statusCode(anyOf(is(HTTP_OK), is(HTTP_NOT_FOUND))); } - public ValidatableResponse postTelemetryAttribute(String entityType, DeviceId deviceId, String scope, JsonNode attribute) { + public ValidatableResponse deleteCalculatedFieldIfExists(CalculatedFieldId calculatedFieldId) { + return given().spec(requestSpec) + .delete("/api/calculatedField/{calculatedFieldId}", calculatedFieldId.getId()) + .then() + .statusCode(anyOf(is(HTTP_OK), is(HTTP_NOT_FOUND))); + } + + public ValidatableResponse postTelemetryAttribute(EntityId entityId, String scope, JsonNode attribute) { return given().spec(requestSpec).body(attribute) - .post("/api/plugins/telemetry/{entityType}/{entityId}/attributes/{scope}", entityType, deviceId.getId(), scope) + .post("/api/plugins/telemetry/{entityType}/{entityId}/attributes/{scope}", entityId.getEntityType(), entityId.getId(), scope) .then() .statusCode(HTTP_OK); } @@ -237,6 +255,15 @@ public class TestRestClient { .as(JsonNode.class); } + public JsonNode getAttributes(EntityId entityId, AttributeScope scope, String keys) { + return given().spec(requestSpec) + .get("/api/plugins/telemetry/{entityType}/{entityId}/values/attributes/{scope}?keys={keys}", entityId.getEntityType(), entityId.getId(), scope, keys) + .then() + .statusCode(HTTP_OK) + .extract() + .as(JsonNode.class); + } + public JsonNode getLatestTelemetry(EntityId entityId) { return given().spec(requestSpec) .get("/api/plugins/telemetry/" + entityId.getEntityType().name() + "/" + entityId.getId() + "/values/timeseries") @@ -640,6 +667,7 @@ public class TestRestClient { .then() .statusCode(anyOf(is(HTTP_OK), is(HTTP_BAD_REQUEST))); } + public void deleteDeviceProfileIfExists(DeviceProfile deviceProfile) { given().spec(requestSpec) .delete("/api/deviceProfile/" + deviceProfile.getId().getId().toString()) @@ -653,11 +681,11 @@ public class TestRestClient { .get("/api/tenant/devices?deviceName={deviceName}") .then() .statusCode(anyOf(is(HTTP_OK), is(HTTP_NOT_FOUND))); - if(((ValidatableResponseImpl) response).extract().response().getStatusCode()==HTTP_OK){ - return response.extract() + if (((ValidatableResponseImpl) response).extract().response().getStatusCode() == HTTP_OK) { + return response.extract() .as(Device.class); } else { - return null; + return null; } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java new file mode 100644 index 0000000000..63faed41ab --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java @@ -0,0 +1,368 @@ +/** + * 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.msa.cf; + +import com.fasterxml.jackson.databind.JsonNode; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.debug.DebugSettings; +import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; +import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; +import org.thingsboard.server.common.data.device.data.DeviceData; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.msa.AbstractContainerTest; +import org.thingsboard.server.msa.ui.utils.EntityPrototypes; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultAssetProfile; +import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultDeviceProfile; +import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultTenantAdmin; + +public class CalculatedFieldTest extends AbstractContainerTest { + + public final int TIMEOUT = 60; + public final int POLL_INTERVAL = 1; + + private final String deviceToken = "zmzURIVRsq3lvnTP2XBE"; + + private final String exampleScript = "var avgTemperature = temperature.mean(); // Get average temperature\n" + + " var temperatureK = (avgTemperature - 32) * (5 / 9) + 273.15; // Convert Fahrenheit to Kelvin\n" + + "\n" + + " // Estimate air pressure based on altitude\n" + + " var pressure = 101325 * Math.pow((1 - 2.25577e-5 * altitude), 5.25588);\n" + + "\n" + + " // Air density formula\n" + + " var airDensity = pressure / (287.05 * temperatureK);\n" + + "\n" + + " return {\n" + + " \"airDensity\": airDensity\n" + + " };"; + + private TenantId tenantId; + private UserId tenantAdminId; + private DeviceProfileId deviceProfileId; + private AssetProfileId assetProfileId; + private Device device; + private Asset asset; + + @BeforeClass + public void beforeClass() { + testRestClient.login("sysadmin@thingsboard.org", "sysadmin"); + + tenantId = testRestClient.postTenant(EntityPrototypes.defaultTenantPrototype("Tenant")).getId(); + tenantAdminId = testRestClient.createUserAndLogin(defaultTenantAdmin(tenantId, "tenantAdmin@thingsboard.org"), "tenant"); + + deviceProfileId = testRestClient.postDeviceProfile(defaultDeviceProfile("Device Profile 1")).getId(); + device = testRestClient.postDevice(deviceToken, createDevice("Device 1", deviceProfileId)); + + assetProfileId = testRestClient.postAssetProfile(defaultAssetProfile("Asset Profile 1")).getId(); + asset = testRestClient.postAsset(createAsset("Asset 1", assetProfileId)); + + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + testRestClient.postTelemetryAttribute(device.getId(), DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"deviceTemperature\":40}")); + + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":72.32}")); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":72.86}")); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":73.58}")); + + testRestClient.postTelemetryAttribute(asset.getId(), DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"altitude\":1035}")); + } + + @BeforeMethod + public void beforeMethod() { + testRestClient.login("sysadmin@thingsboard.org", "sysadmin"); + } + + @AfterClass + public void afterClass() { + testRestClient.resetToken(); + testRestClient.login("sysadmin@thingsboard.org", "sysadmin"); + testRestClient.deleteTenant(tenantId); + } + + @Test + public void testPerformInitialCalculationForSimpleType() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(); + + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); + }); + + testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); + } + + @Test + public void testChangeConfigArgument() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(); + + Argument savedArgument = savedCalculatedField.getConfiguration().getArguments().get("T"); + savedArgument.setRefEntityKey(new ReferencedEntityKey("deviceTemperature", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + testRestClient.postCalculatedField(savedCalculatedField); + + await().alias("update CF argument -> perform calculation with new argument").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("104.0"); + }); + + testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); + } + + @Test + public void testChangeConfigOutput() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(); + + Output savedOutput = savedCalculatedField.getConfiguration().getOutput(); + savedOutput.setType(OutputType.ATTRIBUTES); + savedOutput.setScope(AttributeScope.SERVER_SCOPE); + savedOutput.setName("temperatureF"); + testRestClient.postCalculatedField(savedCalculatedField); + + await().alias("update CF output -> perform calculation with updated output").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode temperatureF = testRestClient.getAttributes(device.getId(), AttributeScope.SERVER_SCOPE, "temperatureF"); + assertThat(temperatureF).isNotNull(); + assertThat(temperatureF.get(0).get("value").asText()).isEqualTo("77.0"); + }); + + testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); + } + + @Test + public void testChangeConfigExpression() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(); + + savedCalculatedField.setName("F to C"); + savedCalculatedField.getConfiguration().setExpression("(T - 32) / 1.8"); + testRestClient.postCalculatedField(savedCalculatedField); + + await().alias("update CF expression -> perform calculation with new expression").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("-3.89"); + }); + + testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); + } + + @Test + public void testTelemetryUpdated() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(); + + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":30}")); + + await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("86.0"); + }); + + testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); + } + + @Test + public void testEntityIdIsProfile() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(deviceProfileId); + + await().alias("create CF -> perform initial calculation for device by profile").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); + }); + + testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); + } + + @Test + public void testEntityAddedAndDeleted() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(deviceProfileId); + + String newDeviceToken = "mmmXRIVRsq9lbnTP2XBE"; + Device newDevice = testRestClient.postDevice(newDeviceToken, createDevice("Device 2", deviceProfileId)); + + await().alias("create device by profile -> perform initial calculation for new device by profile").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + // used default value since telemetry is not present + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(newDevice.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("53.6"); + }); + + DeviceProfile newDeviceProfile = testRestClient.postDeviceProfile(defaultDeviceProfile("Test Profile")); + newDevice.setDeviceProfileId(newDeviceProfile.getId()); + testRestClient.postDevice(newDeviceToken, newDevice); + + testRestClient.postTelemetry(newDeviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + + await().alias("update telemetry -> no updates").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(newDevice.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("53.6"); + }); + + testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); + } + + private CalculatedField createSimpleCalculatedField() { + return createSimpleCalculatedField(device.getId()); + } + + private CalculatedField createSimpleCalculatedField(EntityId entityId) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(entityId); + calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setName("C to F" + RandomStringUtils.randomAlphabetic(5)); + calculatedField.setDebugSettings(DebugSettings.all()); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument argument = new Argument(); + ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); + argument.setRefEntityKey(refEntityKey); + argument.setDefaultValue("12"); // not used because real telemetry value in db is present + config.setArguments(Map.of("T", argument)); + + config.setExpression("(T * 9/5) + 32"); + + Output output = new Output(); + output.setName("fahrenheitTemp"); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(2); + config.setOutput(output); + + calculatedField.setConfiguration(config); + + return testRestClient.postCalculatedField(calculatedField); + } + + private CalculatedField createScriptCalculatedField() { + return createScriptCalculatedField(device.getId()); + } + + private CalculatedField createScriptCalculatedField(EntityId entityId) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(entityId); + calculatedField.setType(CalculatedFieldType.SCRIPT); + calculatedField.setName("Air density" + RandomStringUtils.randomAlphabetic(5)); + calculatedField.setDebugSettings(DebugSettings.all()); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument argument1 = new Argument(); + argument1.setRefEntityId(asset.getId()); + ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("altitude", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE); + argument1.setRefEntityKey(refEntityKey1); + config.setArguments(Map.of("altitude", argument1)); + Argument argument2 = new Argument(); + ReferencedEntityKey refEntityKey2 = new ReferencedEntityKey("temperatureInF", ArgumentType.TS_ROLLING, null); + argument2.setRefEntityKey(refEntityKey2); + config.setArguments(Map.of("temperature", argument2)); + + config.setExpression("return {\"airDensity\": 5};"); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + config.setOutput(output); + + calculatedField.setConfiguration(config); + + return testRestClient.postCalculatedField(calculatedField); + } + + private Device createDevice(String name, DeviceProfileId deviceProfileId) { + Device device = new Device(); + device.setName(name); + device.setType("default"); + device.setDeviceProfileId(deviceProfileId); + DeviceData deviceData = new DeviceData(); + deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); + deviceData.setConfiguration(new DefaultDeviceConfiguration()); + device.setDeviceData(deviceData); + return device; + } + + private Asset createAsset(String name, AssetProfileId assetProfileId) { + Asset asset = new Asset(); + asset.setName(name); + asset.setAssetProfileId(assetProfileId); + return asset; + } + +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java index b96ea386d2..4e28340f9d 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java @@ -77,7 +77,7 @@ public class HttpClientTest extends AbstractContainerTest { assertThat(accessToken).isNotNull(); JsonNode sharedAttribute = mapper.readTree(createPayload().toString()); - testRestClient.postTelemetryAttribute(DEVICE, device.getId(), SHARED_SCOPE, sharedAttribute); + testRestClient.postTelemetryAttribute(device.getId(), SHARED_SCOPE, sharedAttribute); JsonNode clientAttribute = mapper.readTree(createPayload().toString()); testRestClient.postAttribute(accessToken, clientAttribute); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java index 00e496327c..ebdfb4e3c9 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java @@ -212,7 +212,7 @@ public class MqttClientTest extends AbstractContainerTest { String sharedAttributeValue = StringUtils.randomAlphanumeric(8); sharedAttributes.addProperty("sharedAttr", sharedAttributeValue); JsonNode sharedAttribute = mapper.readTree(sharedAttributes.toString()); - testRestClient.postTelemetryAttribute(DEVICE, device.getId(), SHARED_SCOPE, sharedAttribute); + testRestClient.postTelemetryAttribute(device.getId(), SHARED_SCOPE, sharedAttribute); // Subscribe to attributes response mqttClient.on("v1/devices/me/attributes/response/+", listener, MqttQoS.AT_LEAST_ONCE).get(); @@ -255,7 +255,7 @@ public class MqttClientTest extends AbstractContainerTest { sharedAttributes.addProperty(sharedAttributeName, sharedAttributeValue); JsonNode sharedAttribute = mapper.readTree(sharedAttributes.toString()); - testRestClient.postTelemetryAttribute(DataConstants.DEVICE, device.getId(), SHARED_SCOPE, sharedAttribute); + testRestClient.postTelemetryAttribute(device.getId(), SHARED_SCOPE, sharedAttribute); MqttEvent event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get(sharedAttributeName).asText()) @@ -265,7 +265,7 @@ public class MqttClientTest extends AbstractContainerTest { JsonObject updatedSharedAttributes = new JsonObject(); String updatedSharedAttributeValue = StringUtils.randomAlphanumeric(8); updatedSharedAttributes.addProperty(sharedAttributeName, updatedSharedAttributeValue); - testRestClient.postTelemetryAttribute(DEVICE, device.getId(), SHARED_SCOPE, mapper.readTree(updatedSharedAttributes.toString())); + testRestClient.postTelemetryAttribute(device.getId(), SHARED_SCOPE, mapper.readTree(updatedSharedAttributes.toString())); event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get(sharedAttributeName).asText()) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java index b2335ae2f7..cc587fbbd5 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java @@ -184,7 +184,7 @@ public class MqttGatewayClientTest extends AbstractContainerTest { mqttClient.on("v1/gateway/attributes/response", listener, MqttQoS.AT_LEAST_ONCE).get(); - testRestClient.postTelemetryAttribute(DataConstants.DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString())); + testRestClient.postTelemetryAttribute(createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString())); var event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); JsonObject requestData = new JsonObject(); @@ -266,7 +266,7 @@ public class MqttGatewayClientTest extends AbstractContainerTest { // Subscribe for attribute update event mqttClient.on("v1/gateway/attributes", listener, MqttQoS.AT_LEAST_ONCE).get(); - testRestClient.postTelemetryAttribute(DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString())); + testRestClient.postTelemetryAttribute(createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString())); MqttEvent sharedAttributeEvent = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); // Catch attribute update event @@ -299,7 +299,7 @@ public class MqttGatewayClientTest extends AbstractContainerTest { gatewaySharedAttributeValue.addProperty("device", createdDevice.getName()); gatewaySharedAttributeValue.add("data", sharedAttributes); - testRestClient.postTelemetryAttribute(DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString())); + testRestClient.postTelemetryAttribute(createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString())); MqttEvent event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get("data").get(sharedAttributeName).asText()) @@ -314,7 +314,7 @@ public class MqttGatewayClientTest extends AbstractContainerTest { gatewayUpdatedSharedAttributeValue.addProperty("device", createdDevice.getName()); gatewayUpdatedSharedAttributeValue.add("data", updatedSharedAttributes); - testRestClient.postTelemetryAttribute(DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(updatedSharedAttributes.toString())); + testRestClient.postTelemetryAttribute(createdDevice.getId(), SHARED_SCOPE, mapper.readTree(updatedSharedAttributes.toString())); event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get("data").get(sharedAttributeName).asText()) .isEqualTo(updatedSharedAttributeValue); diff --git a/msa/edqs/pom.xml b/msa/edqs/pom.xml index f22cb0187c..b790cc0802 100644 --- a/msa/edqs/pom.xml +++ b/msa/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC msa org.thingsboard.msa @@ -34,7 +34,7 @@ UTF-8 ${basedir}/../.. - edqs + tb-edqs tb-edqs /var/log/${pkg.name} /usr/share/${pkg.name} diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index 9f89413b4b..22e553fbb3 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC msa org.thingsboard.msa diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml index 3de71adb36..a1208a6258 100644 --- a/msa/monitoring/pom.xml +++ b/msa/monitoring/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC msa diff --git a/msa/pom.xml b/msa/pom.xml index 98e820daaf..ce8b422d05 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC thingsboard msa diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index b47aec986c..346776698a 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC msa org.thingsboard.msa diff --git a/msa/tb/docker-cassandra/Dockerfile b/msa/tb/docker-cassandra/Dockerfile index 9c49ffe2f9..20a0d8cf7a 100644 --- a/msa/tb/docker-cassandra/Dockerfile +++ b/msa/tb/docker-cassandra/Dockerfile @@ -29,7 +29,6 @@ ENV CASSANDRA_DATA=/data/cassandra ENV SPRING_DRIVER_CLASS_NAME=org.postgresql.Driver ENV SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/thingsboard ENV SPRING_DATASOURCE_USERNAME=${pkg.user} -ENV SPRING_DATASOURCE_PASSWORD=postgres ENV CASSANDRA_HOST=localhost ENV CASSANDRA_PORT=9042 diff --git a/msa/tb/docker-postgres/Dockerfile b/msa/tb/docker-postgres/Dockerfile index f9e36ed61e..7c66d626c7 100644 --- a/msa/tb/docker-postgres/Dockerfile +++ b/msa/tb/docker-postgres/Dockerfile @@ -29,7 +29,6 @@ ENV PATH=$PATH:/usr/lib/postgresql/$PG_MAJOR/bin ENV SPRING_DRIVER_CLASS_NAME=org.postgresql.Driver ENV SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/thingsboard ENV SPRING_DATASOURCE_USERNAME=${pkg.user} -ENV SPRING_DATASOURCE_PASSWORD=postgres ENV PGLOG=/var/log/postgres diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 1e9774cb42..ad3ec5f7c2 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC msa org.thingsboard.msa diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index 71c86d50a4..e3f51c2044 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.0.0-SNAPSHOT + 4.0.0-RC transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index e6b5611166..193d2e28d1 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.0.0-SNAPSHOT + 4.0.0-RC transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml index 9088a9c996..1722cb906e 100644 --- a/msa/transport/lwm2m/pom.xml +++ b/msa/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.0.0-SNAPSHOT + 4.0.0-RC transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index a61bc62921..c03c22eef3 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.0.0-SNAPSHOT + 4.0.0-RC transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index 46ea6612f9..4d9e341369 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC msa org.thingsboard.msa diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index 0664a89d9a..21e1d3c5d0 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 4.0.0-SNAPSHOT + 4.0.0-RC org.thingsboard.msa.transport diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml index c7e3ee5e11..509807600f 100644 --- a/msa/vc-executor-docker/pom.xml +++ b/msa/vc-executor-docker/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC msa org.thingsboard.msa diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml index f2e4395766..8a3d922269 100644 --- a/msa/vc-executor/pom.xml +++ b/msa/vc-executor/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC msa org.thingsboard.msa diff --git a/msa/vc-executor/src/main/resources/tb-vc-executor.yml b/msa/vc-executor/src/main/resources/tb-vc-executor.yml index f0b1426cc7..bb7f607ca0 100644 --- a/msa/vc-executor/src/main/resources/tb-vc-executor.yml +++ b/msa/vc-executor/src/main/resources/tb-vc-executor.yml @@ -207,7 +207,9 @@ usage: # Enable/Disable collection of statistics about API usage on a customer level enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds - interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + interval: "${USAGE_STATS_REPORT_INTERVAL:60}" + # Amount of statistic messages in pack + pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" # Metrics parameters metrics: diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index f6f06c994a..c7e4e3adf4 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index bf55ec3083..b5fd83a54f 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC thingsboard netty-mqtt - 4.0.0-SNAPSHOT + 4.0.0-RC jar Netty MQTT Client diff --git a/pom.xml b/pom.xml index c0dc7384f7..72195b1e4f 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC pom Thingsboard diff --git a/rest-client/pom.xml b/rest-client/pom.xml index 09f7e34c2c..fd96de9f70 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC thingsboard rest-client diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index b3681c9d69..8a4fbf7601 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index d036695cd3..6993789e0f 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 4edafdf69e..eb568f9cc7 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC rule-engine org.thingsboard.rule-engine diff --git a/tools/pom.xml b/tools/pom.xml index 26e0543ab1..bc8e748c6e 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC thingsboard tools diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index 3cf7d677bf..0c6de9ea4e 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC transport org.thingsboard.transport diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index f8df4bb55e..f60a6bd47e 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -411,7 +411,9 @@ usage: # Enable/Disable collection of statistics about API usage on a customer level enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds - interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + interval: "${USAGE_STATS_REPORT_INTERVAL:60}" + # Amount of statistic messages in pack + pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" # Metrics parameters metrics: diff --git a/transport/http/pom.xml b/transport/http/pom.xml index 91fdd39755..226bab23b3 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC transport org.thingsboard.transport diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index d282b50ff3..c921f9f9ae 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -360,7 +360,9 @@ usage: # Enable/Disable collection of statistics about API usage on a customer level enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds - interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + interval: "${USAGE_STATS_REPORT_INTERVAL:60}" + # Amount of statistic messages in pack + pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" # Metrics parameters metrics: diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml index cc929a0090..6f24b35684 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC transport org.thingsboard.transport diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index a198613e11..85e865e60e 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -461,7 +461,9 @@ usage: # Enable/Disable collection of statistics about API usage on a customer level enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds - interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + interval: "${USAGE_STATS_REPORT_INTERVAL:60}" + # Amount of statistic messages in pack + pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" # Metrics parameters metrics: diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index da818e032b..4d92c1f0e6 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC transport org.thingsboard.transport diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index fb75203499..a6ca2f1a6e 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -394,7 +394,9 @@ usage: # Enable/Disable collection of statistics about API usage on a customer level enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds - interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + interval: "${USAGE_STATS_REPORT_INTERVAL:60}" + # Amount of statistic messages in pack + pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" # Metrics parameters metrics: diff --git a/transport/pom.xml b/transport/pom.xml index 66026fa10b..8be6fff261 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC thingsboard transport diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index 0a36ea1f90..62e1f753e5 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC transport diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index 281e221674..6848e8af26 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -347,7 +347,9 @@ usage: # Enable/Disable collection of statistics about API usage on a customer level enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}" # Interval of reporting the statistics. By default, the summarized statistics are sent every 10 seconds - interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + interval: "${USAGE_STATS_REPORT_INTERVAL:60}" + # Amount of statistic messages in pack + pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" # Metrics parameters metrics: diff --git a/ui-ngx/package.json b/ui-ngx/package.json index b2e2dc18c3..6280fb94c2 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -46,7 +46,7 @@ "canvas-gauges": "^2.1.7", "core-js": "^3.39.0", "dayjs": "1.11.13", - "echarts": "https://github.com/thingsboard/echarts/archive/5.5.0-TB.tar.gz", + "echarts": "https://github.com/thingsboard/echarts/archive/5.5.1-TB.tar.gz", "flot": "https://github.com/thingsboard/flot.git#0.9-work", "flot.curvedlines": "https://github.com/MichaelZinsmaier/CurvedLines.git#master", "font-awesome": "^4.7.0", @@ -61,9 +61,10 @@ "leaflet": "1.9.4", "leaflet-polylinedecorator": "1.6.0", "leaflet-providers": "2.0.0", - "leaflet.gridlayer.googlemutant": "0.14.1", + "leaflet.gridlayer.googlemutant": "0.15.0", "leaflet.markercluster": "1.5.3", "libphonenumber-js": "^1.11.15", + "maplibre-gl": "^5.2.0", "marked": "~12.0.2", "moment": "^2.30.1", "moment-timezone": "^0.5.45", diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index eefea60273..c6e5018d46 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.0.0-SNAPSHOT + 4.0.0-RC thingsboard org.thingsboard diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 7879e4d3bc..01fed28bf3 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -287,6 +287,11 @@ export class EntityService { case EntityType.OAUTH2_CLIENT: observable = this.oauth2Service.findTenantOAuth2ClientInfosByIds(entityIds, config); break; + case EntityType.RULE_CHAIN: + observable = this.getEntitiesByIdsObservable( + (id) => this.ruleChainService.getRuleChain(id, config), + entityIds); + break; } return observable; } diff --git a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts index 75bfa6b1e1..c062ac2e82 100644 --- a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts +++ b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts @@ -15,9 +15,9 @@ /// import { Component, Injectable, Type, ɵComponentDef, ɵNG_COMP_DEF } from '@angular/core'; -import { from, Observable, of } from 'rxjs'; +import { from, Observable, shareReplay } from 'rxjs'; import { CommonModule } from '@angular/common'; -import { mergeMap } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { guid } from '@core/utils'; @Injectable({ @@ -25,6 +25,10 @@ import { guid } from '@core/utils'; }) export class DynamicComponentFactoryService { + private compiler$: Observable = from(import('@angular/compiler')).pipe( + shareReplay({refCount: true, bufferSize: 1}) + ); + constructor() { } @@ -34,14 +38,14 @@ export class DynamicComponentFactoryService { imports?: Type[], preserveWhitespaces?: boolean, styles?: string[]): Observable> { - return from(import('@angular/compiler')).pipe( - mergeMap(() => { + return this.compiler$.pipe( + map(() => { let componentImports: Type[] = [CommonModule]; if (imports) { componentImports = [...componentImports, ...imports]; } const comp = this.createAndCompileDynamicComponent(componentType, template, componentImports, preserveWhitespaces, styles); - return of(comp.type); + return comp.type; }) ); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index cd9f0373db..1cf44966fc 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -33,36 +33,32 @@ import { AppState } from '@core/core.state'; import { getCurrentAuthState, getCurrentAuthUser } from '@core/auth/auth.selectors'; import { DestroyRef, Renderer2 } from '@angular/core'; import { EntityDebugSettings } from '@shared/models/entity.models'; -import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { TbPopoverService } from '@shared/components/popover.service'; -import { EntityDebugSettingsPanelComponent } from '@home/components/entity/debug/entity-debug-settings-panel.component'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { catchError, filter, switchMap, tap } from 'rxjs/operators'; import { ArgumentType, CalculatedField, CalculatedFieldEventArguments, - CalculatedFieldDebugDialogData, - CalculatedFieldDialogData, - CalculatedFieldTestScriptDialogData, + CalculatedFieldType, + CalculatedFieldTypeTranslations, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights, - CalculatedFieldTypeTranslations, - CalculatedFieldType, } from '@shared/models/calculated-field.models'; import { - CalculatedFieldDebugDialogComponent, + CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogData, CalculatedFieldDialogComponent, - CalculatedFieldScriptTestDialogComponent + CalculatedFieldDialogData, + CalculatedFieldScriptTestDialogComponent, + CalculatedFieldTestScriptDialogData } from './components/public-api'; import { ImportExportService } from '@shared/import-export/import-export.service'; import { isObject } from '@core/utils'; +import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; -export class CalculatedFieldsTableConfig extends EntityTableConfig { +export class CalculatedFieldsTableConfig extends EntityTableConfig { - // TODO: [Calculated Fields] remove hardcode when BE variable implemented readonly calculatedFieldsDebugPerTenantLimitsConfiguration = getCurrentAuthState(this.store)['calculatedFieldsDebugPerTenantLimitsConfiguration'] || '1:1'; readonly maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE; @@ -78,12 +74,11 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig, - private durationLeft: DurationLeftPipe, - private popoverService: TbPopoverService, private destroyRef: DestroyRef, private renderer: Renderer2, public entityName: string, - private importExportService: ImportExportService + private importExportService: ImportExportService, + private entityDebugSettingsService: EntityDebugSettingsService, ) { super(); this.tableTitle = this.translate.instant('entity.type-calculated-fields'); @@ -147,10 +142,10 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.getDebugConfigLabel(entity?.debugSettings), + nameFunction: entity => this.entityDebugSettingsService.getDebugConfigLabel(entity?.debugSettings), icon: 'mdi:bug', isEnabled: () => true, - iconFunction: ({ debugSettings }) => this.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline', + iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline', onAction: ($event, entity) => this.onOpenDebugConfig($event, entity), }, { @@ -180,30 +175,26 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.openDebugEventsDialog(calculatedField) }; - const { viewContainerRef } = this.getTable(); if ($event) { $event.stopPropagation(); } - const trigger = $event.target as Element; - if (this.popoverService.hasPopover(trigger)) { - this.popoverService.hidePopover(trigger); - } else { - const debugStrategyPopover = this.popoverService.displayPopover(trigger, this.renderer, - viewContainerRef, EntityDebugSettingsPanelComponent, 'bottom', true, null, - { - debugLimitsConfiguration: this.calculatedFieldsDebugPerTenantLimitsConfiguration, - maxDebugModeDuration: this.maxDebugModeDuration, - entityLabel: this.translate.instant('debug-settings.calculated-field'), - additionalActionConfig, - ...debugSettings - }, - {}, - {}, {}, true); - debugStrategyPopover.tbComponentRef.instance.onSettingsApplied.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((settings: EntityDebugSettings) => { - this.onDebugConfigChanged(id.id, settings); - debugStrategyPopover.hide(); - }); + + const { viewContainerRef, renderer } = this.entityDebugSettingsService; + if (!viewContainerRef || !renderer) { + this.entityDebugSettingsService.viewContainerRef = this.getTable().viewContainerRef; + this.entityDebugSettingsService.renderer = this.renderer; } + + this.entityDebugSettingsService.openDebugStrategyPanel({ + debugSettings, + debugConfig: { + debugLimitsConfiguration: this.calculatedFieldsDebugPerTenantLimitsConfiguration, + maxDebugModeDuration: this.maxDebugModeDuration, + entityLabel: this.translate.instant('debug-settings.calculated-field'), + additionalActionConfig, + }, + onSettingsAppliedFn: settings => this.onDebugConfigChanged(id.id, settings) + }, $event.target as Element); } private editCalculatedField(calculatedField: CalculatedField, isDirty = false): void { @@ -258,25 +249,18 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.getCalculatedFieldDialog(calculatedField, 'action.add')), + filter(Boolean), + switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)), + filter(Boolean), + takeUntilDestroyed(this.destroyRef) + ) .subscribe(() => this.updateData()); } - private getDebugConfigLabel(debugSettings: EntityDebugSettings): string { - const isDebugActive = this.isDebugActive(debugSettings?.allEnabledUntil); - - if (!isDebugActive) { - return debugSettings?.failuresEnabled ? this.translate.instant('debug-settings.failures') : this.translate.instant('common.disabled'); - } else { - return this.durationLeft.transform(debugSettings?.allEnabledUntil); - } - } - - private isDebugActive(allEnabledUntil: number): boolean { - return allEnabledUntil > new Date().getTime(); - } - private onDebugConfigChanged(id: string, debugSettings: EntityDebugSettings): void { this.calculatedFieldsService.getCalculatedFieldById(id).pipe( switchMap(field => this.calculatedFieldsService.saveCalculatedField({ ...field, debugSettings })), diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts index 1d0b9dcb15..e10c4b301e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -31,10 +31,9 @@ import { MatDialog } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { CalculatedFieldsTableConfig } from '@home/components/calculated-fields/calculated-fields-table-config'; -import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; -import { TbPopoverService } from '@shared/components/popover.service'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { ImportExportService } from '@shared/import-export/import-export.service'; +import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; @Component({ @@ -42,6 +41,7 @@ import { DatePipe } from '@angular/common'; templateUrl: './calculated-fields-table.component.html', styleUrls: ['./calculated-fields-table.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, + providers: [EntityDebugSettingsService] }) export class CalculatedFieldsTableComponent { @@ -58,11 +58,10 @@ export class CalculatedFieldsTableComponent { private dialog: MatDialog, private store: Store, private datePipe: DatePipe, - private durationLeft: DurationLeftPipe, - private popoverService: TbPopoverService, private cd: ChangeDetectorRef, private renderer: Renderer2, private importExportService: ImportExportService, + private entityDebugSettingsService: EntityDebugSettingsService, private destroyRef: DestroyRef) { effect(() => { @@ -74,12 +73,11 @@ export class CalculatedFieldsTableComponent { this.datePipe, this.entityId(), this.store, - this.durationLeft, - this.popoverService, this.destroyRef, this.renderer, this.entityName(), - this.importExportService + this.importExportService, + this.entityDebugSettingsService, ); this.cd.markForCheck(); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html index 162bd3aa1e..abb34cb502 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html @@ -97,7 +97,7 @@ matTooltipPosition="above"> ([]); entityNameMap = new Map(); + entityNameErrorSet = new Set(); sortOrder = { direction: 'asc', property: '' }; dataSource = new CalculatedFieldArgumentDatasource(); @@ -168,6 +171,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces buttonTitle: this.argumentsFormArray.at(index)?.value ? 'action.apply' : 'action.add', tenantId: this.tenantId, entityName: this.entityName, + entityHasError: this.entityNameErrorSet.has(argument.refEntityId?.id), usedArgumentNames: this.argumentsFormArray.value.map(({ argumentName }) => argumentName).filter(name => name !== argument.argumentName), }; this.popoverComponent = this.popoverService.displayPopover(trigger, this.renderer, @@ -198,6 +202,8 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces if (this.calculatedFieldType === CalculatedFieldType.SIMPLE && this.argumentsFormArray.controls.some(control => control.value.refEntityKey.type === ArgumentType.Rolling)) { this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling'; + } else if (this.entityNameErrorSet.size) { + this.errorText = 'calculated-fields.hint.arguments-entity-not-found'; } else if (!this.argumentsFormArray.controls.length) { this.errorText = 'calculated-fields.hint.arguments-empty'; } else { @@ -234,11 +240,18 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces } private updateEntityNameMap(value: CalculatedFieldArgumentValue[]): void { + this.entityNameErrorSet.clear(); value.forEach(({ refEntityId = {}}) => { if (refEntityId.id && !this.entityNameMap.has(refEntityId.id)) { const { id, entityType } = refEntityId as EntityId; this.entityService.getEntity(entityType as EntityType, id, { ignoreLoading: true, ignoreErrors: true }) - .pipe(takeUntilDestroyed(this.destroyRef)) + .pipe( + catchError(() => { + this.entityNameErrorSet.add(id); + return NEVER; + }), + takeUntilDestroyed(this.destroyRef) + ) .subscribe(entity => this.entityNameMap.set(id, entity.name)); } }); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts index 8618a11990..0fcac393f0 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts @@ -20,9 +20,19 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; -import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models'; +import { CalculatedFieldEventBody, DebugEventType, Event, EventType } from '@shared/models/event.models'; import { EventTableComponent } from '@home/components/event/event-table.component'; -import { CalculatedFieldDebugDialogData, CalculatedFieldType } from '@shared/models/calculated-field.models'; +import { + CalculatedField, + CalculatedFieldTestScriptFn, + CalculatedFieldType +} from '@shared/models/calculated-field.models'; + +export interface CalculatedFieldDebugDialogData { + tenantId: string; + value: CalculatedField; + getTestScriptDialogFn: CalculatedFieldTestScriptFn; +} @Component({ selector: 'tb-calculated-field-debug-dialog', @@ -46,7 +56,7 @@ export class CalculatedFieldDebugDialogComponent extends DialogComponent this.data.value.type === CalculatedFieldType.SCRIPT; + this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = (event => this.data.value.type === CalculatedFieldType.SCRIPT && !!(event as Event).body.arguments) } cancel(): void { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index 471c722145..813f6b1b4c 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -102,6 +102,7 @@ [scriptLanguage]="ScriptLanguage.TBEL" [highlightRules]="argumentsHighlightRules$ | async" [editorCompleter]="argumentsEditorCompleter$ | async" + [helpPopupStyle]="{ width: '1200px' }" helpId="calculated-field/expression_fn" >
{{ 'api-usage.tbel' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index 052b660c3c..169d6dff50 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -25,7 +25,7 @@ import { CalculatedField, CalculatedFieldConfiguration, calculatedFieldDefaultScript, - CalculatedFieldDialogData, + CalculatedFieldTestScriptFn, CalculatedFieldType, CalculatedFieldTypeTranslations, getCalculatedFieldArgumentsEditorCompleter, @@ -41,6 +41,20 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ScriptLanguage } from '@shared/models/rule-node.models'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { Observable } from 'rxjs'; +import { EntityId } from '@shared/models/id/entity-id'; +import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; + +export interface CalculatedFieldDialogData { + value?: CalculatedField; + buttonTitle: string; + entityId: EntityId; + debugLimitsConfiguration: string; + tenantId: string; + entityName?: string; + additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>; + getTestScriptDialogFn: CalculatedFieldTestScriptFn; + isDirty?: boolean; +} @Component({ selector: 'tb-calculated-field-dialog', diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html index 79f2b6ebbc..15286af377 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html @@ -82,6 +82,7 @@
{{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}
} @else {
-
{{ 'calculated-fields.time-window' | translate }}
+
{{ 'calculated-fields.time-window' | translate }}
@if (maxDataPointsPerRollingArg) {
-
{{ 'calculated-fields.limit' | translate }}
+
{{ 'calculated-fields.limit' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts index 482851c59c..8aa61eb1a4 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, Input, OnInit, output } from '@angular/core'; +import { AfterViewInit, ChangeDetectorRef, Component, Input, OnInit, output, ViewChild } from '@angular/core'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; import { charsWithNumRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; @@ -41,13 +41,14 @@ import { MINUTE } from '@shared/models/time/time.models'; import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { AppState } from '@core/core.state'; import { Store } from '@ngrx/store'; +import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; @Component({ selector: 'tb-calculated-field-argument-panel', templateUrl: './calculated-field-argument-panel.component.html', styleUrls: ['./calculated-field-argument-panel.component.scss'] }) -export class CalculatedFieldArgumentPanelComponent implements OnInit { +export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewInit { @Input() buttonTitle: string; @Input() index: number; @@ -55,9 +56,12 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit { @Input() entityId: EntityId; @Input() tenantId: string; @Input() entityName: string; + @Input() entityHasError: boolean; @Input() calculatedFieldType: CalculatedFieldType; @Input() usedArgumentNames: string[]; + @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent; + argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>(); readonly maxDataPointsPerRollingArg = getCurrentAuthState(this.store).maxDataPointsPerRollingArg; @@ -75,8 +79,8 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit { scope: [{ value: AttributeScope.SERVER_SCOPE, disabled: true }, [Validators.required]], }), defaultValue: ['', [Validators.pattern(oneSpaceInsideRegex)]], - limit: [{ value: this.defaultLimit, disabled: !this.maxDataPointsPerRollingArg }], - timeWindow: [MINUTE * 15], + limit: [{ value: this.defaultLimit, disabled: !this.maxDataPointsPerRollingArg }, [Validators.required, Validators.min(1), Validators.max(this.maxDataPointsPerRollingArg)]], + timeWindow: [MINUTE * 15, [Validators.required]], }); argumentTypes: ArgumentType[]; @@ -136,6 +140,12 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit { .filter(type => type !== ArgumentType.Rolling || this.calculatedFieldType === CalculatedFieldType.SCRIPT); } + ngAfterViewInit(): void { + if (this.entityHasError) { + this.entityAutocomplete.selectEntityFormGroup.get('entity').markAsTouched(); + } + } + saveArgument(): void { const { refEntityId, ...restConfig } = this.argumentFormGroup.value; const value = (refEntityId.entityType === ArgumentEntityType.Current ? restConfig : { refEntityId, ...restConfig }) as CalculatedFieldArgumentValue; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html index 6859ff6dab..942d9484b5 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html @@ -44,6 +44,7 @@ [highlightRules]="data.argumentsHighlightRules" [scriptLanguage]="ScriptLanguage.TBEL" [editorCompleter]="data.argumentsEditorCompleter" + [helpPopupStyle]="{ width: '1200px' }" resultType="object" helpId="calculated-field/expression_fn" /> diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts index 4e15e4a240..56256511c9 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts @@ -42,9 +42,17 @@ import { filter } from 'rxjs/operators'; import { ArgumentType, CalculatedFieldEventArguments, - CalculatedFieldTestScriptDialogData, + CalculatedFieldTestScriptInputParams, TestArgumentTypeMap } from '@shared/models/calculated-field.models'; +import { TbEditorCompleter } from '@shared/models/ace/completion.models'; +import { AceHighlightRules } from '@shared/models/ace/ace.models'; + +export interface CalculatedFieldTestScriptDialogData extends CalculatedFieldTestScriptInputParams { + argumentsEditorCompleter: TbEditorCompleter; + argumentsHighlightRules: AceHighlightRules; + openCalculatedFieldEdit?: boolean; +} @Component({ selector: 'tb-calculated-field-script-test-dialog', diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts index b732b7ff09..510fc1ce63 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts @@ -174,66 +174,71 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo private initHotKeys(): void { this.hotKeys.push( - new Hotkey('ctrl+c', (event: KeyboardEvent) => { + new Hotkey(['ctrl+c', 'meta+c'], (event: KeyboardEvent) => { if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { const widget = this.dashboard.getSelectedWidget(); if (widget) { event.preventDefault(); this.copyWidget(event, widget); } + return false; } - return false; + return true; }, null, this.translate.instant('action.copy')) ); this.hotKeys.push( - new Hotkey('ctrl+r', (event: KeyboardEvent) => { + new Hotkey(['ctrl+r', 'meta+r'], (event: KeyboardEvent) => { if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { const widget = this.dashboard.getSelectedWidget(); if (widget) { event.preventDefault(); this.copyWidgetReference(event, widget); } + return false; } - return false; + return true; }, null, this.translate.instant('action.copy-reference')) ); this.hotKeys.push( - new Hotkey('ctrl+v', (event: KeyboardEvent) => { + new Hotkey(['ctrl+v', 'meta+v'], (event: KeyboardEvent) => { if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { if (this.itembuffer.hasWidget()) { event.preventDefault(); this.pasteWidget(event); } + return false; } - return false; + return true; }, null, this.translate.instant('action.paste')) ); this.hotKeys.push( - new Hotkey('ctrl+i', (event: KeyboardEvent) => { + new Hotkey(['ctrl+i', 'meta+i'], (event: KeyboardEvent) => { if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(), this.dashboardCtx.state, this.layoutCtx.id, this.layoutCtx.breakpoint)) { event.preventDefault(); this.pasteWidgetReference(event); } + return false; } - return false; + return true; }, null, this.translate.instant('action.paste-reference')) ); this.hotKeys.push( - new Hotkey('ctrl+x', (event: KeyboardEvent) => { + new Hotkey(['ctrl+x', 'meta+x'], (event: KeyboardEvent) => { if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { const widget = this.dashboard.getSelectedWidget(); if (widget) { event.preventDefault(); this.layoutCtx.dashboardCtrl.removeWidget(event, this.layoutCtx, widget); } + return false; } - return false; + return true; }, null, this.translate.instant('action.delete')) ); diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts index cc013fe34d..95f2095bad 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts @@ -247,6 +247,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo defaultItemCols: 8, defaultItemRows: 6, displayGrid: this.displayGrid, + useTransformPositioning: false, resizable: { enabled: this.isEdit && !this.isEditingWidget, delayStart: 50, diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.html b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.html index a6033df8cb..4243044fc8 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.html @@ -21,7 +21,7 @@ #matButton [class.active]="((isDebugAllActive$ | async) || failuresEnabled) && !disabled" [disabled]="disabled" - (click)="openDebugStrategyPanel($event, matButton)"> + (click)="onOpenDebugStrategyPanel($event, matButton)"> bug_report @if (isDebugAllActive$ | async) { {{ (allEnabled$ | async) === false ? (allEnabledUntil | durationLeft) : (maxDebugModeDuration | milliSecondsToTimeString: true : true) }} diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts index 852da5f7bf..85a90aef63 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts @@ -20,24 +20,22 @@ import { Component, forwardRef, Input, - Renderer2, - ViewContainerRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; -import { TbPopoverService } from '@shared/components/popover.service'; import { MatButton } from '@angular/material/button'; -import { EntityDebugSettingsPanelComponent } from './entity-debug-settings-panel.component'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { BehaviorSubject, of, shareReplay, timer } from 'rxjs'; import { SECOND, MINUTE } from '@shared/models/time/time.models'; -import { AdditionalDebugActionConfig, EntityDebugSettings } from '@shared/models/entity.models'; +import { EntityDebugSettings } from '@shared/models/entity.models'; import { map, switchMap, takeWhile } from 'rxjs/operators'; import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { AppState } from '@core/core.state'; import { Store } from '@ngrx/store'; import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; +import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; @Component({ selector: 'tb-entity-debug-settings-button', @@ -54,6 +52,7 @@ import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/f useExisting: forwardRef(() => EntityDebugSettingsButtonComponent), multi: true }, + EntityDebugSettingsService ], changeDetection: ChangeDetectionStrategy.OnPush }) @@ -92,11 +91,9 @@ export class EntityDebugSettingsButtonComponent implements ControlValueAccessor private propagateChange: (settings: EntityDebugSettings) => void = () => {}; - constructor(private popoverService: TbPopoverService, - private renderer: Renderer2, - private store: Store, - private viewContainerRef: ViewContainerRef, + constructor(private store: Store, private fb: FormBuilder, + private entityDebugSettingsService: EntityDebugSettingsService, private cd : ChangeDetectorRef, ) { this.debugSettingsFormGroup.valueChanges.pipe( @@ -118,33 +115,23 @@ export class EntityDebugSettingsButtonComponent implements ControlValueAccessor return this.debugSettingsFormGroup.get('allEnabledUntil').value; } - openDebugStrategyPanel($event: Event, matButton: MatButton): void { + onOpenDebugStrategyPanel($event: Event, matButton: MatButton): void { if ($event) { $event.stopPropagation(); } - const trigger = matButton._elementRef.nativeElement; - const debugSettings = this.debugSettingsFormGroup.value; - - if (this.popoverService.hasPopover(trigger)) { - this.popoverService.hidePopover(trigger); - } else { - const debugStrategyPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, EntityDebugSettingsPanelComponent, 'bottom', true, null, - { - ...debugSettings, - maxDebugModeDuration: this.maxDebugModeDuration, - debugLimitsConfiguration: this.debugLimitsConfiguration, - entityLabel: this.entityLabel, - additionalActionConfig: this.additionalActionConfig, - }, - {}, - {}, {}, true); - debugStrategyPopover.tbComponentRef.instance.onSettingsApplied.subscribe((settings: EntityDebugSettings) => { + this.entityDebugSettingsService.openDebugStrategyPanel({ + debugSettings: this.debugSettingsFormGroup.value, + debugConfig: { + maxDebugModeDuration: this.maxDebugModeDuration, + debugLimitsConfiguration: this.debugLimitsConfiguration, + entityLabel: this.entityLabel, + additionalActionConfig: this.additionalActionConfig, + }, + onSettingsAppliedFn: settings => { this.debugSettingsFormGroup.patchValue(settings); this.cd.markForCheck(); - debugStrategyPopover.hide(); - }); - } + } + }, matButton._elementRef.nativeElement); } registerOnChange(fn: (settings: EntityDebugSettings) => void): void { diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html index cf8c0ae14d..4a9bc57d8d 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html @@ -15,17 +15,15 @@ limitations under the License. --> -
+
debug-settings.label
-
-
- @if (debugLimitsConfiguration) { + @if (debugLimitsConfiguration) { +
+
{{ 'debug-settings.hint.main-limited' | translate: { entity: entityLabel ?? ('debug-settings.entity' | translate), msg: maxMessagesCount, time: (maxTimeFrameDuration | milliSecondsToTimeString: true : true) } }} - } @else { - {{ 'debug-settings.hint.main' | translate }} - } +
-
+ }
@@ -33,12 +31,12 @@
- +
- {{ 'debug-settings.all-messages' | translate: { time: (isDebugAllActive$ | async) && !allEnabled ? (allEnabledUntil | durationLeft) : (maxDebugModeDuration | milliSecondsToTimeString: true : true) } }} + {{ 'debug-settings.all-messages' | translate: { time: (isDebugAllActive$ | async) && !allEnabled && debugAllControl.untouched ? (allEnabledUntil | durationLeft) : (maxDebugModeDuration | milliSecondsToTimeString: true : true) } }}
-
-
-
{{'widget-config.icon' | translate}}
- - -
+ @if (widgetActionFormGroup.get('actionSourceId').value === 'headerButton') { +
+
widget-config.header-button.button-settings
+
+
widget-config.header-button.button-type
+ + + + {{ widgetHeaderActionButtonTypeTranslationMap.get(widgetHeaderActionButtonType[button]) | translate }} + + + +
+
+
+ + +
widget-config.icon
+
+ + +
+
+
{{ 'widget-config.header-button.colors' | translate }}
+
+
widget-config.header-button.color
+ + + +
widget-config.header-button.background
+ + + +
widget-config.header-button.border
+ + +
+
+
+ + + + widget-config.header-button.advanced-button-style + + + + + + +
+
+ } @else { +
+
{{'widget-config.icon' | translate}}
+ + +
+ } +
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts index ab2b8870d0..b77b5c860f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, Inject, OnDestroy, OnInit, SkipSelf, ViewChild } from '@angular/core'; +import { Component, DestroyRef, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core'; import { ErrorStateMatcher } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; @@ -28,7 +28,6 @@ import { ValidatorFn, Validators } from '@angular/forms'; -import { Subject } from 'rxjs'; import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; import { @@ -44,14 +43,17 @@ import { defaultWidgetAction, WidgetActionSource, WidgetActionType, + WidgetHeaderActionButtonType, + WidgetHeaderActionButtonTypes, + widgetHeaderActionButtonTypeTranslationMap, widgetType } from '@shared/models/widget.models'; -import { takeUntil } from 'rxjs/operators'; import { CustomActionEditorCompleter } from '@home/components/widget/lib/settings/common/action/custom-action.models'; import { WidgetService } from '@core/http/widget.service'; import { isDefinedAndNotNull, isNotEmptyStr } from '@core/utils'; import { MatSelect } from '@angular/material/select'; import { TranslateService } from '@ngx-translate/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export interface WidgetActionDialogData { isAdd: boolean; @@ -69,9 +71,7 @@ export interface WidgetActionDialogData { styleUrls: [] }) export class WidgetActionDialogComponent extends DialogComponent implements OnInit, OnDestroy, ErrorStateMatcher { - - private destroy$ = new Subject(); + WidgetActionDescriptorInfo> implements OnInit, ErrorStateMatcher { widgetActionFormGroup: FormGroup; @@ -87,6 +87,10 @@ export class WidgetActionDialogComponent extends DialogComponent = []; usedCellClickColumns: Array = []; + widgetHeaderActionButtonType = WidgetHeaderActionButtonType + widgetHeaderActionButtonTypes = WidgetHeaderActionButtonTypes; + widgetHeaderActionButtonTypeTranslationMap = widgetHeaderActionButtonTypeTranslationMap; + @ViewChild('columnIndexSelect') columnIndexSelect: MatSelect; columnIndexPlaceholderText = this.translate.instant('widget-config.select-column-index'); @@ -98,7 +102,8 @@ export class WidgetActionDialogComponent extends DialogComponent, public fb: FormBuilder, - private translate: TranslateService) { + private translate: TranslateService, + private destroyRef: DestroyRef) { super(store, router, dialogRef); this.isAdd = data.isAdd; if (this.isAdd) { @@ -122,14 +127,25 @@ export class WidgetActionDialogComponent extends DialogComponent { this.widgetActionFormGroup.get('name').updateValueAndValidity(); this.updateShowWidgetActionForm(); @@ -139,12 +155,26 @@ export class WidgetActionDialogComponent extends DialogComponent { this.updateShowWidgetActionForm(); }); + this.widgetActionFormGroup.get('buttonType').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => this.widgetHeaderButtonValidators()); setTimeout(() => { if (this.action?.actionSourceId === 'cellClick') { this.widgetActionFormGroup.get('columnIndex').enable(); @@ -156,10 +186,31 @@ export class WidgetActionDialogComponent extends DialogComponent { this.latestChartOption = { tooltip: { trigger: this.settings.showTooltip ? 'item' : 'none', - confine: false, - appendTo: 'body', + confine: true, formatter: (params: CallbackDataParams) => this.settings.showTooltip ? latestChartTooltipFormatter(this.renderer, this.settings, params, this.units, this.total, this.dataItems) 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 9086bb1896..ad121a1eb0 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 @@ -161,6 +161,8 @@ export class TbTimeSeriesChart { private latestData: FormattedData[] = []; + private onParentScroll = this._onParentScroll.bind(this); + yMin$ = this.yMinSubject.asObservable(); yMax$ = this.yMaxSubject.asObservable(); @@ -358,6 +360,7 @@ export class TbTimeSeriesChart { this.yMinSubject.complete(); this.yMaxSubject.complete(); this.darkModeObserver?.disconnect(); + this.ctx.dashboard.gridster.el.removeEventListener('scroll', this.onParentScroll); } public resize(): void { @@ -611,6 +614,7 @@ export class TbTimeSeriesChart { this.timeSeriesChart = echarts.init(this.chartElement, null, { renderer: 'svg' }); + this.ctx.dashboard.gridster.el.addEventListener('scroll', this.onParentScroll); this.timeSeriesChartOptions = { darkMode: this.darkMode, backgroundColor: 'transparent', @@ -837,6 +841,14 @@ export class TbTimeSeriesChart { return this.settings.dataZoom ? 45 : 5; } + private _onParentScroll() { + if (this.timeSeriesChart) { + this.timeSeriesChart.dispatchAction({ + type: 'hideTip' + }); + } + } + private onResize() { const shapeWidth = this.chartElement.offsetWidth; const shapeHeight = this.chartElement.offsetHeight; 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 3ae61642ce..63d77c48b2 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 @@ -15,15 +15,24 @@ /// import { - DataLayerColorSettings, DataLayerColorType, - DataLayerPatternSettings, DataLayerPatternType, - MapDataLayerSettings, MapDataLayerType, mapDataSourceSettingsToDatasource, - MapStringFunction, MapType, + DataLayerColorSettings, + DataLayerColorType, + DataLayerPatternSettings, + DataLayerPatternType, + MapDataLayerSettings, + MapDataLayerType, + mapDataSourceSettingsToDatasource, + MapStringFunction, + MapType, TbMapDatasource } from '@shared/models/widget/maps/map.models'; import { createLabelFromPattern, - guid, isDefined, + guid, + isDefined, + isDefinedAndNotNull, + isNumber, + isNumeric, mergeDeepIgnoreArray, parseTbFunction, safeExecuteTbFunction @@ -32,10 +41,11 @@ import L from 'leaflet'; import { CompiledTbFunction } from '@shared/models/js-function.models'; import { forkJoin, Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; -import { FormattedData } from '@shared/models/widget.models'; +import { DataKey, 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'; +import { ColorRange } from '@shared/models/widget-settings.models'; export class DataLayerPatternProcessor { @@ -77,22 +87,26 @@ export class DataLayerColorProcessor { private colorFunction: CompiledTbFunction; private color: string; + private rangeKey: DataKey; + private range: ColorRange[]; constructor(private dataLayer: TbMapDataLayer, private settings: DataLayerColorSettings) {} public setup(): Observable { this.color = this.settings.color; - if (this.settings.type === DataLayerColorType.function) { + if (this.settings.type === DataLayerColorType.range) { + this.rangeKey = this.settings.rangeKey; + this.range = this.settings.range; + } else if (this.settings.type === DataLayerColorType.function) { return parseTbFunction(this.dataLayer.getCtx().http, this.settings.colorFunction, ['data', 'dsData']).pipe( map((parsed) => { this.colorFunction = parsed; return null; }) ); - } else { - return of(null) } + return of(null) } public processColor(data: FormattedData, dsData: FormattedData[]): string { @@ -102,12 +116,33 @@ export class DataLayerColorProcessor { if (!color) { color = this.color; } + } else if (this.settings.type === DataLayerColorType.range) { + color = this.color; + if (this.rangeKey && this.range?.length) { + const value = data[this.rangeKey.label]; + if (isDefinedAndNotNull(value) && isNumeric(value)) { + const num = Number(value); + for (const range of this.range) { + if (DataLayerColorProcessor.constantRange(range) && range.from === num) { + color = range.color; + break; + } else if ((!isNumber(range.from) || num >= range.from) && (!isNumber(range.to) || num < range.to)) { + color = range.color; + break; + } + } + } + } } else { color = this.color; } return color; } + static constantRange(range: ColorRange): boolean { + return isNumber(range.from) && isNumber(range.to) && range.from === range.to; + } + } export abstract class TbDataLayerItem { @@ -137,7 +172,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); return forkJoin( @@ -243,6 +281,10 @@ export abstract class TbMapDataLayer): boolean { + return data.$datasource.mapDataIds.includes(this.mapDataId); + } + protected createDataLayerContainer(): L.FeatureGroup { return L.featureGroup([], {snapIgnore: true}); } @@ -251,6 +293,10 @@ export abstract class TbMapDataLayer { @@ -218,8 +221,7 @@ abstract class MarkerIconProcessor { abstract class BaseColorMarkerShapeProcessor extends MarkerIconProcessor { - private markerColorFunction: CompiledTbFunction; - + private colorProcessor: DataLayerColorProcessor; private defaultMarkerIconInfo: MarkerIconInfo; protected constructor(protected dataProcessor: MarkerDataProcessor, @@ -229,40 +231,28 @@ abstract class BaseColorMarkerShapeProcessor public setup(): Observable { const colorSettings = this.settings.color; - if (colorSettings.type === DataLayerColorType.function) { - return parseTbFunction(this.dataProcessor.dataLayer.getCtx().http, colorSettings.colorFunction, ['data', 'dsData']).pipe( - map((parsed) => { - this.markerColorFunction = parsed; - return null; - }) - ); - } else { + this.colorProcessor = new DataLayerColorProcessor(this.dataProcessor.dataLayer, colorSettings); + const setup$: Observable[] = [this.colorProcessor.setup()]; + if (colorSettings.type === DataLayerColorType.constant) { const color = tinycolor(colorSettings.color); - return this.createMarkerShape(color, 0, this.settings.size).pipe( - map((info) => { - this.defaultMarkerIconInfo = info; - return null; - } - )); + setup$.push( + this.createMarkerShape(color, 0, this.settings.size).pipe( + map((info) => { + this.defaultMarkerIconInfo = info; + return null; + })) + ); } + return forkJoin(setup$).pipe(map(() => null)); } public createMarkerIcon(data: FormattedData, dsData: FormattedData[], rotationAngle = 0): Observable { const colorSettings = this.settings.color; - let color: tinycolor.Instance; - if (colorSettings.type === DataLayerColorType.function) { - const functionColor = safeExecuteTbFunction(this.markerColorFunction, [data, dsData]); - if (isDefinedAndNotNull(functionColor)) { - color = tinycolor(functionColor); - } else { - color = tinycolor(colorSettings.color); - } - return this.createMarkerShape(color, rotationAngle, this.settings.size); - } else if (rotationAngle === 0) { + if (colorSettings.type === DataLayerColorType.constant && rotationAngle === 0) { return of(this.defaultMarkerIconInfo); } else { - color = tinycolor(colorSettings.color); - return this.createMarkerShape(color, rotationAngle, this.settings.size); + const color = this.colorProcessor.processColor(data, dsData); + return this.createMarkerShape(tinycolor(color), rotationAngle, this.settings.size); } } @@ -639,6 +629,15 @@ export class TbMarkersDataLayer extends TbLatestMapDataLayer): Partial { return defaultBaseMarkersDataLayerSettings(map.type()); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts index ec6c5aba80..836b0e7947 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ShapeDataLayerSettings, TbMapDatasource } from '@shared/models/widget/maps/map.models'; +import { DataLayerColorSettings, ShapeDataLayerSettings, TbMapDatasource } from '@shared/models/widget/maps/map.models'; import L from 'leaflet'; import { TbMap } from '@home/components/widget/lib/maps/map'; import { forkJoin, Observable } from 'rxjs'; @@ -45,6 +45,10 @@ export abstract class TbShapesDataLayer { this.fillColorProcessor = new DataLayerColorProcessor(this, this.settings.fillColor); this.strokeColorProcessor = new DataLayerColorProcessor(this, this.settings.strokeColor); 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 d6cbe5e4c9..e598685786 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 @@ -16,16 +16,16 @@ import { calculateInterpolationRatio, - calculateLastPoints, + calculateLastPoints, DataLayerColorSettings, DataLayerColorType, defaultBaseTripsDataLayerSettings, findRotationAngle, interpolateLineSegment, - MapDataLayerType, + MapDataLayerType, MarkerType, TbMapDatasource, TripsDataLayerSettings } from '@shared/models/widget/maps/map.models'; import { forkJoin, Observable } from 'rxjs'; -import { FormattedData, WidgetActionType } from '@shared/models/widget.models'; +import { DataKey, FormattedData, WidgetActionType } from '@shared/models/widget.models'; import { map } from 'rxjs/operators'; import L from 'leaflet'; import { deepClone, isDefined, isUndefined } from '@core/utils'; @@ -530,9 +530,14 @@ export class TbTripsDataLayer extends TbMapDataLayer settings.type === DataLayerColorType.range && settings.rangeKey) + .map(settings => settings.rangeKey); if (this.settings.additionalDataKeys?.length) { - const tsKeys = this.settings.additionalDataKeys.filter(key => key.type === DataKeyType.timeseries); - const latestKeys = this.settings.additionalDataKeys.filter(key => key.type !== DataKeyType.timeseries); + additionalKeys.push(...this.settings.additionalDataKeys); + } + if (additionalKeys.length) { + const tsKeys = additionalKeys.filter(key => key.type === DataKeyType.timeseries); + const latestKeys = additionalKeys.filter(key => key.type !== DataKeyType.timeseries); datasource.dataKeys.push(...tsKeys); if (latestKeys.length) { datasource.latestDataKeys = latestKeys; @@ -541,6 +546,24 @@ export class TbTripsDataLayer extends TbMapDataLayer): Partial { return defaultBaseTripsDataLayerSettings(map.type()); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts index 06d49d0343..79e2f11ce6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts @@ -17,6 +17,7 @@ import L, { TB } from 'leaflet'; import { guid, isNotEmptyStr } from '@core/utils'; import 'leaflet-providers'; +import { Map as MapLibreGLMap, LngLat as MapLibreGLLngLat } from 'maplibre-gl'; import '@geoman-io/leaflet-geoman-free'; import 'leaflet.markercluster'; import { MatIconRegistry } from '@angular/material/icon'; @@ -26,7 +27,7 @@ import { of } from 'rxjs'; L.MarkerCluster = L.MarkerCluster.mergeOptions({ pmIgnore: true }); -class SidebarControl extends L.Control { +class SidebarControl extends L.Control implements L.TB.SidebarControl { private readonly sidebar: JQuery; @@ -94,7 +95,7 @@ class SidebarControl extends L.Control { } } -class SidebarPaneControl extends L.Control { +class SidebarPaneControl extends L.Control implements L.TB.SidebarPaneControl { private button: JQuery; private $ui: JQuery; @@ -154,7 +155,7 @@ class SidebarPaneControl extends L.Contr } } -class LayersControl extends SidebarPaneControl { +class LayersControl extends SidebarPaneControl implements L.TB.LayersControl { constructor(options: TB.LayersControlOptions) { super(options); } @@ -211,15 +212,16 @@ class LayersControl extends SidebarPaneControl { input.on('click', (e: JQuery.MouseEventBase) => { e.stopPropagation(); - layers.forEach((other) => { - if (other.layer === layerData.layer) { - map.addLayer(other.layer); - map.attributionControl.setPrefix(other.attributionPrefix); - } else { - map.removeLayer(other.layer); - } - }); - map.fire('baselayerchange', { layer: layerData.layer }); + if (!map.hasLayer(layerData.layer)) { + map.addLayer(layerData.layer); + map.attributionControl.setPrefix(layerData.attributionPrefix); + layers.forEach((other) => { + if (other.layer !== layerData.layer) { + map.removeLayer(other.layer); + } + }); + map.fire('baselayerchange', { layer: layerData.layer }); + } }); item.on('dblclick', (e) => { @@ -233,12 +235,12 @@ class LayersControl extends SidebarPaneControl { } } -class GroupsControl extends SidebarPaneControl { +class GroupsControl extends SidebarPaneControl implements L.TB.GroupsControl { constructor(options: TB.GroupsControlOptions) { super(options); } - public onAddPane(map: L.Map, button: JQuery, $ui: JQuery, toggle: (e: JQuery.MouseEventBase) => void) { + public onAddPane(map: L.Map, _button: JQuery, $ui: JQuery, _toggle: (e: JQuery.MouseEventBase) => void) { const paneId = guid(); const groups = this.options.groups; const baseSection = $("
") @@ -280,7 +282,7 @@ class GroupsControl extends SidebarPaneControl { } } -class TopToolbarButton { +class TopToolbarButton implements L.TB.TopToolbarButton { private readonly button: JQuery; private active = false; private disabled = false; @@ -388,7 +390,7 @@ class TopToolbarButton { } } -class ToolbarButton { +class ToolbarButton implements L.TB.ToolbarButton { private readonly id: string; private readonly button: JQuery; private active = false; @@ -456,7 +458,7 @@ class ToolbarButton { } } -class TopToolbarControl { +class TopToolbarControl implements L.TB.TopToolbarControl { private readonly toolbarElement: JQuery; private buttons: Array = []; @@ -485,7 +487,7 @@ class TopToolbarControl { } } -class ToolbarControl extends L.Control { +class ToolbarControl extends L.Control implements L.TB.ToolbarControl { private buttonContainer: JQuery; @@ -511,7 +513,7 @@ class ToolbarControl extends L.Control { } -class BottomToolbarControl { +class BottomToolbarControl implements L.TB.BottomToolbarControl { private readonly buttonContainer: JQuery; private toolbarButtons: ToolbarButton[] = []; @@ -570,35 +572,35 @@ class BottomToolbarControl { } -const sidebar = (options: TB.SidebarControlOptions): SidebarControl => { +const sidebar = (options: TB.SidebarControlOptions): L.TB.SidebarControl => { return new SidebarControl(options); } -const sidebarPane = (options: O): SidebarPaneControl => { +const sidebarPane = (options: O): L.TB.SidebarPaneControl => { return new SidebarPaneControl(options); } -const layers = (options: TB.LayersControlOptions): LayersControl => { +const layers = (options: TB.LayersControlOptions): L.TB.LayersControl => { return new LayersControl(options); } -const groups = (options: TB.GroupsControlOptions): GroupsControl => { +const groups = (options: TB.GroupsControlOptions): L.TB.GroupsControl => { return new GroupsControl(options); } -const topToolbar = (options: TB.TopToolbarControlOptions): TopToolbarControl => { +const topToolbar = (options: TB.TopToolbarControlOptions): L.TB.TopToolbarControl => { return new TopToolbarControl(options); } -const toolbar = (options: L.ControlOptions): ToolbarControl => { +const toolbar = (options: L.ControlOptions): L.TB.ToolbarControl => { return new ToolbarControl(options); } -const bottomToolbar = (options: TB.BottomToolbarControlOptions): BottomToolbarControl => { +const bottomToolbar = (options: TB.BottomToolbarControlOptions): L.TB.BottomToolbarControl => { return new BottomToolbarControl(options); } -class ChinaProvider extends L.TileLayer { +class ChinaProvider extends L.TileLayer implements L.TB.TileLayer.ChinaProvider { static chinaProviders: L.TB.TileLayer.ChinaProvidersData = { Tencent: { @@ -644,10 +646,302 @@ class ChinaProvider extends L.TileLayer { } } -const chinaProvider = (type: string, options?: L.TileLayerOptions): ChinaProvider => { +const chinaProvider = (type: string, options?: L.TileLayerOptions): L.TB.TileLayer.ChinaProvider => { return new ChinaProvider(type, options); } +class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer { + + options: TB.MapLibreGL.LeafletMapLibreGLMapOptions; + + private readonly _throttledUpdate: () => void; + private _container: HTMLDivElement; + private _glMap: MapLibreGLMap; + private _actualCanvas: HTMLCanvasElement; + private _offset: L.Point; + private _zooming: boolean; + + constructor(options: TB.MapLibreGL.LeafletMapLibreGLMapOptions) { + super(); + options = {...options, ...{ + updateInterval: 32, + padding: 0.1, + interactive: false, + pane: 'tilePane' + }}; + options.attribution = this._loadAttribution(options); + this._prepareTransformRequest(options); + L.setOptions(this, options); + this._throttledUpdate = L.Util.throttle(this._update, this.options.updateInterval, this); + } + + onAdd(map: L.Map): this { + let update = false; + if (!this._container) { + this._initContainer(); + } else { + update = true; + } + const paneName = this.getPaneName(); + map.getPane(paneName).appendChild(this._container); + this._initGL(); + + this._offset = this._map.containerPointToLayerPoint([0, 0]); + if ((this._map as any)._proxy && map.options.zoomAnimation) { + L.DomEvent.on((map as any)._proxy, L.DomUtil.TRANSITION_END, this._transitionEnd, this); + } + if (update) { + this._update(); + } + return this; + } + + onRemove(map: L.Map): this { + if ((this._map as any)._proxy && this._map.options.zoomAnimation) { + L.DomEvent.off((map as any)._proxy, L.DomUtil.TRANSITION_END, this._transitionEnd, this); + } + const paneName = this.getPaneName(); + map.getPane(paneName).removeChild(this._container); + + this._glMap.remove(); + this._glMap = null; + + return this; + } + + getEvents(): { [p: string]: L.LeafletEventHandlerFn } { + return { + move: this._throttledUpdate, // sensibly throttle updating while panning + zoomanim: this._animateZoom, // applys the zoom animation to the + zoom: this._pinchZoom, // animate every zoom event for smoother pinch-zooming + zoomstart: this._zoomStart, // flag starting a zoom to disable panning + zoomend: this._zoomEnd, + resize: this._resize + }; + } + + getMapLibreGLMap(): MapLibreGLMap { + return this._glMap; + } + + getCanvas(): HTMLCanvasElement { + return this._glMap.getCanvas(); + } + + getSize(): L.Point { + return this._map.getSize().multiplyBy(1 + this.options.padding * 2); + } + + getBounds(): L.LatLngBounds { + const halfSize = this.getSize().multiplyBy(0.5); + const center = this._map.latLngToContainerPoint(this._map.getCenter()); + return L.latLngBounds( + this._map.containerPointToLatLng(center.subtract(halfSize)), + this._map.containerPointToLatLng(center.add(halfSize)) + ); + } + + getContainer(): HTMLDivElement { + return this._container; + } + + getPaneName(): string { + return this._map.getPane(this.options.pane) ? this.options.pane : 'tilePane'; + } + + private _roundPoint(p: L.Point): L.Point { + return new L.Point(Math.round(p.x), Math.round(p.y)); + } + + private _initContainer() { + const container = this._container = L.DomUtil.create('div', 'leaflet-gl-layer'); + const size = this.getSize(); + const offset = this._map.getSize().multiplyBy(this.options.padding); + container.style.width = size.x + 'px'; + container.style.height = size.y + 'px'; + const topLeft = this._map.containerPointToLayerPoint([0, 0]).subtract(offset); + L.DomUtil.setPosition(container, this._roundPoint(topLeft)); + } + + private _initGL() { + const center = this._map.getCenter(); + const options = L.extend({}, this.options, { + container: this._container, + center: [center.lng, center.lat], + zoom: this._map.getZoom() - 1, + attributionControl: false + }); + this._glMap = new MapLibreGLMap(options); + this._glMap.once('load', () => { + this.fire('load'); + }); + this._glMap.setMaxBounds(null); + this._transformGL(this._glMap); + this._actualCanvas = this._glMap._canvas; + const canvas = this._actualCanvas; + L.DomUtil.addClass(canvas, 'leaflet-image-layer'); + L.DomUtil.addClass(canvas, 'leaflet-zoom-animated'); + if (this.options.interactive) { + L.DomUtil.addClass(canvas, 'leaflet-interactive'); + } + if (this.options.className) { + L.DomUtil.addClass(canvas, this.options.className); + } + } + + private _update() { + if (!this._map) { + return; + } + this._offset = this._map.containerPointToLayerPoint([0, 0]); + + if (this._zooming) { + return; + } + const size = this.getSize(), + container = this._container, + gl = this._glMap, + offset = this._map.getSize().multiplyBy(this.options.padding), + topLeft = this._map.containerPointToLayerPoint([0, 0]).subtract(offset); + + L.DomUtil.setPosition(container, this._roundPoint(topLeft)); + + this._transformGL(gl); + + if (gl.transform.width !== size.x || gl.transform.height !== size.y) { + container.style.width = size.x + 'px'; + container.style.height = size.y + 'px'; + gl.resize(); + } else { + gl._update(); + } + } + + private _transformGL(gl: MapLibreGLMap) { + const center = this._map.getCenter(); + const tr = gl._getTransformForUpdate(); + tr.setCenter(MapLibreGLLngLat.convert([center.lng, center.lat])); + tr.setZoom(this._map.getZoom() - 1); + gl.transform.apply(tr); + gl._fireMoveEvents(); + } + + private _pinchZoom() { + this._glMap.jumpTo({ + zoom: this._map.getZoom() - 1, + center: this._map.getCenter() + }); + } + + private _animateZoom(e: L.ZoomAnimEvent) { + const scale = this._map.getZoomScale(e.zoom); + const padding = this._map.getSize().multiplyBy(this.options.padding * scale); + const viewHalf = this.getSize().divideBy(2); + + const topLeft = this._map.project(e.center, e.zoom) + .subtract(viewHalf) + .add((this._map as any)._getMapPanePos() + .add(padding)).round(); + + const offset = this._map.project(this._map.getBounds().getNorthWest(), e.zoom) + .subtract(topLeft); + + L.DomUtil.setTransform( + this._actualCanvas, + offset.subtract(this._offset), + scale + ); + } + + private _zoomStart() { + this._zooming = true; + } + + private _zoomEnd() { + const scale = this._map.getZoomScale(this._map.getZoom()); + L.DomUtil.setTransform( + this._actualCanvas, + null, + scale + ); + this._zooming = false; + this._update(); + } + + private _transitionEnd() { + L.Util.requestAnimFrame(() => { + const zoom = this._map.getZoom(); + const center = this._map.getCenter(); + const offset = this._map.latLngToContainerPoint( + this._map.getBounds().getNorthWest() + ); + + L.DomUtil.setTransform(this._actualCanvas, offset, 1); + + this._glMap.once('moveend', () => { + this._zoomEnd(); + }); + this._glMap.jumpTo({ + center: center, + zoom: zoom - 1 + }); + }); + } + + private _resize() { + this._transitionEnd(); + } + + private _loadAttribution(options: TB.MapLibreGL.LeafletMapLibreGLMapOptions): string { + if (options.attributionControl !== false && typeof options.attributionControl?.customAttribution === 'string') { + return options.attributionControl.customAttribution; + } + if (options.attributionControl !== false) { + const style = options.style; + if (typeof style !== 'string' && style?.sources) { + return Object.keys(style.sources) + .map((sourceId) => { + const source = style.sources[sourceId]; + return (source && source.type !== 'video' && source.type !== 'image' + && typeof source.attribution === 'string') ? source.attribution.trim() : null; + }) + .filter(Boolean) // Remove null/undefined values + .join(', '); + } + } + return ''; + } + + private _prepareTransformRequest(options: TB.MapLibreGL.LeafletMapLibreGLMapOptions) { + if (!options.transformRequest) { + const style = options.style; + if (typeof style !== 'string' && style.glyphs) { + const glyphs = style.glyphs; + const glyphsRegexString = glyphs.replace(/\//g, '\\/').replace(/\./g, '\\.').replace('{fontstack}', '(.*)').replace('{range}', '(.*)'); + const glyphsRegex = new RegExp(glyphsRegexString); + options.transformRequest = (url, resourceType) => { + if (resourceType === 'Glyphs' && glyphsRegex && glyphsRegex.test(url)) { + const res = glyphsRegex.exec(url); + if (res.length === 3) { + const fontStack = res[1]; + const fonts = fontStack.split(','); + if (fonts.length > 1) { + const newFontStack = fonts[0]; + url = url.replace(fontStack, newFontStack); + } + } + } + return {url}; + }; + } + } + } +} + +const mapLibreGLLayer = (options: TB.MapLibreGL.LeafletMapLibreGLMapOptions): TB.MapLibreGL.MapLibreGLLayer => { + return new MapLibreGLLayer(options); +} + L.TB = L.TB || { SidebarControl, SidebarPaneControl, @@ -670,5 +964,9 @@ L.TB = L.TB || { }, tileLayer: { chinaProvider + }, + MapLibreGL: { + MapLibreGLLayer, + mapLibreGLLayer } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts index 304100828b..b3a41ec2f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts @@ -27,16 +27,33 @@ import { MapLayerSettings, MapProvider, OpenStreetMapLayerSettings, + ReferenceLayerType, TencentMapLayerSettings } from '@shared/models/widget/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { DeepPartial } from '@shared/models/common'; import { mergeDeep } from '@core/utils'; -import { Observable, of, switchMap } from 'rxjs'; +import { Observable, of, shareReplay, switchMap } from 'rxjs'; import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import L from 'leaflet'; import { catchError, map } from 'rxjs/operators'; import { ResourcesService } from '@core/services/resources.service'; +import { StyleSpecification } from '@maplibre/maplibre-gl-style-spec'; + +const referenceLayerStyleUrlMap = new Map( + [ + [ReferenceLayerType.openstreetmap_hybrid, '/assets/map/openstreetmap_hybrid_reference_style.json'], + [ReferenceLayerType.world_edition_hybrid, '/assets/map/world_edition_hybrid_reference_style.json'], + [ReferenceLayerType.enhanced_contrast_hybrid, '/assets/map/enhanced_contrast_hybrid_reference_style.json'] + ] +); + +const referenceLayerCache = new Map>(); + +interface TbMapLayerData { + layer: L.Layer; + attribution: boolean; +} export abstract class TbMapLayer { @@ -65,19 +82,18 @@ export abstract class TbMapLayer { } public loadLayer(theMap: L.Map): Observable { - return this.createLayer().pipe( - switchMap((layer) => { - if (layer) { - return this.createLayer().pipe( - map((mini) => { - if (mini) { - const attribution = layer.getAttribution(); - const attributionPrefix = attribution ? theMap.attributionControl.options.prefix as string : null; + return this.generateLayer().pipe( + switchMap((layerData) => { + if (layerData) { + return this.generateLayer().pipe( + map((miniLayerData) => { + if (miniLayerData) { + const attributionPrefix = layerData.attribution ? theMap.attributionControl.options.prefix as string : null; return { title: this.title(), attributionPrefix: attributionPrefix, - layer, - mini + layer: layerData.layer, + mini: miniLayerData.layer }; } else { return null; @@ -91,6 +107,76 @@ export abstract class TbMapLayer { ); } + private generateLayer(): Observable { + return this.createLayer().pipe( + switchMap((baseLayer) => { + if (baseLayer) { + if (this.settings.referenceLayer) { + return this.loadReferenceLayer(this.settings.referenceLayer).pipe( + map((referenceLayer) => { + if (referenceLayer) { + const layer = L.featureGroup(); + let baseLayerLoaded = false; + let referenceLayerLoaded = false; + baseLayer.addTo(layer); + referenceLayer.addTo(layer); + baseLayer.once('load', () => { + baseLayerLoaded = true; + if (referenceLayerLoaded) { + layer.fire('load'); + } + }); + referenceLayer.once('load', () => { + referenceLayerLoaded = true; + if (baseLayerLoaded) { + layer.fire('load'); + } + }); + return { + layer, + attribution: !!baseLayer.getAttribution() || !!referenceLayer.getAttribution() + }; + } else { + return { + layer: baseLayer, + attribution: !!baseLayer.getAttribution() + }; + } + })); + } else { + return of({ + layer: baseLayer, + attribution: !!baseLayer.getAttribution() + }); + } + } else { + return of(null); + } + } + )); + } + + private loadReferenceLayer(referenceLayer: ReferenceLayerType): Observable { + let spec$ = referenceLayerCache.get(referenceLayer); + if (!spec$) { + const styleUrl = referenceLayerStyleUrlMap.get(referenceLayer); + spec$ = this.ctx.http.get(styleUrl).pipe( + shareReplay({ + bufferSize: 1, + refCount: true + }) + ); + referenceLayerCache.set(referenceLayer, spec$); + } + return spec$.pipe( + map(spec => { + return L.TB.MapLibreGL.mapLibreGLLayer({ + style: spec, + }); + }) + ); + } + private title(): string { const customTranslate = this.ctx.$injector.get(CustomTranslatePipe); if (this.settings.label) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index 592610f8b2..985bdc17f5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -35,6 +35,10 @@ div.tb-widget .tb-widget-content.tb-no-interaction { .tb-map-container { flex-direction: column; + .leaflet-gl-layer.maplibregl-map { + position: relative; + z-index: 1; + } } .tb-map-layout { @@ -47,6 +51,10 @@ div.tb-widget .tb-widget-content.tb-no-interaction { .tb-map { position: relative; flex: 1; + .leaflet-control-attribution { + font-size: 0.6rem; + background: rgba(255,255,255,0.5); + } &.leaflet-touch { .leaflet-bar { border: 1px solid rgba(0,0,0,0.38); 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 054eb5943e..6672941045 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 @@ -41,7 +41,7 @@ import { } from '@core/utils'; import { DeepPartial } from '@shared/models/common'; import L from 'leaflet'; -import { forkJoin, Observable, of } from 'rxjs'; +import { EMPTY, forkJoin, Observable, of } from 'rxjs'; import { map, switchMap, tap } from 'rxjs/operators'; import '@home/components/widget/lib/maps/leaflet/leaflet-tb'; import { @@ -142,6 +142,7 @@ export abstract class TbMap { protected inputSettings: DeepPartial, protected containerElement: HTMLElement) { this.ctx.actionsApi.placeMapItem = this.placeMapItem.bind(this); + (this.ctx as any).mapInstance = this; this.settings = mergeDeepIgnoreArray({} as S, this.defaultSettings(), this.inputSettings as S); $(containerElement).empty(); @@ -1101,6 +1102,47 @@ export abstract class TbMap { return this.dragMode; } + public saveMarkerLocation(data: FormattedData, lat?: number, lng?: number): Observable { + const targetDataLayer = this.latestDataLayers.find(dl => dl.dataLayerType() === 'markers' && dl.hasData(data)); + if (targetDataLayer) { + let location: L.LatLng = null; + if (isDefinedAndNotNull(lat) && isDefinedAndNotNull(lng)) { + location = new L.LatLng(lat, lng); + } + return (targetDataLayer as TbMarkersDataLayer).saveMarkerLocation(data, location); + } else { + return EMPTY; + } + } + + public savePolygonLocation(data: FormattedData, coordinates?: TbPolygonCoordinates): Observable { + const targetDataLayer = this.latestDataLayers.find(dl => dl.dataLayerType() === 'polygons' && dl.hasData(data)); + if (targetDataLayer) { + return (targetDataLayer as TbPolygonsDataLayer).savePolygonCoordinates(data, coordinates); + } else { + return EMPTY; + } + } + + public saveLocation(data: FormattedData, values: {[key: string]: any}): Observable { + const datasource = data.$datasource; + let dataKeys = datasource.dataKeys; + if (datasource.latestDataKeys) { + dataKeys = dataKeys.concat(datasource.latestDataKeys); + } + const itemData: DataKeyValuePair[] = []; + for (const dataKeyName of Object.keys(values)) { + const dataKey = dataKeys.find(key => key.name === dataKeyName); + if (dataKey) { + itemData.push({ + dataKey, + value: values[dataKeyName] + }); + } + } + return this.saveItemData(datasource, itemData, AttributeScope.SERVER_SCOPE); + } + public saveItemData(datasource: TbMapDatasource, data: DataKeyValuePair[], attributeScope: AttributeScope): Observable { const attributeService = this.ctx.$injector.get(AttributeService); const attributes: AttributeData[] = []; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html index 3ef8d6c023..ce1a1438aa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html @@ -19,4 +19,4 @@ [additionalStyles]="additionalStyles" [containerClass]="markdownClass" [applyDefaultMarkdownStyle]="applyDefaultMarkdownStyle" - [context]="{ ctx: ctx }" lineNumbers fallbackToPlainMarkdown (click)="markdownClick($event)"> + [context]="{ ctx: ctx, data: data }" lineNumbers fallbackToPlainMarkdown (click)="markdownClick($event)"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts index 1e80baddfa..61f5073953 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts @@ -63,6 +63,8 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { @Input() ctx: WidgetContext; + data: FormattedData[]; + markdownText: string; additionalStyles: string[]; @@ -128,15 +130,15 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { } else { initialData = []; } - const data = formattedDataFormDatasourceData(initialData); + this.data = formattedDataFormDatasourceData(initialData); - let markdownText = this.settings.useMarkdownTextFunction ? - this.markdownTextFunction.pipe(map(markdownTextFunction => safeExecuteTbFunction(markdownTextFunction, [data, this.ctx]))) : this.settings.markdownTextPattern; + const markdownText = this.settings.useMarkdownTextFunction ? + this.markdownTextFunction.pipe(map(markdownTextFunction => safeExecuteTbFunction(markdownTextFunction, [this.data, this.ctx]))) : this.settings.markdownTextPattern; if (typeof markdownText === 'string') { - this.updateMarkdownText(markdownText, data); + this.updateMarkdownText(markdownText, this.data); } else { markdownText.subscribe((text) => { - this.updateMarkdownText(text, data); + this.updateMarkdownText(text, this.data); }); } } @@ -146,8 +148,8 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { markdownText = createLabelFromPattern(markdownText, allData); if (this.markdownText !== markdownText) { this.markdownText = this.utils.customTranslation(markdownText, markdownText); - this.cd.detectChanges(); } + this.cd.markForCheck(); } markdownClick($event: MouseEvent) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.ts index 1d1df6a8d5..9f85e9dd85 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.ts @@ -171,6 +171,8 @@ export class GetValueActionSettingsPanelComponent extends PageComponent implemen const action: GetValueAction = this.getValueSettingsFormGroup.get('action').value; if (action === GetValueAction.GET_DASHBOARD_STATE_OBJECT) { return 'widget/config/parse_value_get_dashboard_state_object_fn'; + } else if (action === GetValueAction.GET_DASHBOARD_STATE) { + return 'widget/config/parse_value_get_dashboard_state_id_fn'; } return 'widget/lib/rpc/parse_value_fn'; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html index 7dbab5d9dc..c3194676cf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html @@ -36,6 +36,17 @@
+ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts index c5ae5c3192..52064fdf91 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts @@ -27,7 +27,7 @@ import { WidgetActionType, WidgetMobileActionDescriptor, WidgetMobileActionType, - widgetMobileActionTypeTranslationMap + widgetMobileActionTypeTranslationMap, } from '@shared/models/widget.models'; import { CustomActionEditorCompleter } from '@home/components/widget/lib/settings/common/action/custom-action.models'; import { @@ -38,7 +38,8 @@ import { getDefaultProcessImageFunction, getDefaultProcessLaunchResultFunction, getDefaultProcessLocationFunction, - getDefaultProcessQrCodeFunction + getDefaultProcessQrCodeFunction, + getDefaultProvisionSuccessFunction } from '@home/components/widget/lib/settings/common/action/mobile-action-editor.models'; import { WidgetService } from '@core/http/widget.service'; import { TbFunction } from '@shared/models/js-function.models'; @@ -254,6 +255,18 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit this.fb.control(processLocationFunction, [Validators.required]) ); break; + case WidgetMobileActionType.deviceProvision: + let handleProvisionSuccessFunction = action?.handleProvisionSuccessFunction; + if (changed) { + const defaultProvisionSuccessFunction = getDefaultProvisionSuccessFunction(); + if (defaultProvisionSuccessFunction !== handleProvisionSuccessFunction) { + handleProvisionSuccessFunction = defaultProvisionSuccessFunction; + } + } + this.mobileActionTypeFormGroup.addControl( + 'handleProvisionSuccessFunction', + this.fb.control(handleProvisionSuccessFunction, [Validators.required]) + ); } } this.mobileActionTypeFormGroup.valueChanges.pipe( diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts index 24a84c2093..0d4c46c589 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts @@ -138,6 +138,18 @@ const processLocationFunction: TbFunction = ' }, 100);\n' + '}'; +const provisionSuccessFunction: TbFunction = + '// Function body to handle device provision success. \n' + + '// - deviceName - name of device that was successfully provisioned.\n' + + '\n' + + 'showDeviceProvisionSuccess(deviceName);\n' + + '\n' + + 'function showDeviceProvisionSuccess(deviceName) {\n' + + ' setTimeout(function() {\n' + + ' widgetContext.showSuccessToast(`Device ` + deviceName + ` was successfully provisioned`).subscribe();\n' + + ' }, 100);\n' + + '}\n'; + const handleEmptyResultFunctionTemplate: TbFunction = '// Optional function body to handle empty result. \n' + '// Usually this happens when user cancels the action (for ex. by pressing phone back button). \n\n' + @@ -145,7 +157,7 @@ const handleEmptyResultFunctionTemplate: TbFunction = '\n' + 'function showEmptyResultDialog(message) {\n' + ' setTimeout(function() {\n' + - ' widgetContext.dialogs.alert(\'Empty result\', message).subscribe();\n' + + ' widgetContext.showInfoToast(message).subscribe();\n' + ' }, 100);\n' + '}\n'; @@ -241,6 +253,8 @@ export const getDefaultProcessQrCodeFunction = () => processQrCodeFunction; export const getDefaultProcessLocationFunction = () => processLocationFunction; +export const getDefaultProvisionSuccessFunction = () => provisionSuccessFunction; + export const getDefaultGetLocationFunction = () => getLocationFunctionTemplate; export const getDefaultGetPhoneNumberFunction = () => getPhoneNumberFunctionTemplate; @@ -272,6 +286,9 @@ export const getDefaultHandleEmptyResultFunction = (type: WidgetMobileActionType case WidgetMobileActionType.takeScreenshot: message = 'Take screenshot action was cancelled!'; break; + case WidgetMobileActionType.deviceProvision: + message = 'Device provision was not invoked!'; + break; } return handleEmptyResultFunctionTemplate.replace('--MESSAGE--', message); }; @@ -303,6 +320,9 @@ export const getDefaultHandleErrorFunction = (type: WidgetMobileActionType): TbF case WidgetMobileActionType.takeScreenshot: title = 'Failed to take screenshot'; break; + case WidgetMobileActionType.deviceProvision: + title = 'Failed to make device provision'; + break; } return handleErrorFunctionTemplate.replace('--TITLE--', title); }; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts index b0b677e2b0..f7c68fec3d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts @@ -85,6 +85,10 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On @Input() datasource: Datasource; + @Input() + @coerceBoolean() + simpleRange = false; + @Input() @coerceBoolean() advancedMode = false; @@ -133,7 +137,7 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On writeValue(value: any): void { if (value) { let rangeList: ColorRangeSettings = {}; - if (isUndefined(value?.advancedMode) && value?.length) { + if (this.simpleRange || (isUndefined(value?.advancedMode) && value?.length)) { rangeList.advancedMode = false; rangeList.range = value; } else { @@ -229,7 +233,11 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On } updateModel() { - this.propagateChange(this.colorRangeListFormGroup.value); + if (this.simpleRange) { + this.propagateChange(this.colorRangeListFormGroup.get('range').value); + } else { + this.propagateChange(this.colorRangeListFormGroup.value); + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.html index f7d4b8e44b..6188371288 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.html @@ -93,7 +93,7 @@
{{ 'widgets.maps.data-layer.color-type-constant' | translate }} + + {{ 'widgets.maps.data-layer.color-type-range' | translate }} + {{ 'widgets.maps.data-layer.color-type-function' | translate }} @@ -35,6 +38,35 @@
+
+ + +
+
widgets.maps.data-layer.color-range
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.scss index b3d0e20de8..47ab07639b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.scss @@ -16,6 +16,8 @@ @import '../../../../../../../../../scss/constants'; .tb-data-layer-color-settings-panel { + --mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12); + --mat-form-field-trailing-icon-color: rgba(0,0,0,0.38); width: 700px; max-width: 90vw; min-height: 300px; @@ -50,4 +52,14 @@ justify-content: flex-end; align-items: flex-end; } + .tb-color-ranges-panel { + flex: 1; + min-height: 0; + gap: 16px; + display: flex; + flex-direction: column; + .tb-color-ranges { + --mat-icon-color: rgba(0, 0, 0, 0.38); + } + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts index 2ce10c25ed..895fb3e782 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts @@ -17,12 +17,15 @@ import { Component, DestroyRef, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { TbPopoverComponent } from '@shared/components/popover.component'; -import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { DataLayerColorSettings, DataLayerColorType } from '@shared/models/widget/maps/map.models'; +import { DataLayerColorSettings, DataLayerColorType, MapType } from '@shared/models/widget/maps/map.models'; +import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; @Component({ selector: 'tb-data-layer-color-settings-panel', @@ -33,9 +36,25 @@ import { DataLayerColorSettings, DataLayerColorType } from '@shared/models/widge }) export class DataLayerColorSettingsPanelComponent extends PageComponent implements OnInit { + widgetType = widgetType; + + DataKeyType = DataKeyType; + @Input() colorSettings: DataLayerColorSettings; + @Input() + context: MapSettingsContext; + + @Input() + dsType: DatasourceType; + + @Input() + dsEntityAliasId: string; + + @Input() + dsDeviceId: string; + @Input() helpId = 'widget/lib/map/color_fn'; @@ -63,14 +82,18 @@ export class DataLayerColorSettingsPanelComponent extends PageComponent implemen { type: [this.colorSettings?.type || DataLayerColorType.constant, []], color: [this.colorSettings?.color, []], + rangeKey: [this.colorSettings?.rangeKey, [Validators.required]], + range: [this.colorSettings?.range, []], colorFunction: [this.colorSettings?.colorFunction, []] } ); this.colorSettingsFormGroup.get('type').valueChanges.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe(() => { + this.updateValidators(); setTimeout(() => {this.popover?.updatePosition();}, 0); }); + this.updateValidators(); } cancel() { @@ -82,4 +105,25 @@ export class DataLayerColorSettingsPanelComponent extends PageComponent implemen this.colorSettingsApplied.emit(colorSettings); } + public editRangeKey() { + const targetDataKey: DataKey = this.colorSettingsFormGroup.get('rangeKey').value; + this.context.editKey(targetDataKey, + this.dsDeviceId, this.dsEntityAliasId, widgetType.latest).subscribe( + (updatedDataKey) => { + if (updatedDataKey) { + this.colorSettingsFormGroup.get('rangeKey').patchValue(updatedDataKey); + this.colorSettingsFormGroup.markAsDirty(); + } + } + ); + } + + private updateValidators() { + const type: DataLayerColorType = this.colorSettingsFormGroup.get('type').value; + if (type === DataLayerColorType.range) { + this.colorSettingsFormGroup.get('rangeKey').enable({emitEvent: false}); + } else { + this.colorSettingsFormGroup.get('rangeKey').disable({emitEvent: false}); + } + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts index 75cfb9c7bc..5f56bd85c3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings.component.ts @@ -16,13 +16,15 @@ import { Component, forwardRef, Input, Renderer2, ViewContainerRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { ComponentStyle } from '@shared/models/widget-settings.models'; +import { ColorType, ComponentStyle } from '@shared/models/widget-settings.models'; import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; import { DataLayerColorSettings, DataLayerColorType } from '@shared/models/widget/maps/map.models'; import { DataLayerColorSettingsPanelComponent } from '@home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; +import { DatasourceType } from '@shared/models/widget.models'; @Component({ selector: 'tb-data-layer-color-settings', @@ -41,6 +43,18 @@ export class DataLayerColorSettingsComponent implements ControlValueAccessor { @Input() disabled: boolean; + @Input() + context: MapSettingsContext; + + @Input() + dsType: DatasourceType; + + @Input() + dsEntityAliasId: string; + + @Input() + dsDeviceId: string; + @Input() helpId = 'widget/lib/map/color_fn'; @@ -85,6 +99,10 @@ export class DataLayerColorSettingsComponent implements ControlValueAccessor { } else { const ctx: any = { colorSettings: this.modelValue, + context: this.context, + dsType: this.dsType, + dsEntityAliasId: this.dsEntityAliasId, + dsDeviceId: this.dsDeviceId, helpId: this.helpId }; const colorSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, @@ -103,11 +121,23 @@ export class DataLayerColorSettingsComponent implements ControlValueAccessor { } private updateColorStyle() { - if (!this.disabled && this.modelValue) { - if (this.modelValue.type === DataLayerColorType.constant) { - this.colorStyle = {backgroundColor: this.modelValue.color}; + if (!this.disabled && this.modelValue && this.modelValue.type !== DataLayerColorType.function) { + let colors: string[] = [this.modelValue.color]; + const rangeList = this.modelValue.range; + if (this.modelValue.type === DataLayerColorType.range && rangeList?.length) { + const rangeColors = rangeList.slice(0, Math.min(2, rangeList.length)).map(r => r.color); + colors = colors.concat(rangeColors); + } + if (colors.length === 1) { + this.colorStyle = {backgroundColor: colors[0]}; } else { - this.colorStyle = {}; + const gradientValues: string[] = []; + const step = 100 / colors.length; + for (let i = 0; i < colors.length; i++) { + gradientValues.push(`${colors[i]} ${step*i}%`); + gradientValues.push(`${colors[i]} ${step*(i+1)}%`); + } + this.colorStyle = {background: `linear-gradient(90deg, ${gradientValues.join(', ')})`}; } } else { this.colorStyle = {}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index e4ae3a1c58..803cdd01f1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -200,11 +200,21 @@
widgets.maps.data-layer.marker.shape
- +
widgets.maps.data-layer.marker.icon
- +
widgets.maps.data-layer.marker.image
@@ -275,7 +285,12 @@ px - +
@@ -361,7 +376,12 @@ px - +
widgets.maps.data-layer.fill-color
- +
widgets.maps.data-layer.stroke
@@ -388,7 +413,12 @@ px - +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts index 7112a0c3c2..392c9f9565 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts @@ -123,7 +123,8 @@ export class MapLayerRowComponent implements ControlValueAccessor, OnInit { provider: [null, [Validators.required]], layerType: [null, [Validators.required]], tileUrl: [null, [Validators.required]], - apiKey: [null, [Validators.required]] + apiKey: [null, [Validators.required]], + referenceLayer: [null, []] }); this.layerFormGroup.valueChanges.pipe( takeUntilDestroyed(this.destroyRef) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html index c30033f7e1..cf4c4b90c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html @@ -82,6 +82,17 @@
+
+
widgets.maps.layer.reference.reference-layer
+ + + {{ 'widgets.maps.layer.reference.no-layer' | translate }} + + {{ referenceLayerTypeTranslationMap.get(layer) | translate }} + + + +
- + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts index 14e80e4fd2..4400c5a151 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-shape-settings.component.ts @@ -49,6 +49,8 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { MarkerIconShapesComponent } from '@home/components/widget/lib/settings/common/map/marker-icon-shapes.component'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; +import { DatasourceType } from '@shared/models/widget.models'; @Component({ selector: 'tb-marker-shape-settings', @@ -69,6 +71,18 @@ export class MarkerShapeSettingsComponent implements ControlValueAccessor, OnIni @Input() disabled: boolean; + @Input() + context: MapSettingsContext; + + @Input() + dsType: DatasourceType; + + @Input() + dsEntityAliasId: string; + + @Input() + dsDeviceId: string; + @Input() markerType: MarkerType; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html index 40b618dc88..24658a8ad7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html @@ -37,14 +37,12 @@ class="tb-widget-actions" [class]="{'tb-widget-actions-absolute': !(widget.showWidgetTitlePanel && !widgetComponent.widgetContext?.embedTitlePanel && (widget.showTitle||widget.hasAggregation))}" (mousedown)="$event.stopPropagation()"> - +
+ @for (action of widget.customHeaderActions; track action.name; let last = $last) { + + } +
+ + } + @case (widgetHeaderActionButtonType.basic) { + + } + @case (widgetHeaderActionButtonType.raised) { + + } + @case (widgetHeaderActionButtonType.stroked) { + + } + @case (widgetHeaderActionButtonType.flat) { + + } + @default { + + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss index 0894da25f7..b246bc1662 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss @@ -86,7 +86,7 @@ div.tb-widget { flex-direction: row; place-content: center flex-start; align-items: center; - z-index: 19; + z-index: 101; margin: 5px 0 0; &-absolute { 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 b6f19e62a1..430cce40b1 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 @@ -44,9 +44,10 @@ import { GridsterItemComponent } from 'angular-gridster2'; import { UtilsService } from '@core/services/utils.service'; import { from } from 'rxjs'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { TbContextMenuEvent } from '@shared/models/jquery-event.models'; +import { WidgetHeaderActionButtonType } from '@shared/models/widget.models'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import ITooltipsterGeoHelper = JQueryTooltipster.ITooltipsterGeoHelper; -import { TbContextMenuEvent } from '@shared/models/jquery-event.models'; export enum WidgetComponentActionType { MOUSE_DOWN, @@ -130,6 +131,8 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O return (this.isEditActionEnabled || this.isRemoveActionEnabled || this.isExportActionEnabled) && !this.widget?.isFullscreen; } + widgetHeaderActionButtonType = WidgetHeaderActionButtonType; + private cssClass: string; private editWidgetActionsTooltip: ITooltipsterInstance; 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 da56004ae2..f8fa032c8c 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 @@ -44,6 +44,7 @@ import { widgetActionSources, WidgetActionType, WidgetComparisonSettings, + WidgetHeaderActionButtonType, WidgetMobileActionDescriptor, WidgetMobileActionType, WidgetResource, @@ -298,7 +299,16 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, const headerAction: WidgetHeaderAction = { name: descriptor.name, displayName: descriptor.displayName, + buttonType: descriptor.buttonType, + showIcon: descriptor.showIcon, icon: descriptor.icon, + customButtonStyle: this.headerButtonStyle( + descriptor.buttonType, + descriptor.customButtonStyle, + descriptor.buttonColor, + descriptor.buttonFillColor, + descriptor.buttonBorderColor + ), descriptor, useShowWidgetHeaderActionFunction, showWidgetHeaderActionFunction, @@ -353,6 +363,39 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } } + headerButtonStyle(buttonType: WidgetHeaderActionButtonType = WidgetHeaderActionButtonType.icon, + customButtonStyle:{[key: string]: string}, + buttonColor: string = 'rgba(0,0,0,0.87)', + backgroundColor: string, + borderColor: string) { + const buttonStyle = {}; + switch (buttonType) { + case WidgetHeaderActionButtonType.basic: + buttonStyle['--mdc-text-button-label-text-color'] = buttonColor; + break; + case WidgetHeaderActionButtonType.raised: + buttonStyle['--mdc-protected-button-label-text-color'] = buttonColor; + buttonStyle['--mdc-protected-button-container-color'] = backgroundColor; + break; + case WidgetHeaderActionButtonType.stroked: + buttonStyle['--mdc-outlined-button-label-text-color'] = buttonColor; + buttonStyle['--mdc-outlined-button-outline-color'] = borderColor; + break; + case WidgetHeaderActionButtonType.flat: + buttonStyle['--mdc-filled-button-label-text-color'] = buttonColor; + buttonStyle['--mdc-filled-button-container-color'] = backgroundColor; + break; + case WidgetHeaderActionButtonType.miniFab: + buttonStyle['--mat-fab-small-foreground-color'] = buttonColor; + buttonStyle['--mdc-fab-small-container-color'] = backgroundColor; + break; + default: + buttonStyle['--mat-icon-color'] = buttonColor; + break; + } + return {...buttonStyle, ...customButtonStyle}; + } + ngOnChanges(changes: SimpleChanges): void { for (const propName of Object.keys(changes)) { const change = changes[propName]; @@ -1177,6 +1220,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, case WidgetMobileActionType.scanQrCode: case WidgetMobileActionType.getLocation: case WidgetMobileActionType.takeScreenshot: + case WidgetMobileActionType.deviceProvision: argsObservable = of([]); break; case WidgetMobileActionType.mapDirection: @@ -1266,6 +1310,26 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, ); } break; + case WidgetMobileActionType.deviceProvision: + const deviceName = actionResult.deviceName; + if (isNotEmptyTbFunction(mobileAction.handleProvisionSuccessFunction)) { + compileTbFunction(this.http, mobileAction.handleProvisionSuccessFunction, 'deviceName', '$event', 'widgetContext', 'entityId', + 'entityName', 'additionalParams', 'entityLabel').subscribe( + { + next: (compiled) => { + try { + compiled.execute(deviceName, $event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); + } catch (e) { + console.error(e); + } + }, + error: (err) => { + console.error(err); + } + } + ); + } + break; case WidgetMobileActionType.scanQrCode: const code = actionResult.code; const format = actionResult.format; 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 66bddabd62..11adb2f806 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 @@ -26,6 +26,7 @@ import { WidgetActionSource, WidgetConfig, WidgetControllerDescriptor, + WidgetHeaderActionButtonType, WidgetType, widgetType, WidgetTypeDescriptor, @@ -126,6 +127,12 @@ export type ShowWidgetHeaderActionFunction = (ctx: WidgetContext, data: Formatte export interface WidgetHeaderAction extends IWidgetAction { displayName: string; descriptor: WidgetActionDescriptor; + buttonType?: WidgetHeaderActionButtonType; + showIcon?:boolean; + buttonColor?: string; + buttonFillColor?: string; + buttonBorderColor?: string; + customButtonStyle?: {[key: string]: string}; useShowWidgetHeaderActionFunction: boolean; showWidgetHeaderActionFunction: CompiledTbFunction; } diff --git a/ui-ngx/src/app/modules/home/pages/admin/settings-card.scss b/ui-ngx/src/app/modules/home/pages/admin/settings-card.scss index 50cd35d9da..7c707dee6d 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/settings-card.scss +++ b/ui-ngx/src/app/modules/home/pages/admin/settings-card.scss @@ -19,6 +19,10 @@ .mat-mdc-card.settings-card { margin: 8px; + mat-card-header.mat-mdc-card-header { + align-items: center; + } + .fields-group { padding: 0 16px 8px; margin: 10px 0; diff --git a/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.scss b/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.scss index 137da30c50..0897a72ec1 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.scss +++ b/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.scss @@ -15,7 +15,6 @@ */ :host { .mat-mdc-card-header { - align-items: center; min-height: 64px; } diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html index 46aa08ff9f..b9003c4c1c 100644 --- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html @@ -45,9 +45,9 @@ + label="{{ this.detailsForm.get('profileData.alarms').value?.length + ? ('device-profile.alarm-rules-with-count' | translate: { count: this.detailsForm.get('profileData.alarms').value.length }) + : 'device-profile.alarm-rules' | translate }}">
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index d927f64fdd..2ac9b806e6 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -369,7 +369,7 @@ export class RuleChainPageComponent extends PageComponent private initHotKeys(): void { if (!this.hotKeys.length) { this.hotKeys.push( - new Hotkey('ctrl+a', (event: KeyboardEvent) => { + new Hotkey(['ctrl+a', 'meta+a'], (event: KeyboardEvent) => { if (this.enableHotKeys) { event.preventDefault(); this.ruleChainCanvas.modelService.selectAll(); @@ -380,7 +380,7 @@ export class RuleChainPageComponent extends PageComponent this.translate.instant('rulenode.select-all-objects')) ); this.hotKeys.push( - new Hotkey('ctrl+c', (event: KeyboardEvent) => { + new Hotkey(['ctrl+c', 'meta+c'], (event: KeyboardEvent) => { if (this.enableHotKeys) { event.preventDefault(); this.copyRuleNodes(); @@ -391,7 +391,7 @@ export class RuleChainPageComponent extends PageComponent this.translate.instant('rulenode.copy-selected')) ); this.hotKeys.push( - new Hotkey('ctrl+v', (event: KeyboardEvent) => { + new Hotkey(['ctrl+v', 'meta+v'], (event: KeyboardEvent) => { if (this.enableHotKeys) { event.preventDefault(); if (this.itembuffer.hasRuleNodes()) { @@ -416,7 +416,7 @@ export class RuleChainPageComponent extends PageComponent this.translate.instant('rulenode.deselect-all-objects')) ); this.hotKeys.push( - new Hotkey('ctrl+s', (event: KeyboardEvent) => { + new Hotkey(['ctrl+s', 'meta+s'], (event: KeyboardEvent) => { if (this.enableHotKeys) { event.preventDefault(); this.saveRuleChain(); @@ -427,7 +427,7 @@ export class RuleChainPageComponent extends PageComponent this.translate.instant('action.apply')) ); this.hotKeys.push( - new Hotkey('ctrl+z', (event: KeyboardEvent) => { + new Hotkey(['ctrl+z', 'meta+z'], (event: KeyboardEvent) => { if (this.enableHotKeys) { event.preventDefault(); this.revertRuleChain(); @@ -449,7 +449,7 @@ export class RuleChainPageComponent extends PageComponent this.translate.instant('rulenode.delete-selected-objects')) ); this.hotKeys.push( - new Hotkey('ctrl+r', (event: KeyboardEvent) => { + new Hotkey(['ctrl+r', 'meta+r'], (event: KeyboardEvent) => { if (this.enableHotKeys && this.canCreateNestedRuleChain()) { event.preventDefault(); this.createNestedRuleChain(); diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index 848550a94b..ac8054283a 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -391,7 +391,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit private updateView(value: string | EntityId | null, entity: BaseData | null) { if (!isEqual(this.modelValue, value)) { this.modelValue = value; - this.entityURL = !entity ? '' : getEntityDetailsPageURL(entity.id.id, entity.id.entityType as EntityType); + this.entityURL = (typeof entity === 'string' || !entity) ? '' : getEntityDetailsPageURL(entity.id.id, entity.id.entityType as EntityType); this.propagateChange(this.modelValue); this.entityChanged.emit(entity); } diff --git a/ui-ngx/src/app/shared/components/js-func.component.html b/ui-ngx/src/app/shared/components/js-func.component.html index 3155669f29..07f6dfe1b4 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.html +++ b/ui-ngx/src/app/shared/components/js-func.component.html @@ -41,7 +41,7 @@ {{'js-func.tidy' | translate }} -
+
; + @Input() helpPopupStyle: Record = {}; + @Input() @coerceBoolean() disableUndefinedCheck = false; @@ -560,6 +563,9 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal break; case 'scriptLanguage': this.updatedScriptLanguage(); + this.updateHighlightRules(); + this.updateCompleters(); + this.updateJsWorkerGlobals(); break; case 'disableUndefinedCheck': case 'globalVariables': @@ -576,19 +582,24 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal private updateHighlightRules(): void { // @ts-ignore - if (!!this.highlightRules && !!this.jsEditor.session.$mode) { + if (!!this.jsEditor.session.$mode) { // @ts-ignore const newMode = new this.jsEditor.session.$mode.constructor(); newMode.$highlightRules = new newMode.HighlightRules(); - for(const group in this.highlightRules) { - if(!!newMode.$highlightRules.$rules[group]) { - newMode.$highlightRules.$rules[group].unshift(...this.highlightRules[group]); - } else { - newMode.$highlightRules.$rules[group] = this.highlightRules[group]; + if (!!this.highlightRules) { + for(const group in this.highlightRules) { + if(!!newMode.$highlightRules.$rules[group]) { + newMode.$highlightRules.$rules[group].unshift(...this.highlightRules[group]); + } else { + newMode.$highlightRules.$rules[group] = this.highlightRules[group]; + } } } + if (this.scriptLanguage === ScriptLanguage.TBEL) { + newMode.$highlightRules.$rules.start = [...tbelUtilsFuncHighlightRules, ...newMode.$highlightRules.$rules.start]; + } const identifierRule = newMode.$highlightRules.$rules.no_regex.find(rule => rule.token?.includes('identifier')); - if (identifierRule) { + if (identifierRule && identifierRule.next === 'no_regex') { identifierRule.next = 'start'; } // @ts-ignore @@ -641,6 +652,9 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal if (modulesCompleter) { completers.push(modulesCompleter); } + if (this.scriptLanguage === ScriptLanguage.TBEL) { + completers.push(tbelUtilsAutocompletes); + } completers.push(...this.initialCompleters); this.jsEditor.completers = completers; }); diff --git a/ui-ngx/src/app/shared/components/markdown.component.ts b/ui-ngx/src/app/shared/components/markdown.component.ts index bee7da0a51..0db86a9eef 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.ts +++ b/ui-ngx/src/app/shared/components/markdown.component.ts @@ -32,16 +32,14 @@ import { ViewChild, ViewContainerRef } from '@angular/core'; -import { HelpService } from '@core/services/help.service'; import { MarkdownService, PrismPlugin } from 'ngx-markdown'; import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { SHARED_MODULE_TOKEN } from '@shared/components/tokens'; -import { deepClone, guid, isDefinedAndNotNull } from '@core/utils'; +import { guid, isDefinedAndNotNull } from '@core/utils'; import { Observable, of, ReplaySubject } from 'rxjs'; import { coerceBoolean } from '@shared/decorators/coercion'; -let defaultMarkdownStyle; +let defaultMarkdownStyle: string; @Component({ selector: 'tb-markdown', @@ -70,12 +68,12 @@ export class TbMarkdownComponent implements OnChanges { @Input() additionalStyles: string[]; @Input() - get lineNumbers(): boolean { return this.lineNumbersValue; } - set lineNumbers(value: boolean) { this.lineNumbersValue = coerceBooleanProperty(value); } + @coerceBoolean() + lineNumbers = false; @Input() - get fallbackToPlainMarkdown(): boolean { return this.fallbackToPlainMarkdownValue; } - set fallbackToPlainMarkdown(value: boolean) { this.fallbackToPlainMarkdownValue = coerceBooleanProperty(value); } + @coerceBoolean() + fallbackToPlainMarkdown = false; @Input() @coerceBoolean() @@ -83,9 +81,6 @@ export class TbMarkdownComponent implements OnChanges { @Output() ready = new EventEmitter(); - private lineNumbersValue = false; - private fallbackToPlainMarkdownValue = false; - isMarkdownReady = false; error = null; @@ -93,8 +88,7 @@ export class TbMarkdownComponent implements OnChanges { private tbMarkdownInstanceComponentRef: ComponentRef; private tbMarkdownInstanceComponentType: Type; - constructor(private help: HelpService, - private cd: ChangeDetectorRef, + constructor(private cd: ChangeDetectorRef, private zone: NgZone, public markdownService: MarkdownService, @Inject(SHARED_MODULE_TOKEN) private sharedModule: Type, @@ -102,8 +96,19 @@ export class TbMarkdownComponent implements OnChanges { private renderer: Renderer2) {} ngOnChanges(changes: SimpleChanges): void { - if (isDefinedAndNotNull(this.data)) { - this.zone.run(() => this.render(this.data)); + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (propName === 'data' && change.currentValue !== change.previousValue) { + if (isDefinedAndNotNull(this.data)) { + this.zone.run(() => this.render(this.data)); + } + } else if (propName === 'context' && !change.firstChange) { + if (this.context && this.tbMarkdownInstanceComponentRef) { + for (const propName of Object.keys(this.context)) { + this.tbMarkdownInstanceComponentRef.instance[propName] = this.context[propName]; + } + } + } } } @@ -134,8 +139,8 @@ export class TbMarkdownComponent implements OnChanges { if (this.applyDefaultMarkdownStyle) { if (!defaultMarkdownStyle) { const compDef = this.dynamicComponentFactoryService.getComponentDef(TbMarkdownComponent); - defaultMarkdownStyle = deepClone(compDef.styles[0]).replace(/\[_nghost\-%COMP%\]/g, '') - .replace(/\[_ngcontent\-%COMP%\]/g, ''); + defaultMarkdownStyle = compDef.styles[0].replace(/\[_nghost-%COMP%]/g, '') + .replace(/\[_ngcontent-%COMP%]/g, ''); } styles.push(defaultMarkdownStyle); } @@ -149,7 +154,7 @@ export class TbMarkdownComponent implements OnChanges { this.ready.emit(); }); } else { - const parent = this; + const destroyMarkdownInstanceResources = this.destroyMarkdownInstanceResources.bind(this); let compileModules = [this.sharedModule]; if (this.additionalCompileModules) { compileModules = compileModules.concat(this.additionalCompileModules); @@ -157,13 +162,14 @@ export class TbMarkdownComponent implements OnChanges { this.dynamicComponentFactoryService.createDynamicComponent( class TbMarkdownInstance { ngOnDestroy(): void { - parent.destroyMarkdownInstanceResources(); + destroyMarkdownInstanceResources(); } }, template, compileModules, true, styles - ).subscribe((componentType) => { + ).subscribe({ + next: (componentType) => { this.tbMarkdownInstanceComponentType = componentType; const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector}); try { @@ -187,20 +193,21 @@ export class TbMarkdownComponent implements OnChanges { this.ready.emit(); }); }, - (error) => { + error: (error) => { readyObservable = this.handleError(template, error, styles); this.cd.detectChanges(); readyObservable.subscribe(() => { this.ready.emit(); }); - }); + } + }); } } - private handleError(template: string, error, styles?: string[]): Observable { + private handleError(template: string, error: any, styles?: string[]): Observable { this.error = (error ? error + '' : 'Failed to render markdown!').replace(/\n/g, '
'); this.markdownContainer.clear(); - if (this.fallbackToPlainMarkdownValue) { + if (this.fallbackToPlainMarkdown) { return this.plainMarkdown(template, styles); } else { return of(null); @@ -209,7 +216,7 @@ export class TbMarkdownComponent implements OnChanges { private plainMarkdown(template: string, styles?: string[]): Observable { const element = this.fallbackElement.nativeElement; - let styleElement; + let styleElement: any; if (styles?.length) { const markdownClass = 'tb-markdown-view-' + guid(); let innerStyle = styles.join('\n'); @@ -244,7 +251,7 @@ export class TbMarkdownComponent implements OnChanges { if (imgs.length) { let totalImages = imgs.length; const imagesLoadedSubject = new ReplaySubject(); - imgs.each((index, img) => { + imgs.each((_index, img) => { $(img).one('load error', () => { totalImages--; if (totalImages === 0) { diff --git a/ui-ngx/src/app/shared/components/nav-tree.component.ts b/ui-ngx/src/app/shared/components/nav-tree.component.ts index 789b352a92..5b698a37d3 100644 --- a/ui-ngx/src/app/shared/components/nav-tree.component.ts +++ b/ui-ngx/src/app/shared/components/nav-tree.component.ts @@ -149,9 +149,9 @@ export class NavTreeComponent implements OnInit { this.treeElement = $('.tb-nav-tree-container', this.elementRef.nativeElement).jstree(config); - this.treeElement.on('select_node.jstree', (e: any, data) => { - const node: NavTreeNode = data.instance.get_selected(true)[0]; - if (this.onNodeSelected) { + this.treeElement.on('changed.jstree', (e: any, data) => { + if (this.onNodeSelected && data.action !== 'ready') { + const node: NavTreeNode = data.instance.get_selected(true)[0]; this.ngZone.run(() => this.onNodeSelected(node, e as Event)); } }); diff --git a/ui-ngx/src/app/shared/components/string-autocomplete.component.html b/ui-ngx/src/app/shared/components/string-autocomplete.component.html index 459e41fdd8..8a9a1a38d0 100644 --- a/ui-ngx/src/app/shared/components/string-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/string-autocomplete.component.html @@ -24,7 +24,7 @@ [matAutocomplete]="optionsAutocomplete">