diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md index 6d8889f98d..f5d093f424 100644 --- a/.github/ISSUE_TEMPLATE/---bug-report.md +++ b/.github/ISSUE_TEMPLATE/---bug-report.md @@ -3,7 +3,7 @@ name: "\U0001F41E Bug report" about: Create a report to help us improve title: "Your title here" labels: ['bug', 'unconfirmed'] -assignees: AndriichnekoDm +assignees: Ultrazombie --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index c73f810cd8..1168adaf15 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -3,7 +3,7 @@ name: Feature request about: Suggest an idea for this project title: "Your title here" labels: ['feature'] -assignees: 'AndriichnekoDm' +assignees: 'Ultrazombie' --- diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 2f15690f68..dea6318ac5 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -3,7 +3,7 @@ name: Question about: Describe your questions in detail title: "Your title here" labels: ['question'] -assignees: 'AndriichnekoDm' +assignees: 'Ultrazombie' --- diff --git a/application/src/main/data/json/demo/dashboards/rule_engine_statistics.json b/application/src/main/data/json/demo/dashboards/rule_engine_statistics.json index 83a96c6fe6..99ff00e602 100644 --- a/application/src/main/data/json/demo/dashboards/rule_engine_statistics.json +++ b/application/src/main/data/json/demo/dashboards/rule_engine_statistics.json @@ -13,6 +13,7 @@ "datasources": [ { "type": "entity", + "entityAliasId": "140f23dd-e3a0-ed98-6189-03c49d2d8018", "dataKeys": [ { "name": "ruleEngineException", @@ -51,7 +52,59 @@ "_hash": 0.7255162989552142 } ], - "entityAliasId": "140f23dd-e3a0-ed98-6189-03c49d2d8018" + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + }, + "latestDataKeys": [ + { + "name": "queueName", + "type": "entityField", + "label": "Queue name", + "color": "#ffc107", + "settings": { + "show": false, + "order": null, + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "", + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.8104572478982748, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "serviceId", + "type": "entityField", + "label": "Service Id", + "color": "#607d8b", + "settings": { + "show": false, + "order": null, + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "", + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.38329217099945034, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] } ], "timewindow": { @@ -71,7 +124,9 @@ "settings": { "showTimestamp": true, "displayPagination": true, - "defaultPageSize": 10 + "defaultPageSize": 10, + "enableSearch": true, + "enableSelectColumnDisplay": true }, "title": "Exceptions", "dropShadow": true, @@ -89,7 +144,10 @@ "iconColor": "rgba(0, 0, 0, 0.87)", "iconSize": "24px", "titleTooltip": "", - "displayTimewindow": true + "displayTimewindow": true, + "configMode": "basic", + "titleFont": null, + "titleColor": null }, "id": "5eb79712-5c24-3060-7e4f-6af36b8f842d", "typeFullFqn": "system.cards.timeseries_table" @@ -329,7 +387,25 @@ "statusList": [ "ACTIVE" ] - } + }, + "latestDataKeys": [ + { + "name": "queueName", + "type": "entityField", + "label": "Queue name", + "color": "#ffc107", + "settings": {}, + "_hash": 0.8012481564934415 + }, + { + "name": "serviceId", + "type": "entityField", + "label": "Service Id", + "color": "#607d8b", + "settings": {}, + "_hash": 0.0724871638610094 + } + ] } ], "timewindow": { @@ -724,7 +800,25 @@ "statusList": [ "ACTIVE" ] - } + }, + "latestDataKeys": [ + { + "name": "queueName", + "type": "entityField", + "label": "Queue name", + "color": "#f44336", + "settings": {}, + "_hash": 0.7242351292118758 + }, + { + "name": "serviceId", + "type": "entityField", + "label": "Service Id", + "color": "#ffc107", + "settings": {}, + "_hash": 0.3347262075244206 + } + ] } ], "timewindow": { @@ -1004,12 +1098,9 @@ "id": "140f23dd-e3a0-ed98-6189-03c49d2d8018", "alias": "TbServiceQueues", "filter": { - "type": "assetType", + "type": "entityType", "resolveMultiple": true, - "assetNameFilter": "", - "assetTypes": [ - "TbServiceQueue" - ] + "entityType": "QUEUE_STATS" } } }, diff --git a/application/src/main/data/json/system/widget_bundles/charts.json b/application/src/main/data/json/system/widget_bundles/charts.json index 979b241e67..a66975d1e2 100644 --- a/application/src/main/data/json/system/widget_bundles/charts.json +++ b/application/src/main/data/json/system/widget_bundles/charts.json @@ -12,10 +12,11 @@ "line_chart", "bar_chart", "point_chart", + "state_chart", + "bar_chart_with_labels", + "range_chart", "charts.basic_timeseries", "charts.state_chart", - "range_chart", - "bar_chart_with_labels", "charts.timeseries_bars_flot", "cards.aggregated_value_card", "charts.bars", diff --git a/application/src/main/data/json/system/widget_types/state_chart.json b/application/src/main/data/json/system/widget_types/state_chart.json index a78fc06f64..0af2631813 100644 --- a/application/src/main/data/json/system/widget_types/state_chart.json +++ b/application/src/main/data/json/system/widget_types/state_chart.json @@ -1,26 +1,34 @@ { - "fqn": "charts.state_chart", - "name": "State Chart", + "fqn": "state_chart", + "name": "State chart", "deprecated": false, - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAB9VBMVEUAAAAhlvMilvMymeE+nNVDoetInslNq/VZqOdpuPd3d3d5p5Z5suB6enp8fHyBgYGDg4ODqoqEhISIiIiKioqKuN2MjIyNjY2Ojo6QkJCRkZGSkpKUlJSVlZWWlpaXl5eYmJiZmZmampqcnJydnZ2enp6goKCgr3KhoaGioqKisXSjo6OjvsmkpKSlpaWlwdempqanp6eoqKiosGOo1vqpqampsGOqqqqqsWOrq6usrKysw9Wurq6wsLCysrK0tLS1tbW1wbK2tra3t7e4wau5ubm6urq7u7u8vLy9vb2+vr6/xbTAwMDAxbjCwsLC3ejDw8PExMTFxcXGxsbHx8fIv3jIyMjJycnKysrK5vzMzc7Nzc3Ozs7Pz8/QuDnRzcHS0tLT09PT39HU1NTU6/3VzLLV1dXV3sjW1tbX19fYuTHY2NjZ2dna0Ira2trb29vc3Nzex4De3t7f39/guyvhvCfhvCvh4eHh6Nbi4uLjvCTj4+PkyXbk5OTlvCTm5ubm693o6Ojpxl7q6urr6+vswTTs7Ozt7e3uvhju7u7vvhjv7+/wvxjw8PDx8fHyvxX0yTv09PT19fX29vb39/f4wyL4+Pj5+fn6+vr75J37+/v8whP8/Pz9/f39/v/+/v7/wQf/xRb/3HT/5JH/9tz/++/////APs7XAAAAAWJLR0Smt7AblQAAA1RJREFUeNrt3dlT01AUBvAEd8UNl2oLrdrFolZArUul1hWlVhQXFAUF1xaxIqi4FUQUrDsUClZi4nb+Th96S9NQkjDOOKZ+3wuZw8md++M25OXMlKOccJQnTUYocdRqdw8UAqTfOjG4lvr7uowOqbtAtG6o5OiGFoNDapuJHO8tFK4xOKS9msTVgoUiaQjPclnSl01snZ/y4ly2yBNZbRarbc+3yvdpt/hLawOPJp9ur7O0mRiEmzFkY1M6N/4I8qJpulzR2sBx1sgRjQuyA2pk+STdylw2VqR/FPFfWdcCvjhduiM7kVF2db1Nuspu7JGes+I76SRba7f0Wfnn/6F6It+mfo7m8TvYvh7KTiT3PQIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggPwXkFa7e7AQIP3WiUFzdl7LuBDFvJZxIYp5LeNCFPNaBn7Yc+e1KlheSYcrFCniL7LZqPl8cbp0TjavNcqu9rZJB9gdPdIJVnwjbWG1ndI95UzWM9V5rS9Ti3P4VWy1+5jXAgQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAMK+FeS3Ma2FeC/NamNfCCxEQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBA/hZkTKSkwSGn7VUJOmOLV241NiRSRZ2uYcvt+Io+Y0N83USm+MqGWEnU2JCqXqI1ZE+QhYiIy36tXR5INpOQbB5kftcmK85mtdey2iJekeWqX6/3kc+TLCQTrq6eEuZCgKTsbnNXBsIZOERJMedNQnry73XpW8UAKVhIyBPScVfQE9RqEaP7iMT6g+pdw7vKbxKl3IJqV8K7OUqXPHvG9UMGykk2lz1d4g5yvNXo6QqZiA41aHT5OwQTUWBJSrWrvlMwj1jFU2f1Q8K1dCysCUmVhdenNLvMRKZt3hGNriEnxfxlGqt9aPYRkb9bP6Q1SMFrmltMukKuMT2QpckWjc/WhP2lYB/TgjzdHyCKVM/gGXnsp+qY5hbba+hIVA/ETL0+9SepsoNiTs/igGpXZIhKqVv9QVJARIfXIWqfiM1nS+qBnHdae1V7Qss8ngEijRO5a6sMCAtdqv+HfgPwpNPbU6ipOwAAAABJRU5ErkJggg==", + "image": "tb-image:Y2hhcnRfKDUpLnN2Zw==:IlN0YXRlIGNoYXJ0IiBzeXN0ZW0gd2lkZ2V0IGltYWdl;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF80NDM3Xzk2ODU4KSI+CjxwYXRoIGQ9Ik0yLjc2NTYyIDEuODE2NDFMMC44ODI4MTIgN0gwLjExMzI4MUwyLjI4MTI1IDEuMzEyNUgyLjc3NzM0TDIuNzY1NjIgMS44MTY0MVpNNC4zNDM3NSA3TDIuNDU3MDMgMS44MTY0MUwyLjQ0NTMxIDEuMzEyNUgyLjk0MTQxTDUuMTE3MTkgN0g0LjM0Mzc1Wk00LjI0NjA5IDQuODk0NTNWNS41MTE3MkgxLjA1MDc4VjQuODk0NTNINC4yNDYwOVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNTQiLz4KPHBhdGggZD0iTTMuNjg3NSAzMi41NDY0SDIuMjQ2MDlMMi4yMzgyOCAzMS45NDA5SDMuNTQ2ODhDMy43NjMwMiAzMS45NDA5IDMuOTUxODIgMzEuOTA0NSA0LjExMzI4IDMxLjgzMTVDNC4yNzQ3NCAzMS43NTg2IDQuMzk5NzQgMzEuNjU0NSA0LjQ4ODI4IDMxLjUxOUM0LjU3OTQzIDMxLjM4MSA0LjYyNSAzMS4yMTcgNC42MjUgMzEuMDI2OUM0LjYyNSAzMC44MTg1IDQuNTg0NjQgMzAuNjQ5MyA0LjUwMzkxIDMwLjUxOUM0LjQyNTc4IDMwLjM4NjIgNC4zMDQ2OSAzMC4yODk5IDQuMTQwNjIgMzAuMjNDMy45NzkxNyAzMC4xNjc1IDMuNzczNDQgMzAuMTM2MiAzLjUyMzQ0IDMwLjEzNjJIMi40MTQwNlYzNS4yMDY1SDEuNjYwMTZWMjkuNTE5SDMuNTIzNDRDMy44MTUxIDI5LjUxOSA0LjA3NTUyIDI5LjU0OSA0LjMwNDY5IDI5LjYwODlDNC41MzM4NSAyOS42NjYyIDQuNzI3ODYgMjkuNzU3MyA0Ljg4NjcyIDI5Ljg4MjNDNS4wNDgxOCAzMC4wMDQ3IDUuMTcwNTcgMzAuMTYxIDUuMjUzOTEgMzAuMzUxMUM1LjMzNzI0IDMwLjU0MTIgNS4zNzg5MSAzMC43NjkgNS4zNzg5MSAzMS4wMzQ3QzUuMzc4OTEgMzEuMjY5IDUuMzE5MDEgMzEuNDgxMyA1LjE5OTIyIDMxLjY3MTRDNS4wNzk0MyAzMS44NTg5IDQuOTEyNzYgMzIuMDEyNSA0LjY5OTIyIDMyLjEzMjNDNC40ODgyOCAzMi4yNTIxIDQuMjQwODkgMzIuMzI4OSAzLjk1NzAzIDMyLjM2MjhMMy42ODc1IDMyLjU0NjRaTTMuNjUyMzQgMzUuMjA2NUgxLjk0OTIyTDIuMzc1IDM0LjU5MzNIMy42NTIzNEMzLjg5MTkzIDM0LjU5MzMgNC4wOTUwNSAzNC41NTE2IDQuMjYxNzIgMzQuNDY4M0M0LjQzMDk5IDM0LjM4NDkgNC41NTk5IDM0LjI2NzcgNC42NDg0NCAzNC4xMTY3QzQuNzM2OTggMzMuOTYzMSA0Ljc4MTI1IDMzLjc4MjEgNC43ODEyNSAzMy41NzM3QzQuNzgxMjUgMzMuMzYyOCA0Ljc0MzQ5IDMzLjE4MDUgNC42Njc5NyAzMy4wMjY5QzQuNTkyNDUgMzIuODczMiA0LjQ3Mzk2IDMyLjc1NDcgNC4zMTI1IDMyLjY3MTRDNC4xNTEwNCAzMi41ODgxIDMuOTQyNzEgMzIuNTQ2NCAzLjY4NzUgMzIuNTQ2NEgyLjYxMzI4TDIuNjIxMDkgMzEuOTQwOUg0LjA4OTg0TDQuMjUgMzIuMTU5N0M0LjUyMzQ0IDMyLjE4MzEgNC43NTUyMSAzMi4yNjEyIDQuOTQ1MzEgMzIuMzk0QzUuMTM1NDIgMzIuNTI0MyA1LjI3OTk1IDMyLjY5MDkgNS4zNzg5MSAzMi44OTRDNS40ODA0NyAzMy4wOTcyIDUuNTMxMjUgMzMuMzIxMSA1LjUzMTI1IDMzLjU2NTlDNS41MzEyNSAzMy45MjAxIDUuNDUzMTIgMzQuMjE5NiA1LjI5Njg4IDM0LjQ2NDRDNS4xNDMyMyAzNC43MDY1IDQuOTI1NzggMzQuODkxNCA0LjY0NDUzIDM1LjAxOUM0LjM2MzI4IDM1LjE0NCA0LjAzMjU1IDM1LjIwNjUgMy42NTIzNCAzNS4yMDY1WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC41NCIvPgo8cGF0aCBkPSJNNC4wOTM3NSA2MS42MDVINC44NDM3NUM0LjgwNDY5IDYxLjk2NDQgNC43MDE4MiA2Mi4yODYgNC41MzUxNiA2Mi41Njk4QzQuMzY4NDkgNjIuODUzNyA0LjEzMjgxIDYzLjA3ODkgMy44MjgxMiA2My4yNDU2QzMuNTIzNDQgNjMuNDA5NyAzLjE0MzIzIDYzLjQ5MTcgMi42ODc1IDYzLjQ5MTdDMi4zNTQxNyA2My40OTE3IDIuMDUwNzggNjMuNDI5MiAxLjc3NzM0IDYzLjMwNDJDMS41MDY1MSA2My4xNzkyIDEuMjczNDQgNjMuMDAyMSAxLjA3ODEyIDYyLjc3MjlDMC44ODI4MTIgNjIuNTQxMiAwLjczMTc3MSA2Mi4yNjM4IDAuNjI1IDYxLjk0MDlDMC41MjA4MzMgNjEuNjE1NCAwLjQ2ODc1IDYxLjI1MzQgMC40Njg3NSA2MC44NTVWNjAuMjg4NkMwLjQ2ODc1IDU5Ljg5MDEgMC41MjA4MzMgNTkuNTI5NSAwLjYyNSA1OS4yMDY1QzAuNzMxNzcxIDU4Ljg4MSAwLjg4NDExNSA1OC42MDI0IDEuMDgyMDMgNTguMzcwNkMxLjI4MjU1IDU4LjEzODggMS41MjM0NCA1Ny45NjA0IDEuODA0NjkgNTcuODM1NEMyLjA4NTk0IDU3LjcxMDQgMi40MDIzNCA1Ny42NDc5IDIuNzUzOTEgNTcuNjQ3OUMzLjE4MzU5IDU3LjY0NzkgMy41NDY4OCA1Ny43Mjg3IDMuODQzNzUgNTcuODkwMUM0LjE0MDYyIDU4LjA1MTYgNC4zNzEwOSA1OC4yNzU2IDQuNTM1MTYgNTguNTYyQzQuNzAxODIgNTguODQ1OSA0LjgwNDY5IDU5LjE3NTMgNC44NDM3NSA1OS41NTAzSDQuMDkzNzVDNC4wNTcyOSA1OS4yODQ3IDMuOTg5NTggNTkuMDU2OCAzLjg5MDYyIDU4Ljg2NjdDMy43OTE2NyA1OC42NzQgMy42NTEwNCA1OC41MjU2IDMuNDY4NzUgNTguNDIxNEMzLjI4NjQ2IDU4LjMxNzIgMy4wNDgxOCA1OC4yNjUxIDIuNzUzOTEgNTguMjY1MUMyLjUwMTMgNTguMjY1MSAyLjI3ODY1IDU4LjMxMzMgMi4wODU5NCA1OC40MDk3QzEuODk1ODMgNTguNTA2IDEuNzM1NjggNTguNjQyNyAxLjYwNTQ3IDU4LjgxOThDMS40Nzc4NiA1OC45OTY5IDEuMzgxNTEgNTkuMjA5MSAxLjMxNjQxIDU5LjQ1NjVDMS4yNTEzIDU5LjcwMzkgMS4yMTg3NSA1OS45Nzg3IDEuMjE4NzUgNjAuMjgwOFY2MC44NTVDMS4yMTg3NSA2MS4xMzM2IDEuMjQ3NCA2MS4zOTUzIDEuMzA0NjkgNjEuNjQwMUMxLjM2NDU4IDYxLjg4NDkgMS40NTQ0MyA2Mi4wOTk4IDEuNTc0MjIgNjIuMjg0N0MxLjY5NDAxIDYyLjQ2OTYgMS44NDYzNSA2Mi42MTU0IDIuMDMxMjUgNjIuNzIyMkMyLjIxNjE1IDYyLjgyNjMgMi40MzQ5IDYyLjg3ODQgMi42ODc1IDYyLjg3ODRDMy4wMDc4MSA2Mi44Nzg0IDMuMjYzMDIgNjIuODI3NiAzLjQ1MzEyIDYyLjcyNjFDMy42NDMyMyA2Mi42MjQ1IDMuNzg2NDYgNjIuNDc4NyAzLjg4MjgxIDYyLjI4ODZDMy45ODE3NyA2Mi4wOTg1IDQuMDUyMDggNjEuODcwNiA0LjA5Mzc1IDYxLjYwNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNTQiLz4KPHBhdGggZD0iTTIuMTk5MjIgOTEuNjIwMUgxLjAxMTcyTDEuMDE5NTMgOTEuMDA2OEgyLjE5OTIyQzIuNjA1NDcgOTEuMDA2OCAyLjk0NDAxIDkwLjkyMjIgMy4yMTQ4NCA5MC43NTI5QzMuNDg1NjggOTAuNTgxMSAzLjY4ODggOTAuMzQxNSAzLjgyNDIyIDkwLjAzNDJDMy45NjIyNCA4OS43MjQzIDQuMDMxMjUgODkuMzYyMyA0LjAzMTI1IDg4Ljk0ODJWODguNjAwNkM0LjAzMTI1IDg4LjI3NTEgMy45OTIxOSA4Ny45ODYgMy45MTQwNiA4Ny43MzM0QzMuODM1OTQgODcuNDc4MiAzLjcyMTM1IDg3LjI2MzMgMy41NzAzMSA4Ny4wODg5QzMuNDE5MjcgODYuOTExOCAzLjIzNDM4IDg2Ljc3NzcgMy4wMTU2MiA4Ni42ODY1QzIuNzk5NDggODYuNTk1NCAyLjU1MDc4IDg2LjU0OTggMi4yNjk1MyA4Ni41NDk4SDAuOTg4MjgxVjg1LjkzMjZIMi4yNjk1M0MyLjY0MTkzIDg1LjkzMjYgMi45ODE3NyA4NS45OTUxIDMuMjg5MDYgODYuMTIwMUMzLjU5NjM1IDg2LjI0MjUgMy44NjA2OCA4Ni40MjA5IDQuMDgyMDMgODYuNjU1M0M0LjMwNTk5IDg2Ljg4NyA0LjQ3Nzg2IDg3LjE2ODMgNC41OTc2NiA4Ny40OTlDNC43MTc0NSA4Ny44MjcxIDQuNzc3MzQgODguMTk2OSA0Ljc3NzM0IDg4LjYwODRWODguOTQ4MkM0Ljc3NzM0IDg5LjM1OTcgNC43MTc0NSA4OS43MzA4IDQuNTk3NjYgOTAuMDYxNUM0LjQ3Nzg2IDkwLjM4OTYgNC4zMDQ2OSA5MC42Njk2IDQuMDc4MTIgOTAuOTAxNEMzLjg1NDE3IDkxLjEzMzEgMy41ODMzMyA5MS4zMTE1IDMuMjY1NjIgOTEuNDM2NUMyLjk1MDUyIDkxLjU1ODkgMi41OTUwNSA5MS42MjAxIDIuMTk5MjIgOTEuNjIwMVpNMS40MTQwNiA4NS45MzI2VjkxLjYyMDFIMC42NjAxNTZWODUuOTMyNkgxLjQxNDA2WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC41NCIvPgo8cGF0aCBkPSJNNS4yNzM0NCAxMTkuMjE0VjExOS44MjdIMi4yNjE3MlYxMTkuMjE0SDUuMjczNDRaTTIuNDE0MDYgMTE0LjE0VjExOS44MjdIMS42NjAxNlYxMTQuMTRIMi40MTQwNlpNNC44NzUgMTE2LjU4NVYxMTcuMTk4SDIuMjYxNzJWMTE2LjU4NUg0Ljg3NVpNNS4yMzQzOCAxMTQuMTRWMTE0Ljc1N0gyLjI2MTcyVjExNC4xNEg1LjIzNDM4WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC41NCIvPgo8cGF0aCBkPSJNMTEgNC4xNjExM0wxODYgNC4xNjExNiIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiLz4KPHBhdGggZD0iTTExIDMzLjE2MTFMMTg2IDMzLjE2MTIiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS1vcGFjaXR5PSIwLjEyIi8+CjxwYXRoIGQ9Ik0xMSA2MS4xNjExTDE4NiA2MS4xNjEyIiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC4xMiIvPgo8cGF0aCBkPSJNMTEgODkuMTYxMUwxODYgODkuMTYxMiIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiLz4KPHBhdGggZD0iTTExIDExOC4xNjFMMTg2IDExOC4xNjEiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS1vcGFjaXR5PSIwLjEyIi8+CjxsaW5lIHgxPSI5LjIiIHkxPSIxNDUuOTYxIiB4Mj0iMTg4LjgiIHkyPSIxNDUuOTYxIiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC43IiBzdHJva2Utd2lkdGg9IjAuNCIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIvPgo8bGluZSB4MT0iMjYuNDUwMiIgeTE9IjE0OC4wNzIiIHgyPSIyNi40NTAyIiB5Mj0iMTQ3LjI1IiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC41IiBzdHJva2Utd2lkdGg9IjAuNSIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIvPgo8cGF0aCBkPSJNMTguNTA5MSAxNTIuMDI1VjE1Mi44OTNDMTguNTA5MSAxNTMuMzU5IDE4LjQ2NzQgMTUzLjc1MiAxOC4zODQxIDE1NC4wNzJDMTguMzAwNyAxNTQuMzkzIDE4LjE4MDkgMTU0LjY1IDE4LjAyNDcgMTU0Ljg0NkMxNy44Njg0IDE1NS4wNDEgMTcuNjc5NiAxNTUuMTgzIDE3LjQ1ODMgMTU1LjI3MUMxNy4yMzk1IDE1NS4zNTcgMTYuOTkyMSAxNTUuNCAxNi43MTYxIDE1NS40QzE2LjQ5NzMgMTU1LjQgMTYuMjk1NSAxNTUuMzczIDE2LjExMDYgMTU1LjMxOEMxNS45MjU3IDE1NS4yNjQgMTUuNzU5MSAxNTUuMTc2IDE1LjYxMDYgMTU1LjA1N0MxNS40NjQ4IDE1NC45MzQgMTUuMzM5OCAxNTQuNzc1IDE1LjIzNTYgMTU0LjU4QzE1LjEzMTUgMTU0LjM4NSAxNS4wNTIgMTU0LjE0OCAxNC45OTczIDE1My44NjlDMTQuOTQyNyAxNTMuNTkgMTQuOTE1MyAxNTMuMjY1IDE0LjkxNTMgMTUyLjg5M1YxNTIuMDI1QzE0LjkxNTMgMTUxLjU1OSAxNC45NTcgMTUxLjE2OSAxNS4wNDAzIDE1MC44NTRDMTUuMTI2MiAxNTAuNTM4IDE1LjI0NzMgMTUwLjI4NiAxNS40MDM2IDE1MC4wOTZDMTUuNTU5OCAxNDkuOTAzIDE1Ljc0NzMgMTQ5Ljc2NSAxNS45NjYxIDE0OS42ODJDMTYuMTg3NCAxNDkuNTk4IDE2LjQzNDggMTQ5LjU1NyAxNi43MDgzIDE0OS41NTdDMTYuOTI5NiAxNDkuNTU3IDE3LjEzMjggMTQ5LjU4NCAxNy4zMTc3IDE0OS42MzlDMTcuNTA1MiAxNDkuNjkxIDE3LjY3MTggMTQ5Ljc3NSAxNy44MTc3IDE0OS44OTNDMTcuOTYzNSAxNTAuMDA3IDE4LjA4NzIgMTUwLjE2MSAxOC4xODg3IDE1MC4zNTRDMTguMjkyOSAxNTAuNTQ0IDE4LjM3MjMgMTUwLjc3NyAxOC40MjcgMTUxLjA1M0MxOC40ODE3IDE1MS4zMjkgMTguNTA5MSAxNTEuNjUzIDE4LjUwOTEgMTUyLjAyNVpNMTcuNzgyNSAxNTMuMDFWMTUxLjkwNEMxNy43ODI1IDE1MS42NDkgMTcuNzY2OSAxNTEuNDI1IDE3LjczNTYgMTUxLjIzMkMxNy43MDcgMTUxLjAzNyAxNy42NjQgMTUwLjg3IDE3LjYwNjcgMTUwLjczMkMxNy41NDk0IDE1MC41OTQgMTcuNDc2NSAxNTAuNDgyIDE3LjM4OCAxNTAuMzk2QzE3LjMwMiAxNTAuMzExIDE3LjIwMTggMTUwLjI0OCAxNy4wODcyIDE1MC4yMDlDMTYuOTc1MiAxNTAuMTY3IDE2Ljg0ODkgMTUwLjE0NiAxNi43MDgzIDE1MC4xNDZDMTYuNTM2NCAxNTAuMTQ2IDE2LjM4NDEgMTUwLjE3OSAxNi4yNTEyIDE1MC4yNDRDMTYuMTE4NCAxNTAuMzA3IDE2LjAwNjUgMTUwLjQwNyAxNS45MTUzIDE1MC41NDVDMTUuODI2OCAxNTAuNjgzIDE1Ljc1OTEgMTUwLjg2NCAxNS43MTIyIDE1MS4wODhDMTUuNjY1MyAxNTEuMzEyIDE1LjY0MTkgMTUxLjU4NCAxNS42NDE5IDE1MS45MDRWMTUzLjAxQzE1LjY0MTkgMTUzLjI2NSAxNS42NTYyIDE1My40OSAxNS42ODQ4IDE1My42ODZDMTUuNzE2MSAxNTMuODgxIDE1Ljc2MTcgMTU0LjA1IDE1LjgyMTYgMTU0LjE5M0MxNS44ODE1IDE1NC4zMzQgMTUuOTU0NCAxNTQuNDUgMTYuMDQwMyAxNTQuNTQxQzE2LjEyNjIgMTU0LjYzMiAxNi4yMjUyIDE1NC43IDE2LjMzNzIgMTU0Ljc0NEMxNi40NTE4IDE1NC43ODYgMTYuNTc4MSAxNTQuODA3IDE2LjcxNjEgMTU0LjgwN0MxNi44OTMyIDE1NC44MDcgMTcuMDQ4MSAxNTQuNzczIDE3LjE4MDkgMTU0LjcwNUMxNy4zMTM3IDE1NC42MzcgMTcuNDI0NCAxNTQuNTMyIDE3LjUxMyAxNTQuMzg5QzE3LjYwNDEgMTU0LjI0MyAxNy42NzE4IDE1NC4wNTcgMTcuNzE2MSAxNTMuODNDMTcuNzYwNCAxNTMuNjAxIDE3Ljc4MjUgMTUzLjMyNyAxNy43ODI1IDE1My4wMVpNMjEuODk2NCAxNDkuNjA0VjE1NS4zMjJIMjEuMTczN1YxNTAuNTA2TDE5LjcxNjcgMTUxLjAzN1YxNTAuMzg1TDIxLjc4MzEgMTQ5LjYwNEgyMS44OTY0Wk0yNy4xMTI0IDE0OS42MzVWMTU1LjMyMkgyNi4zNTg1VjE0OS42MzVIMjcuMTEyNFpNMjkuNDk1MiAxNTIuMTkzVjE1Mi44MTFIMjYuOTQ4M1YxNTIuMTkzSDI5LjQ5NTJaTTI5Ljg4MTkgMTQ5LjYzNVYxNTAuMjUySDI2Ljk0ODNWMTQ5LjYzNUgyOS44ODE5Wk0zMi40MjE2IDE1NS40QzMyLjEyNzMgMTU1LjQgMzEuODYwNCAxNTUuMzUxIDMxLjYyMDggMTU1LjI1MkMzMS4zODM4IDE1NS4xNSAzMS4xNzk0IDE1NS4wMDggMzEuMDA3NSAxNTQuODI2QzMwLjgzODMgMTU0LjY0NCAzMC43MDgxIDE1NC40MjggMzAuNjE2OSAxNTQuMTc4QzMwLjUyNTggMTUzLjkyOCAzMC40ODAyIDE1My42NTQgMzAuNDgwMiAxNTMuMzU3VjE1My4xOTNDMzAuNDgwMiAxNTIuODUgMzAuNTMxIDE1Mi41NDQgMzAuNjMyNSAxNTIuMjc1QzMwLjczNDEgMTUyLjAwNSAzMC44NzIxIDE1MS43NzUgMzEuMDQ2NiAxNTEuNTg4QzMxLjIyMTEgMTUxLjQgMzEuNDE5IDE1MS4yNTggMzEuNjQwMyAxNTEuMTYyQzMxLjg2MTcgMTUxLjA2NiAzMi4wOTA5IDE1MS4wMTggMzIuMzI3OCAxNTEuMDE4QzMyLjYyOTkgMTUxLjAxOCAzMi44OTAzIDE1MS4wNyAzMy4xMDkxIDE1MS4xNzRDMzMuMzMwNSAxNTEuMjc4IDMzLjUxMTQgMTUxLjQyNCAzMy42NTIxIDE1MS42MTFDMzMuNzkyNyAxNTEuNzk2IDMzLjg5NjkgMTUyLjAxNSAzMy45NjQ2IDE1Mi4yNjhDMzQuMDMyMyAxNTIuNTE4IDM0LjA2NjEgMTUyLjc5MSAzNC4wNjYxIDE1My4wODhWMTUzLjQxMkgzMC45MDk5VjE1Mi44MjJIMzMuMzQzNVYxNTIuNzY4QzMzLjMzMzEgMTUyLjU4IDMzLjI5NCAxNTIuMzk4IDMzLjIyNjMgMTUyLjIyMUMzMy4xNjEyIDE1Mi4wNDQgMzMuMDU3IDE1MS44OTggMzIuOTEzOCAxNTEuNzgzQzMyLjc3MDYgMTUxLjY2OSAzMi41NzUyIDE1MS42MTEgMzIuMzI3OCAxNTEuNjExQzMyLjE2MzggMTUxLjYxMSAzMi4wMTI3IDE1MS42NDYgMzEuODc0NyAxNTEuNzE3QzMxLjczNjcgMTUxLjc4NSAzMS42MTgyIDE1MS44ODYgMzEuNTE5MyAxNTIuMDIxQzMxLjQyMDMgMTUyLjE1NyAzMS4zNDM1IDE1Mi4zMjIgMzEuMjg4OCAxNTIuNTE4QzMxLjIzNDEgMTUyLjcxMyAzMS4yMDY4IDE1Mi45MzggMzEuMjA2OCAxNTMuMTkzVjE1My4zNTdDMzEuMjA2OCAxNTMuNTU4IDMxLjIzNDEgMTUzLjc0NyAzMS4yODg4IDE1My45MjRDMzEuMzQ2MSAxNTQuMDk4IDMxLjQyODEgMTU0LjI1MiAzMS41MzQ5IDE1NC4zODVDMzEuNjQ0MyAxNTQuNTE4IDMxLjc3NTggMTU0LjYyMiAzMS45Mjk0IDE1NC42OTdDMzIuMDg1NyAxNTQuNzczIDMyLjI2MjcgMTU0LjgxMSAzMi40NjA3IDE1NC44MTFDMzIuNzE1OSAxNTQuODExIDMyLjkzMiAxNTQuNzU4IDMzLjEwOTEgMTU0LjY1NEMzMy4yODYyIDE1NC41NSAzMy40NDExIDE1NC40MTEgMzMuNTczOSAxNTQuMjM2TDM0LjAxMTQgMTU0LjU4NEMzMy45MjAzIDE1NC43MjIgMzMuODA0NCAxNTQuODU0IDMzLjY2MzggMTU0Ljk3OUMzMy41MjMyIDE1NS4xMDQgMzMuMzUgMTU1LjIwNSAzMy4xNDQzIDE1NS4yODNDMzIuOTQxMSAxNTUuMzYxIDMyLjcwMDIgMTU1LjQgMzIuNDIxNiAxNTUuNFpNMzQuOTg4NiAxNDkuMzIySDM1LjcxNTJWMTU0LjUwMkwzNS42NTI3IDE1NS4zMjJIMzQuOTg4NlYxNDkuMzIyWk0zOC41NzA2IDE1My4xNzRWMTUzLjI1NkMzOC41NzA2IDE1My41NjMgMzguNTM0MiAxNTMuODQ4IDM4LjQ2MTMgMTU0LjExMUMzOC4zODgzIDE1NC4zNzIgMzguMjgxNiAxNTQuNTk4IDM4LjE0MDkgMTU0Ljc5MUMzOC4wMDAzIDE1NC45ODQgMzcuODI4NCAxNTUuMTMzIDM3LjYyNTMgMTU1LjI0QzM3LjQyMjIgMTU1LjM0NyAzNy4xODkxIDE1NS40IDM2LjkyNjEgMTU1LjRDMzYuNjU3OSAxNTUuNCAzNi40MjIyIDE1NS4zNTUgMzYuMjE5MSAxNTUuMjY0QzM2LjAxODUgMTU1LjE3IDM1Ljg0OTMgMTU1LjAzNiAzNS43MTEzIDE1NC44NjFDMzUuNTczMiAxNTQuNjg3IDM1LjQ2MjYgMTU0LjQ3NiAzNS4zNzkyIDE1NC4yMjlDMzUuMjk4NSAxNTMuOTgxIDM1LjI0MjUgMTUzLjcwMiAzNS4yMTEzIDE1My4zOTNWMTUzLjAzM0MzNS4yNDI1IDE1Mi43MjEgMzUuMjk4NSAxNTIuNDQxIDM1LjM3OTIgMTUyLjE5M0MzNS40NjI2IDE1MS45NDYgMzUuNTczMiAxNTEuNzM1IDM1LjcxMTMgMTUxLjU2MUMzNS44NDkzIDE1MS4zODMgMzYuMDE4NSAxNTEuMjQ5IDM2LjIxOTEgMTUxLjE1OEMzNi40MTk2IDE1MS4wNjQgMzYuNjUyNyAxNTEuMDE4IDM2LjkxODMgMTUxLjAxOEMzNy4xODM5IDE1MS4wMTggMzcuNDE5NiAxNTEuMDcgMzcuNjI1MyAxNTEuMTc0QzM3LjgzMSAxNTEuMjc1IDM4LjAwMjkgMTUxLjQyMSAzOC4xNDA5IDE1MS42MTFDMzguMjgxNiAxNTEuODAxIDM4LjM4ODMgMTUyLjAyOSAzOC40NjEzIDE1Mi4yOTVDMzguNTM0MiAxNTIuNTU4IDM4LjU3MDYgMTUyLjg1MSAzOC41NzA2IDE1My4xNzRaTTM3Ljg0NDEgMTUzLjI1NlYxNTMuMTc0QzM3Ljg0NDEgMTUyLjk2MyAzNy44MjQ1IDE1Mi43NjUgMzcuNzg1NSAxNTIuNThDMzcuNzQ2NCAxNTIuMzkzIDM3LjY4MzkgMTUyLjIyOSAzNy41OTggMTUyLjA4OEMzNy41MTIgMTUxLjk0NSAzNy4zOTg4IDE1MS44MzMgMzcuMjU4MSAxNTEuNzUyQzM3LjExNzUgMTUxLjY2OSAzNi45NDQzIDE1MS42MjcgMzYuNzM4NiAxNTEuNjI3QzM2LjU1NjMgMTUxLjYyNyAzNi4zOTc1IDE1MS42NTggMzYuMjYyIDE1MS43MjFDMzYuMTI5MiAxNTEuNzgzIDM2LjAxNTkgMTUxLjg2OCAzNS45MjIyIDE1MS45NzVDMzUuODI4NCAxNTIuMDc5IDM1Ljc1MTYgMTUyLjE5OSAzNS42OTE3IDE1Mi4zMzRDMzUuNjM0NCAxNTIuNDY3IDM1LjU5MTUgMTUyLjYwNSAzNS41NjI4IDE1Mi43NDhWMTUzLjY4OUMzNS42MDQ1IDE1My44NzIgMzUuNjcyMiAxNTQuMDQ4IDM1Ljc2NTkgMTU0LjIxN0MzNS44NjIzIDE1NC4zODMgMzUuOTg5OSAxNTQuNTIgMzYuMTQ4OCAxNTQuNjI3QzM2LjMxMDIgMTU0LjczNCAzNi41MDk0IDE1NC43ODcgMzYuNzQ2NCAxNTQuNzg3QzM2Ljk0MTcgMTU0Ljc4NyAzNy4xMDg0IDE1NC43NDggMzcuMjQ2NCAxNTQuNjdDMzcuMzg3IDE1NC41ODkgMzcuNTAwMyAxNTQuNDc5IDM3LjU4NjMgMTU0LjMzOEMzNy42NzQ4IDE1NC4xOTcgMzcuNzM5OSAxNTQuMDM1IDM3Ljc4MTYgMTUzLjg1QzM3LjgyMzIgMTUzLjY2NSAzNy44NDQxIDE1My40NjcgMzcuODQ0MSAxNTMuMjU2WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC41NCIvPgo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDFfNDQzN185Njg1OCkiPgo8bGluZSB4MT0iNjEuODUwMSIgeTE9IjE0Ny4wNzIiIHgyPSI2MS44NTAxIiB5Mj0iMTQ2LjI1IiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC41IiBzdHJva2Utd2lkdGg9IjAuNSIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIvPgo8cGF0aCBkPSJNNTMuOTA5IDE1Mi4wMjVWMTUyLjg5M0M1My45MDkgMTUzLjM1OSA1My44NjczIDE1My43NTIgNTMuNzg0IDE1NC4wNzJDNTMuNzAwNiAxNTQuMzkzIDUzLjU4MDggMTU0LjY1IDUzLjQyNDYgMTU0Ljg0NkM1My4yNjgzIDE1NS4wNDEgNTMuMDc5NSAxNTUuMTgzIDUyLjg1ODIgMTU1LjI3MUM1Mi42Mzk0IDE1NS4zNTcgNTIuMzkyIDE1NS40IDUyLjExNiAxNTUuNEM1MS44OTcyIDE1NS40IDUxLjY5NTQgMTU1LjM3MyA1MS41MTA1IDE1NS4zMThDNTEuMzI1NiAxNTUuMjY0IDUxLjE1OSAxNTUuMTc2IDUxLjAxMDUgMTU1LjA1N0M1MC44NjQ3IDE1NC45MzQgNTAuNzM5NyAxNTQuNzc1IDUwLjYzNTUgMTU0LjU4QzUwLjUzMTQgMTU0LjM4NSA1MC40NTE5IDE1NC4xNDggNTAuMzk3MiAxNTMuODY5QzUwLjM0MjYgMTUzLjU5IDUwLjMxNTIgMTUzLjI2NSA1MC4zMTUyIDE1Mi44OTNWMTUyLjAyNUM1MC4zMTUyIDE1MS41NTkgNTAuMzU2OSAxNTEuMTY5IDUwLjQ0MDIgMTUwLjg1NEM1MC41MjYxIDE1MC41MzggNTAuNjQ3MiAxNTAuMjg2IDUwLjgwMzUgMTUwLjA5NkM1MC45NTk3IDE0OS45MDMgNTEuMTQ3MiAxNDkuNzY1IDUxLjM2NiAxNDkuNjgyQzUxLjU4NzMgMTQ5LjU5OCA1MS44MzQ3IDE0OS41NTcgNTIuMTA4MiAxNDkuNTU3QzUyLjMyOTUgMTQ5LjU1NyA1Mi41MzI3IDE0OS41ODQgNTIuNzE3NiAxNDkuNjM5QzUyLjkwNTEgMTQ5LjY5MSA1My4wNzE3IDE0OS43NzUgNTMuMjE3NiAxNDkuODkzQzUzLjM2MzQgMTUwLjAwNyA1My40ODcxIDE1MC4xNjEgNTMuNTg4NiAxNTAuMzU0QzUzLjY5MjggMTUwLjU0NCA1My43NzIyIDE1MC43NzcgNTMuODI2OSAxNTEuMDUzQzUzLjg4MTYgMTUxLjMyOSA1My45MDkgMTUxLjY1MyA1My45MDkgMTUyLjAyNVpNNTMuMTgyNCAxNTMuMDFWMTUxLjkwNEM1My4xODI0IDE1MS42NDkgNTMuMTY2OCAxNTEuNDI1IDUzLjEzNTUgMTUxLjIzMkM1My4xMDY5IDE1MS4wMzcgNTMuMDYzOSAxNTAuODcgNTMuMDA2NiAxNTAuNzMyQzUyLjk0OTMgMTUwLjU5NCA1Mi44NzY0IDE1MC40ODIgNTIuNzg3OSAxNTAuMzk2QzUyLjcwMTkgMTUwLjMxMSA1Mi42MDE3IDE1MC4yNDggNTIuNDg3MSAxNTAuMjA5QzUyLjM3NTEgMTUwLjE2NyA1Mi4yNDg4IDE1MC4xNDYgNTIuMTA4MiAxNTAuMTQ2QzUxLjkzNjMgMTUwLjE0NiA1MS43ODQgMTUwLjE3OSA1MS42NTExIDE1MC4yNDRDNTEuNTE4MyAxNTAuMzA3IDUxLjQwNjQgMTUwLjQwNyA1MS4zMTUyIDE1MC41NDVDNTEuMjI2NyAxNTAuNjgzIDUxLjE1OSAxNTAuODY0IDUxLjExMjEgMTUxLjA4OEM1MS4wNjUyIDE1MS4zMTIgNTEuMDQxOCAxNTEuNTg0IDUxLjA0MTggMTUxLjkwNFYxNTMuMDFDNTEuMDQxOCAxNTMuMjY1IDUxLjA1NjEgMTUzLjQ5IDUxLjA4NDcgMTUzLjY4NkM1MS4xMTYgMTUzLjg4MSA1MS4xNjE2IDE1NC4wNSA1MS4yMjE1IDE1NC4xOTNDNTEuMjgxNCAxNTQuMzM0IDUxLjM1NDMgMTU0LjQ1IDUxLjQ0MDIgMTU0LjU0MUM1MS41MjYxIDE1NC42MzIgNTEuNjI1MSAxNTQuNyA1MS43MzcxIDE1NC43NDRDNTEuODUxNyAxNTQuNzg2IDUxLjk3OCAxNTQuODA3IDUyLjExNiAxNTQuODA3QzUyLjI5MzEgMTU0LjgwNyA1Mi40NDggMTU0Ljc3MyA1Mi41ODA4IDE1NC43MDVDNTIuNzEzNiAxNTQuNjM3IDUyLjgyNDMgMTU0LjUzMiA1Mi45MTI5IDE1NC4zODlDNTMuMDA0IDE1NC4yNDMgNTMuMDcxNyAxNTQuMDU3IDUzLjExNiAxNTMuODNDNTMuMTYwMyAxNTMuNjAxIDUzLjE4MjQgMTUzLjMyNyA1My4xODI0IDE1My4wMVpNNTguNjQ3OCAxNTQuNzI5VjE1NS4zMjJINTQuOTI1MlYxNTQuODAzTDU2Ljc4ODUgMTUyLjcyOUM1Ny4wMTc2IDE1Mi40NzMgNTcuMTk0NyAxNTIuMjU3IDU3LjMxOTcgMTUyLjA4QzU3LjQ0NzMgMTUxLjkgNTcuNTM1OSAxNTEuNzQgNTcuNTg1MyAxNTEuNkM1Ny42Mzc0IDE1MS40NTYgNTcuNjYzNSAxNTEuMzExIDU3LjY2MzUgMTUxLjE2MkM1Ny42NjM1IDE1MC45NzUgNTcuNjI0NCAxNTAuODA1IDU3LjU0NjMgMTUwLjY1NEM1Ny40NzA4IDE1MC41MDEgNTcuMzU4OCAxNTAuMzc4IDU3LjIxMDMgMTUwLjI4N0M1Ny4wNjE5IDE1MC4xOTYgNTYuODgyMiAxNTAuMTUgNTYuNjcxMyAxNTAuMTVDNTYuNDE4NyAxNTAuMTUgNTYuMjA3NyAxNTAuMiA1Ni4wMzg1IDE1MC4yOTlDNTUuODcxOCAxNTAuMzk1IDU1Ljc0NjggMTUwLjUzMSA1NS42NjM1IDE1MC43MDVDNTUuNTgwMSAxNTAuODggNTUuNTM4NSAxNTEuMDggNTUuNTM4NSAxNTEuMzA3SDU0LjgxNThDNTQuODE1OCAxNTAuOTg2IDU0Ljg4NjEgMTUwLjY5MyA1NS4wMjY3IDE1MC40MjhDNTUuMTY3NCAxNTAuMTYyIDU1LjM3NTcgMTQ5Ljk1MSA1NS42NTE3IDE0OS43OTVDNTUuOTI3OCAxNDkuNjM2IDU2LjI2NzYgMTQ5LjU1NyA1Ni42NzEzIDE0OS41NTdDNTcuMDMwNyAxNDkuNTU3IDU3LjMzNzkgMTQ5LjYyIDU3LjU5MzIgMTQ5Ljc0OEM1Ny44NDg0IDE0OS44NzMgNTguMDQzNyAxNTAuMDUgNTguMTc5MSAxNTAuMjc5QzU4LjMxNzEgMTUwLjUwNiA1OC4zODYxIDE1MC43NzEgNTguMzg2MSAxNTEuMDc2QzU4LjM4NjEgMTUxLjI0MyA1OC4zNTc1IDE1MS40MTIgNTguMzAwMiAxNTEuNTg0QzU4LjI0NTUgMTUxLjc1MyA1OC4xNjg3IDE1MS45MjMgNTguMDY5NyAxNTIuMDkyQzU3Ljk3MzQgMTUyLjI2MSA1Ny44NjAxIDE1Mi40MjggNTcuNzI5OSAxNTIuNTkyQzU3LjYwMjMgMTUyLjc1NiA1Ny40NjU1IDE1Mi45MTcgNTcuMzE5NyAxNTMuMDc2TDU1Ljc5NjMgMTU0LjcyOUg1OC42NDc4Wk02Mi41MTIzIDE0OS42MzVWMTU1LjMyMkg2MS43NTg0VjE0OS42MzVINjIuNTEyM1pNNjQuODk1MSAxNTIuMTkzVjE1Mi44MTFINjIuMzQ4MlYxNTIuMTkzSDY0Ljg5NTFaTTY1LjI4MTggMTQ5LjYzNVYxNTAuMjUySDYyLjM0ODJWMTQ5LjYzNUg2NS4yODE4Wk02Ny44MjE1IDE1NS40QzY3LjUyNzIgMTU1LjQgNjcuMjYwMyAxNTUuMzUxIDY3LjAyMDcgMTU1LjI1MkM2Ni43ODM3IDE1NS4xNSA2Ni41NzkzIDE1NS4wMDggNjYuNDA3NCAxNTQuODI2QzY2LjIzODIgMTU0LjY0NCA2Ni4xMDggMTU0LjQyOCA2Ni4wMTY4IDE1NC4xNzhDNjUuOTI1NyAxNTMuOTI4IDY1Ljg4MDEgMTUzLjY1NCA2NS44ODAxIDE1My4zNTdWMTUzLjE5M0M2NS44ODAxIDE1Mi44NSA2NS45MzA5IDE1Mi41NDQgNjYuMDMyNCAxNTIuMjc1QzY2LjEzNCAxNTIuMDA1IDY2LjI3MiAxNTEuNzc1IDY2LjQ0NjUgMTUxLjU4OEM2Ni42MjEgMTUxLjQgNjYuODE4OSAxNTEuMjU4IDY3LjA0MDIgMTUxLjE2MkM2Ny4yNjE2IDE1MS4wNjYgNjcuNDkwOCAxNTEuMDE4IDY3LjcyNzcgMTUxLjAxOEM2OC4wMjk4IDE1MS4wMTggNjguMjkwMiAxNTEuMDcgNjguNTA5IDE1MS4xNzRDNjguNzMwNCAxNTEuMjc4IDY4LjkxMTMgMTUxLjQyNCA2OS4wNTIgMTUxLjYxMUM2OS4xOTI2IDE1MS43OTYgNjkuMjk2OCAxNTIuMDE1IDY5LjM2NDUgMTUyLjI2OEM2OS40MzIyIDE1Mi41MTggNjkuNDY2IDE1Mi43OTEgNjkuNDY2IDE1My4wODhWMTUzLjQxMkg2Ni4zMDk4VjE1Mi44MjJINjguNzQzNFYxNTIuNzY4QzY4LjczMyAxNTIuNTggNjguNjkzOSAxNTIuMzk4IDY4LjYyNjIgMTUyLjIyMUM2OC41NjExIDE1Mi4wNDQgNjguNDU2OSAxNTEuODk4IDY4LjMxMzcgMTUxLjc4M0M2OC4xNzA1IDE1MS42NjkgNjcuOTc1MSAxNTEuNjExIDY3LjcyNzcgMTUxLjYxMUM2Ny41NjM3IDE1MS42MTEgNjcuNDEyNiAxNTEuNjQ2IDY3LjI3NDYgMTUxLjcxN0M2Ny4xMzY2IDE1MS43ODUgNjcuMDE4MSAxNTEuODg2IDY2LjkxOTIgMTUyLjAyMUM2Ni44MjAyIDE1Mi4xNTcgNjYuNzQzNCAxNTIuMzIyIDY2LjY4ODcgMTUyLjUxOEM2Ni42MzQgMTUyLjcxMyA2Ni42MDY3IDE1Mi45MzggNjYuNjA2NyAxNTMuMTkzVjE1My4zNTdDNjYuNjA2NyAxNTMuNTU4IDY2LjYzNCAxNTMuNzQ3IDY2LjY4ODcgMTUzLjkyNEM2Ni43NDYgMTU0LjA5OCA2Ni44MjggMTU0LjI1MiA2Ni45MzQ4IDE1NC4zODVDNjcuMDQ0MiAxNTQuNTE4IDY3LjE3NTcgMTU0LjYyMiA2Ny4zMjkzIDE1NC42OTdDNjcuNDg1NiAxNTQuNzczIDY3LjY2MjYgMTU0LjgxMSA2Ny44NjA2IDE1NC44MTFDNjguMTE1OCAxNTQuODExIDY4LjMzMTkgMTU0Ljc1OCA2OC41MDkgMTU0LjY1NEM2OC42ODYxIDE1NC41NSA2OC44NDEgMTU0LjQxMSA2OC45NzM4IDE1NC4yMzZMNjkuNDExMyAxNTQuNTg0QzY5LjMyMDIgMTU0LjcyMiA2OS4yMDQzIDE1NC44NTQgNjkuMDYzNyAxNTQuOTc5QzY4LjkyMzEgMTU1LjEwNCA2OC43NDk5IDE1NS4yMDUgNjguNTQ0MiAxNTUuMjgzQzY4LjM0MSAxNTUuMzYxIDY4LjEwMDEgMTU1LjQgNjcuODIxNSAxNTUuNFpNNzAuMzg4NSAxNDkuMzIySDcxLjExNTFWMTU0LjUwMkw3MS4wNTI2IDE1NS4zMjJINzAuMzg4NVYxNDkuMzIyWk03My45NzA1IDE1My4xNzRWMTUzLjI1NkM3My45NzA1IDE1My41NjMgNzMuOTM0MSAxNTMuODQ4IDczLjg2MTIgMTU0LjExMUM3My43ODgyIDE1NC4zNzIgNzMuNjgxNSAxNTQuNTk4IDczLjU0MDggMTU0Ljc5MUM3My40MDAyIDE1NC45ODQgNzMuMjI4MyAxNTUuMTMzIDczLjAyNTIgMTU1LjI0QzcyLjgyMjEgMTU1LjM0NyA3Mi41ODkgMTU1LjQgNzIuMzI2IDE1NS40QzcyLjA1NzggMTU1LjQgNzEuODIyMSAxNTUuMzU1IDcxLjYxOSAxNTUuMjY0QzcxLjQxODUgMTU1LjE3IDcxLjI0OTIgMTU1LjAzNiA3MS4xMTEyIDE1NC44NjFDNzAuOTczMSAxNTQuNjg3IDcwLjg2MjUgMTU0LjQ3NiA3MC43NzkxIDE1NC4yMjlDNzAuNjk4NCAxNTMuOTgxIDcwLjY0MjQgMTUzLjcwMiA3MC42MTEyIDE1My4zOTNWMTUzLjAzM0M3MC42NDI0IDE1Mi43MjEgNzAuNjk4NCAxNTIuNDQxIDcwLjc3OTEgMTUyLjE5M0M3MC44NjI1IDE1MS45NDYgNzAuOTczMSAxNTEuNzM1IDcxLjExMTIgMTUxLjU2MUM3MS4yNDkyIDE1MS4zODMgNzEuNDE4NSAxNTEuMjQ5IDcxLjYxOSAxNTEuMTU4QzcxLjgxOTUgMTUxLjA2NCA3Mi4wNTI2IDE1MS4wMTggNzIuMzE4MiAxNTEuMDE4QzcyLjU4MzggMTUxLjAxOCA3Mi44MTk1IDE1MS4wNyA3My4wMjUyIDE1MS4xNzRDNzMuMjMxIDE1MS4yNzUgNzMuNDAyOCAxNTEuNDIxIDczLjU0MDggMTUxLjYxMUM3My42ODE1IDE1MS44MDEgNzMuNzg4MiAxNTIuMDI5IDczLjg2MTIgMTUyLjI5NUM3My45MzQxIDE1Mi41NTggNzMuOTcwNSAxNTIuODUxIDczLjk3MDUgMTUzLjE3NFpNNzMuMjQ0IDE1My4yNTZWMTUzLjE3NEM3My4yNDQgMTUyLjk2MyA3My4yMjQ0IDE1Mi43NjUgNzMuMTg1NCAxNTIuNThDNzMuMTQ2MyAxNTIuMzkzIDczLjA4MzggMTUyLjIyOSA3Mi45OTc5IDE1Mi4wODhDNzIuOTExOSAxNTEuOTQ1IDcyLjc5ODcgMTUxLjgzMyA3Mi42NTggMTUxLjc1MkM3Mi41MTc0IDE1MS42NjkgNzIuMzQ0MiAxNTEuNjI3IDcyLjEzODUgMTUxLjYyN0M3MS45NTYyIDE1MS42MjcgNzEuNzk3NCAxNTEuNjU4IDcxLjY2MTkgMTUxLjcyMUM3MS41MjkxIDE1MS43ODMgNzEuNDE1OCAxNTEuODY4IDcxLjMyMjEgMTUxLjk3NUM3MS4yMjgzIDE1Mi4wNzkgNzEuMTUxNSAxNTIuMTk5IDcxLjA5MTYgMTUyLjMzNEM3MS4wMzQzIDE1Mi40NjcgNzAuOTkxNCAxNTIuNjA1IDcwLjk2MjcgMTUyLjc0OFYxNTMuNjg5QzcxLjAwNDQgMTUzLjg3MiA3MS4wNzIxIDE1NC4wNDggNzEuMTY1OCAxNTQuMjE3QzcxLjI2MjIgMTU0LjM4MyA3MS4zODk4IDE1NC41MiA3MS41NDg3IDE1NC42MjdDNzEuNzEwMSAxNTQuNzM0IDcxLjkwOTMgMTU0Ljc4NyA3Mi4xNDYzIDE1NC43ODdDNzIuMzQxNiAxNTQuNzg3IDcyLjUwODMgMTU0Ljc0OCA3Mi42NDYzIDE1NC42N0M3Mi43ODY5IDE1NC41ODkgNzIuOTAwMiAxNTQuNDc5IDcyLjk4NjIgMTU0LjMzOEM3My4wNzQ3IDE1NC4xOTcgNzMuMTM5OCAxNTQuMDM1IDczLjE4MTUgMTUzLjg1QzczLjIyMzEgMTUzLjY2NSA3My4yNDQgMTUzLjQ2NyA3My4yNDQgMTUzLjI1NloiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNTQiLz4KPC9nPgo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDJfNDQzN185Njg1OCkiPgo8bGluZSB4MT0iOTcuMjUiIHkxPSIxNDcuMDcyIiB4Mj0iOTcuMjUiIHkyPSIxNDYuMjUiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS1vcGFjaXR5PSIwLjUiIHN0cm9rZS13aWR0aD0iMC41IiBzdHJva2UtbGluZWNhcD0ic3F1YXJlIi8+CjxwYXRoIGQ9Ik04OS4zMDg5IDE1Mi4wMjVWMTUyLjg5M0M4OS4zMDg5IDE1My4zNTkgODkuMjY3MiAxNTMuNzUyIDg5LjE4MzkgMTU0LjA3MkM4OS4xMDA1IDE1NC4zOTMgODguOTgwNyAxNTQuNjUgODguODI0NSAxNTQuODQ2Qzg4LjY2ODIgMTU1LjA0MSA4OC40Nzk0IDE1NS4xODMgODguMjU4MSAxNTUuMjcxQzg4LjAzOTMgMTU1LjM1NyA4Ny43OTE5IDE1NS40IDg3LjUxNTkgMTU1LjRDODcuMjk3MSAxNTUuNCA4Ny4wOTUzIDE1NS4zNzMgODYuOTEwNCAxNTUuMzE4Qzg2LjcyNTUgMTU1LjI2NCA4Ni41NTg5IDE1NS4xNzYgODYuNDEwNCAxNTUuMDU3Qzg2LjI2NDYgMTU0LjkzNCA4Ni4xMzk2IDE1NC43NzUgODYuMDM1NCAxNTQuNThDODUuOTMxMyAxNTQuMzg1IDg1Ljg1MTggMTU0LjE0OCA4NS43OTcxIDE1My44NjlDODUuNzQyNSAxNTMuNTkgODUuNzE1MSAxNTMuMjY1IDg1LjcxNTEgMTUyLjg5M1YxNTIuMDI1Qzg1LjcxNTEgMTUxLjU1OSA4NS43NTY4IDE1MS4xNjkgODUuODQwMSAxNTAuODU0Qzg1LjkyNjEgMTUwLjUzOCA4Ni4wNDcxIDE1MC4yODYgODYuMjAzNCAxNTAuMDk2Qzg2LjM1OTYgMTQ5LjkwMyA4Ni41NDcxIDE0OS43NjUgODYuNzY1OSAxNDkuNjgyQzg2Ljk4NzIgMTQ5LjU5OCA4Ny4yMzQ2IDE0OS41NTcgODcuNTA4MSAxNDkuNTU3Qzg3LjcyOTQgMTQ5LjU1NyA4Ny45MzI2IDE0OS41ODQgODguMTE3NSAxNDkuNjM5Qzg4LjMwNSAxNDkuNjkxIDg4LjQ3MTYgMTQ5Ljc3NSA4OC42MTc1IDE0OS44OTNDODguNzYzMyAxNTAuMDA3IDg4Ljg4NyAxNTAuMTYxIDg4Ljk4ODYgMTUwLjM1NEM4OS4wOTI3IDE1MC41NDQgODkuMTcyMSAxNTAuNzc3IDg5LjIyNjggMTUxLjA1M0M4OS4yODE1IDE1MS4zMjkgODkuMzA4OSAxNTEuNjUzIDg5LjMwODkgMTUyLjAyNVpNODguNTgyMyAxNTMuMDFWMTUxLjkwNEM4OC41ODIzIDE1MS42NDkgODguNTY2NyAxNTEuNDI1IDg4LjUzNTQgMTUxLjIzMkM4OC41MDY4IDE1MS4wMzcgODguNDYzOCAxNTAuODcgODguNDA2NSAxNTAuNzMyQzg4LjM0OTIgMTUwLjU5NCA4OC4yNzYzIDE1MC40ODIgODguMTg3OCAxNTAuMzk2Qzg4LjEwMTggMTUwLjMxMSA4OC4wMDE2IDE1MC4yNDggODcuODg3IDE1MC4yMDlDODcuNzc1IDE1MC4xNjcgODcuNjQ4NyAxNTAuMTQ2IDg3LjUwODEgMTUwLjE0NkM4Ny4zMzYyIDE1MC4xNDYgODcuMTgzOSAxNTAuMTc5IDg3LjA1MTEgMTUwLjI0NEM4Ni45MTgyIDE1MC4zMDcgODYuODA2MyAxNTAuNDA3IDg2LjcxNTEgMTUwLjU0NUM4Ni42MjY2IDE1MC42ODMgODYuNTU4OSAxNTAuODY0IDg2LjUxMiAxNTEuMDg4Qzg2LjQ2NTEgMTUxLjMxMiA4Ni40NDE3IDE1MS41ODQgODYuNDQxNyAxNTEuOTA0VjE1My4wMUM4Ni40NDE3IDE1My4yNjUgODYuNDU2IDE1My40OSA4Ni40ODQ2IDE1My42ODZDODYuNTE1OSAxNTMuODgxIDg2LjU2MTUgMTU0LjA1IDg2LjYyMTQgMTU0LjE5M0M4Ni42ODEzIDE1NC4zMzQgODYuNzU0MiAxNTQuNDUgODYuODQwMSAxNTQuNTQxQzg2LjkyNjEgMTU0LjYzMiA4Ny4wMjUgMTU0LjcgODcuMTM3IDE1NC43NDRDODcuMjUxNiAxNTQuNzg2IDg3LjM3NzkgMTU0LjgwNyA4Ny41MTU5IDE1NC44MDdDODcuNjkzIDE1NC44MDcgODcuODQ3OSAxNTQuNzczIDg3Ljk4MDcgMTU0LjcwNUM4OC4xMTM2IDE1NC42MzcgODguMjI0MiAxNTQuNTMyIDg4LjMxMjggMTU0LjM4OUM4OC40MDM5IDE1NC4yNDMgODguNDcxNiAxNTQuMDU3IDg4LjUxNTkgMTUzLjgzQzg4LjU2MDIgMTUzLjYwMSA4OC41ODIzIDE1My4zMjcgODguNTgyMyAxNTMuMDFaTTkxLjM3NTkgMTUyLjEyM0g5MS44OTE1QzkyLjE0NDEgMTUyLjEyMyA5Mi4zNTI0IDE1Mi4wODEgOTIuNTE2NSAxNTEuOTk4QzkyLjY4MzIgMTUxLjkxMiA5Mi44MDY5IDE1MS43OTYgOTIuODg3NiAxNTEuNjVDOTIuOTcwOSAxNTEuNTAyIDkzLjAxMjYgMTUxLjMzNSA5My4wMTI2IDE1MS4xNUM5My4wMTI2IDE1MC45MzIgOTIuOTc2MSAxNTAuNzQ4IDkyLjkwMzIgMTUwLjZDOTIuODMwMyAxNTAuNDUxIDkyLjcyMDkgMTUwLjMzOSA5Mi41NzUxIDE1MC4yNjRDOTIuNDI5MyAxNTAuMTg4IDkyLjI0NDQgMTUwLjE1IDkyLjAyMDQgMTUwLjE1QzkxLjgxNzMgMTUwLjE1IDkxLjYzNzYgMTUwLjE5MSA5MS40ODEzIDE1MC4yNzFDOTEuMzI3NyAxNTAuMzUgOTEuMjA2NiAxNTAuNDYyIDkxLjExODEgMTUwLjYwN0M5MS4wMzIxIDE1MC43NTMgOTAuOTg5MSAxNTAuOTI1IDkwLjk4OTEgMTUxLjEyM0g5MC4yNjY1QzkwLjI2NjUgMTUwLjgzNCA5MC4zMzk0IDE1MC41NzEgOTAuNDg1MiAxNTAuMzM0QzkwLjYzMTEgMTUwLjA5NyA5MC44MzU1IDE0OS45MDggOTEuMDk4NSAxNDkuNzY4QzkxLjM2NDEgMTQ5LjYyNyA5MS42NzE0IDE0OS41NTcgOTIuMDIwNCAxNDkuNTU3QzkyLjM2NDEgMTQ5LjU1NyA5Mi42NjQ5IDE0OS42MTggOTIuOTIyNyAxNDkuNzRDOTMuMTgwNiAxNDkuODYgOTMuMzgxMSAxNTAuMDQgOTMuNTI0MyAxNTAuMjc5QzkzLjY2NzUgMTUwLjUxNiA5My43MzkxIDE1MC44MTIgOTMuNzM5MSAxNTEuMTY2QzkzLjczOTEgMTUxLjMwOSA5My43MDUzIDE1MS40NjMgOTMuNjM3NiAxNTEuNjI3QzkzLjU3MjUgMTUxLjc4OCA5My40Njk2IDE1MS45MzkgOTMuMzI5IDE1Mi4wOEM5My4xOTEgMTUyLjIyMSA5My4wMTEzIDE1Mi4zMzcgOTIuNzg5OSAxNTIuNDI4QzkyLjU2ODYgMTUyLjUxNiA5Mi4zMDI5IDE1Mi41NjEgOTEuOTkzMSAxNTIuNTYxSDkxLjM3NTlWMTUyLjEyM1pNOTEuMzc1OSAxNTIuNzE3VjE1Mi4yODNIOTEuOTkzMUM5Mi4zNTUgMTUyLjI4MyA5Mi42NTQ1IDE1Mi4zMjYgOTIuODkxNSAxNTIuNDEyQzkzLjEyODUgMTUyLjQ5OCA5My4zMTQ3IDE1Mi42MTMgOTMuNDUwMSAxNTIuNzU2QzkzLjU4ODEgMTUyLjg5OSA5My42ODQ1IDE1My4wNTcgOTMuNzM5MSAxNTMuMjI5QzkzLjc5NjQgMTUzLjM5OCA5My44MjUxIDE1My41NjcgOTMuODI1MSAxNTMuNzM2QzkzLjgyNTEgMTU0LjAwMiA5My43Nzk1IDE1NC4yMzggOTMuNjg4NCAxNTQuNDQzQzkzLjU5OTggMTU0LjY0OSA5My40NzM1IDE1NC44MjQgOTMuMzA5NSAxNTQuOTY3QzkzLjE0OCAxNTUuMTEgOTIuOTU3OSAxNTUuMjE4IDkyLjczOTEgMTU1LjI5MUM5Mi41MjA0IDE1NS4zNjQgOTIuMjgyMSAxNTUuNCA5Mi4wMjQzIDE1NS40QzkxLjc3NjkgMTU1LjQgOTEuNTQzOCAxNTUuMzY1IDkxLjMyNTEgMTU1LjI5NUM5MS4xMDg5IDE1NS4yMjUgOTAuOTE3NSAxNTUuMTIzIDkwLjc1MDkgMTU0Ljk5QzkwLjU4NDIgMTU0Ljg1NSA5MC40NTQgMTU0LjY4OSA5MC4zNjAyIDE1NC40OTRDOTAuMjY2NSAxNTQuMjk2IDkwLjIxOTYgMTU0LjA3MSA5MC4yMTk2IDE1My44MThIOTAuOTQyM0M5MC45NDIzIDE1NC4wMTYgOTAuOTg1MiAxNTQuMTg5IDkxLjA3MTIgMTU0LjMzOEM5MS4xNTk3IDE1NC40ODYgOTEuMjg0NyAxNTQuNjAyIDkxLjQ0NjIgMTU0LjY4NkM5MS42MTAyIDE1NC43NjYgOTEuODAyOSAxNTQuODA3IDkyLjAyNDMgMTU0LjgwN0M5Mi4yNDU3IDE1NC44MDcgOTIuNDM1OCAxNTQuNzY5IDkyLjU5NDYgMTU0LjY5M0M5Mi43NTYxIDE1NC42MTUgOTIuODc5OCAxNTQuNDk4IDkyLjk2NTcgMTU0LjM0MkM5My4wNTQzIDE1NC4xODYgOTMuMDk4NSAxNTMuOTg5IDkzLjA5ODUgMTUzLjc1MkM5My4wOTg1IDE1My41MTUgOTMuMDQ5IDE1My4zMjEgOTIuOTUwMSAxNTMuMTdDOTIuODUxMSAxNTMuMDE2IDkyLjcxMDUgMTUyLjkwMyA5Mi41MjgyIDE1Mi44M0M5Mi4zNDg1IDE1Mi43NTUgOTIuMTM2MyAxNTIuNzE3IDkxLjg5MTUgMTUyLjcxN0g5MS4zNzU5Wk05Ny45MTIyIDE0OS42MzVWMTU1LjMyMkg5Ny4xNTgzVjE0OS42MzVIOTcuOTEyMlpNMTAwLjI5NSAxNTIuMTkzVjE1Mi44MTFIOTcuNzQ4MVYxNTIuMTkzSDEwMC4yOTVaTTEwMC42ODIgMTQ5LjYzNVYxNTAuMjUySDk3Ljc0ODFWMTQ5LjYzNUgxMDAuNjgyWk0xMDMuMjIxIDE1NS40QzEwMi45MjcgMTU1LjQgMTAyLjY2IDE1NS4zNTEgMTAyLjQyMSAxNTUuMjUyQzEwMi4xODQgMTU1LjE1IDEwMS45NzkgMTU1LjAwOCAxMDEuODA3IDE1NC44MjZDMTAxLjYzOCAxNTQuNjQ0IDEwMS41MDggMTU0LjQyOCAxMDEuNDE3IDE1NC4xNzhDMTAxLjMyNiAxNTMuOTI4IDEwMS4yOCAxNTMuNjU0IDEwMS4yOCAxNTMuMzU3VjE1My4xOTNDMTAxLjI4IDE1Mi44NSAxMDEuMzMxIDE1Mi41NDQgMTAxLjQzMiAxNTIuMjc1QzEwMS41MzQgMTUyLjAwNSAxMDEuNjcyIDE1MS43NzUgMTAxLjg0NiAxNTEuNTg4QzEwMi4wMjEgMTUxLjQgMTAyLjIxOSAxNTEuMjU4IDEwMi40NCAxNTEuMTYyQzEwMi42NjIgMTUxLjA2NiAxMDIuODkxIDE1MS4wMTggMTAzLjEyOCAxNTEuMDE4QzEwMy40MyAxNTEuMDE4IDEwMy42OSAxNTEuMDcgMTAzLjkwOSAxNTEuMTc0QzEwNC4xMyAxNTEuMjc4IDEwNC4zMTEgMTUxLjQyNCAxMDQuNDUyIDE1MS42MTFDMTA0LjU5MiAxNTEuNzk2IDEwNC42OTcgMTUyLjAxNSAxMDQuNzY0IDE1Mi4yNjhDMTA0LjgzMiAxNTIuNTE4IDEwNC44NjYgMTUyLjc5MSAxMDQuODY2IDE1My4wODhWMTUzLjQxMkgxMDEuNzFWMTUyLjgyMkgxMDQuMTQzVjE1Mi43NjhDMTA0LjEzMyAxNTIuNTggMTA0LjA5NCAxNTIuMzk4IDEwNC4wMjYgMTUyLjIyMUMxMDMuOTYxIDE1Mi4wNDQgMTAzLjg1NyAxNTEuODk4IDEwMy43MTQgMTUxLjc4M0MxMDMuNTcgMTUxLjY2OSAxMDMuMzc1IDE1MS42MTEgMTAzLjEyOCAxNTEuNjExQzEwMi45NjQgMTUxLjYxMSAxMDIuODEzIDE1MS42NDYgMTAyLjY3NSAxNTEuNzE3QzEwMi41MzcgMTUxLjc4NSAxMDIuNDE4IDE1MS44ODYgMTAyLjMxOSAxNTIuMDIxQzEwMi4yMiAxNTIuMTU3IDEwMi4xNDMgMTUyLjMyMiAxMDIuMDg5IDE1Mi41MThDMTAyLjAzNCAxNTIuNzEzIDEwMi4wMDcgMTUyLjkzOCAxMDIuMDA3IDE1My4xOTNWMTUzLjM1N0MxMDIuMDA3IDE1My41NTggMTAyLjAzNCAxNTMuNzQ3IDEwMi4wODkgMTUzLjkyNEMxMDIuMTQ2IDE1NC4wOTggMTAyLjIyOCAxNTQuMjUyIDEwMi4zMzUgMTU0LjM4NUMxMDIuNDQ0IDE1NC41MTggMTAyLjU3NiAxNTQuNjIyIDEwMi43MjkgMTU0LjY5N0MxMDIuODg1IDE1NC43NzMgMTAzLjA2MyAxNTQuODExIDEwMy4yNiAxNTQuODExQzEwMy41MTYgMTU0LjgxMSAxMDMuNzMyIDE1NC43NTggMTAzLjkwOSAxNTQuNjU0QzEwNC4wODYgMTU0LjU1IDEwNC4yNDEgMTU0LjQxMSAxMDQuMzc0IDE1NC4yMzZMMTA0LjgxMSAxNTQuNTg0QzEwNC43MiAxNTQuNzIyIDEwNC42MDQgMTU0Ljg1NCAxMDQuNDY0IDE1NC45NzlDMTA0LjMyMyAxNTUuMTA0IDEwNC4xNSAxNTUuMjA1IDEwMy45NDQgMTU1LjI4M0MxMDMuNzQxIDE1NS4zNjEgMTAzLjUgMTU1LjQgMTAzLjIyMSAxNTUuNFpNMTA1Ljc4OCAxNDkuMzIySDEwNi41MTVWMTU0LjUwMkwxMDYuNDUyIDE1NS4zMjJIMTA1Ljc4OFYxNDkuMzIyWk0xMDkuMzcgMTUzLjE3NFYxNTMuMjU2QzEwOS4zNyAxNTMuNTYzIDEwOS4zMzQgMTUzLjg0OCAxMDkuMjYxIDE1NC4xMTFDMTA5LjE4OCAxNTQuMzcyIDEwOS4wODEgMTU0LjU5OCAxMDguOTQxIDE1NC43OTFDMTA4LjggMTU0Ljk4NCAxMDguNjI4IDE1NS4xMzMgMTA4LjQyNSAxNTUuMjRDMTA4LjIyMiAxNTUuMzQ3IDEwNy45ODkgMTU1LjQgMTA3LjcyNiAxNTUuNEMxMDcuNDU4IDE1NS40IDEwNy4yMjIgMTU1LjM1NSAxMDcuMDE5IDE1NS4yNjRDMTA2LjgxOCAxNTUuMTcgMTA2LjY0OSAxNTUuMDM2IDEwNi41MTEgMTU0Ljg2MUMxMDYuMzczIDE1NC42ODcgMTA2LjI2MiAxNTQuNDc2IDEwNi4xNzkgMTU0LjIyOUMxMDYuMDk4IDE1My45ODEgMTA2LjA0MiAxNTMuNzAyIDEwNi4wMTEgMTUzLjM5M1YxNTMuMDMzQzEwNi4wNDIgMTUyLjcyMSAxMDYuMDk4IDE1Mi40NDEgMTA2LjE3OSAxNTIuMTkzQzEwNi4yNjIgMTUxLjk0NiAxMDYuMzczIDE1MS43MzUgMTA2LjUxMSAxNTEuNTYxQzEwNi42NDkgMTUxLjM4MyAxMDYuODE4IDE1MS4yNDkgMTA3LjAxOSAxNTEuMTU4QzEwNy4yMTkgMTUxLjA2NCAxMDcuNDUyIDE1MS4wMTggMTA3LjcxOCAxNTEuMDE4QzEwNy45ODQgMTUxLjAxOCAxMDguMjE5IDE1MS4wNyAxMDguNDI1IDE1MS4xNzRDMTA4LjYzMSAxNTEuMjc1IDEwOC44MDMgMTUxLjQyMSAxMDguOTQxIDE1MS42MTFDMTA5LjA4MSAxNTEuODAxIDEwOS4xODggMTUyLjAyOSAxMDkuMjYxIDE1Mi4yOTVDMTA5LjMzNCAxNTIuNTU4IDEwOS4zNyAxNTIuODUxIDEwOS4zNyAxNTMuMTc0Wk0xMDguNjQ0IDE1My4yNTZWMTUzLjE3NEMxMDguNjQ0IDE1Mi45NjMgMTA4LjYyNCAxNTIuNzY1IDEwOC41ODUgMTUyLjU4QzEwOC41NDYgMTUyLjM5MyAxMDguNDg0IDE1Mi4yMjkgMTA4LjM5OCAxNTIuMDg4QzEwOC4zMTIgMTUxLjk0NSAxMDguMTk5IDE1MS44MzMgMTA4LjA1OCAxNTEuNzUyQzEwNy45MTcgMTUxLjY2OSAxMDcuNzQ0IDE1MS42MjcgMTA3LjUzOCAxNTEuNjI3QzEwNy4zNTYgMTUxLjYyNyAxMDcuMTk3IDE1MS42NTggMTA3LjA2MiAxNTEuNzIxQzEwNi45MjkgMTUxLjc4MyAxMDYuODE2IDE1MS44NjggMTA2LjcyMiAxNTEuOTc1QzEwNi42MjggMTUyLjA3OSAxMDYuNTUxIDE1Mi4xOTkgMTA2LjQ5MiAxNTIuMzM0QzEwNi40MzQgMTUyLjQ2NyAxMDYuMzkxIDE1Mi42MDUgMTA2LjM2MyAxNTIuNzQ4VjE1My42ODlDMTA2LjQwNCAxNTMuODcyIDEwNi40NzIgMTU0LjA0OCAxMDYuNTY2IDE1NC4yMTdDMTA2LjY2MiAxNTQuMzgzIDEwNi43OSAxNTQuNTIgMTA2Ljk0OSAxNTQuNjI3QzEwNy4xMSAxNTQuNzM0IDEwNy4zMDkgMTU0Ljc4NyAxMDcuNTQ2IDE1NC43ODdDMTA3Ljc0MiAxNTQuNzg3IDEwNy45MDggMTU0Ljc0OCAxMDguMDQ2IDE1NC42N0MxMDguMTg3IDE1NC41ODkgMTA4LjMgMTU0LjQ3OSAxMDguMzg2IDE1NC4zMzhDMTA4LjQ3NSAxNTQuMTk3IDEwOC41NCAxNTQuMDM1IDEwOC41ODEgMTUzLjg1QzEwOC42MjMgMTUzLjY2NSAxMDguNjQ0IDE1My40NjcgMTA4LjY0NCAxNTMuMjU2WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC41NCIvPgo8L2c+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwM180NDM3Xzk2ODU4KSI+CjxsaW5lIHgxPSIxMzIuNjUiIHkxPSIxNDcuMDcyIiB4Mj0iMTMyLjY1IiB5Mj0iMTQ2LjI1IiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC41IiBzdHJva2Utd2lkdGg9IjAuNSIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIvPgo8cGF0aCBkPSJNMTI0LjcwOSAxNTIuMDI1VjE1Mi44OTNDMTI0LjcwOSAxNTMuMzU5IDEyNC42NjggMTUzLjc1MiAxMjQuNTg0IDE1NC4wNzJDMTI0LjUwMSAxNTQuMzkzIDEyNC4zODEgMTU0LjY1IDEyNC4yMjUgMTU0Ljg0NkMxMjQuMDY5IDE1NS4wNDEgMTIzLjg4IDE1NS4xODMgMTIzLjY1OCAxNTUuMjcxQzEyMy40NCAxNTUuMzU3IDEyMy4xOTIgMTU1LjQgMTIyLjkxNiAxNTUuNEMxMjIuNjk4IDE1NS40IDEyMi40OTYgMTU1LjM3MyAxMjIuMzExIDE1NS4zMThDMTIyLjEyNiAxNTUuMjY0IDEyMS45NTkgMTU1LjE3NiAxMjEuODExIDE1NS4wNTdDMTIxLjY2NSAxNTQuOTM0IDEyMS41NCAxNTQuNzc1IDEyMS40MzYgMTU0LjU4QzEyMS4zMzIgMTU0LjM4NSAxMjEuMjUyIDE1NC4xNDggMTIxLjE5OCAxNTMuODY5QzEyMS4xNDMgMTUzLjU5IDEyMS4xMTYgMTUzLjI2NSAxMjEuMTE2IDE1Mi44OTNWMTUyLjAyNUMxMjEuMTE2IDE1MS41NTkgMTIxLjE1NyAxNTEuMTY5IDEyMS4yNDEgMTUwLjg1NEMxMjEuMzI2IDE1MC41MzggMTIxLjQ0OCAxNTAuMjg2IDEyMS42MDQgMTUwLjA5NkMxMjEuNzYgMTQ5LjkwMyAxMjEuOTQ4IDE0OS43NjUgMTIyLjE2NiAxNDkuNjgyQzEyMi4zODggMTQ5LjU5OCAxMjIuNjM1IDE0OS41NTcgMTIyLjkwOCAxNDkuNTU3QzEyMy4xMyAxNDkuNTU3IDEyMy4zMzMgMTQ5LjU4NCAxMjMuNTE4IDE0OS42MzlDMTIzLjcwNSAxNDkuNjkxIDEyMy44NzIgMTQ5Ljc3NSAxMjQuMDE4IDE0OS44OTNDMTI0LjE2NCAxNTAuMDA3IDEyNC4yODcgMTUwLjE2MSAxMjQuMzg5IDE1MC4zNTRDMTI0LjQ5MyAxNTAuNTQ0IDEyNC41NzMgMTUwLjc3NyAxMjQuNjI3IDE1MS4wNTNDMTI0LjY4MiAxNTEuMzI5IDEyNC43MDkgMTUxLjY1MyAxMjQuNzA5IDE1Mi4wMjVaTTEyMy45ODMgMTUzLjAxVjE1MS45MDRDMTIzLjk4MyAxNTEuNjQ5IDEyMy45NjcgMTUxLjQyNSAxMjMuOTM2IDE1MS4yMzJDMTIzLjkwNyAxNTEuMDM3IDEyMy44NjQgMTUwLjg3IDEyMy44MDcgMTUwLjczMkMxMjMuNzUgMTUwLjU5NCAxMjMuNjc3IDE1MC40ODIgMTIzLjU4OCAxNTAuMzk2QzEyMy41MDIgMTUwLjMxMSAxMjMuNDAyIDE1MC4yNDggMTIzLjI4NyAxNTAuMjA5QzEyMy4xNzUgMTUwLjE2NyAxMjMuMDQ5IDE1MC4xNDYgMTIyLjkwOCAxNTAuMTQ2QzEyMi43MzcgMTUwLjE0NiAxMjIuNTg0IDE1MC4xNzkgMTIyLjQ1MSAxNTAuMjQ0QzEyMi4zMTkgMTUwLjMwNyAxMjIuMjA3IDE1MC40MDcgMTIyLjExNiAxNTAuNTQ1QzEyMi4wMjcgMTUwLjY4MyAxMjEuOTU5IDE1MC44NjQgMTIxLjkxMiAxNTEuMDg4QzEyMS44NjYgMTUxLjMxMiAxMjEuODQyIDE1MS41ODQgMTIxLjg0MiAxNTEuOTA0VjE1My4wMUMxMjEuODQyIDE1My4yNjUgMTIxLjg1NiAxNTMuNDkgMTIxLjg4NSAxNTMuNjg2QzEyMS45MTYgMTUzLjg4MSAxMjEuOTYyIDE1NC4wNSAxMjIuMDIyIDE1NC4xOTNDMTIyLjA4MiAxNTQuMzM0IDEyMi4xNTUgMTU0LjQ1IDEyMi4yNDEgMTU0LjU0MUMxMjIuMzI2IDE1NC42MzIgMTIyLjQyNSAxNTQuNyAxMjIuNTM3IDE1NC43NDRDMTIyLjY1MiAxNTQuNzg2IDEyMi43NzggMTU0LjgwNyAxMjIuOTE2IDE1NC44MDdDMTIzLjA5MyAxNTQuODA3IDEyMy4yNDggMTU0Ljc3MyAxMjMuMzgxIDE1NC43MDVDMTIzLjUxNCAxNTQuNjM3IDEyMy42MjUgMTU0LjUzMiAxMjMuNzEzIDE1NC4zODlDMTIzLjgwNCAxNTQuMjQzIDEyMy44NzIgMTU0LjA1NyAxMjMuOTE2IDE1My44M0MxMjMuOTYxIDE1My42MDEgMTIzLjk4MyAxNTMuMzI3IDEyMy45ODMgMTUzLjAxWk0xMjkuNTY1IDE1My40MDhWMTU0LjAwMkgxMjUuNDU2VjE1My41NzZMMTI4LjAwMyAxNDkuNjM1SDEyOC41OTNMMTI3Ljk2IDE1MC43NzVMMTI2LjI3NiAxNTMuNDA4SDEyOS41NjVaTTEyOC43NzIgMTQ5LjYzNVYxNTUuMzIySDEyOC4wNVYxNDkuNjM1SDEyOC43NzJaTTEzMy4zMTMgMTQ5LjYzNVYxNTUuMzIySDEzMi41NTlWMTQ5LjYzNUgxMzMuMzEzWk0xMzUuNjk1IDE1Mi4xOTNWMTUyLjgxMUgxMzMuMTQ5VjE1Mi4xOTNIMTM1LjY5NVpNMTM2LjA4MiAxNDkuNjM1VjE1MC4yNTJIMTMzLjE0OVYxNDkuNjM1SDEzNi4wODJaTTEzOC42MjIgMTU1LjRDMTM4LjMyOCAxNTUuNCAxMzguMDYxIDE1NS4zNTEgMTM3LjgyMSAxNTUuMjUyQzEzNy41ODQgMTU1LjE1IDEzNy4zOCAxNTUuMDA4IDEzNy4yMDggMTU0LjgyNkMxMzcuMDM4IDE1NC42NDQgMTM2LjkwOCAxNTQuNDI4IDEzNi44MTcgMTU0LjE3OEMxMzYuNzI2IDE1My45MjggMTM2LjY4IDE1My42NTQgMTM2LjY4IDE1My4zNTdWMTUzLjE5M0MxMzYuNjggMTUyLjg1IDEzNi43MzEgMTUyLjU0NCAxMzYuODMzIDE1Mi4yNzVDMTM2LjkzNCAxNTIuMDA1IDEzNy4wNzIgMTUxLjc3NSAxMzcuMjQ3IDE1MS41ODhDMTM3LjQyMSAxNTEuNCAxMzcuNjE5IDE1MS4yNTggMTM3Ljg0MSAxNTEuMTYyQzEzOC4wNjIgMTUxLjA2NiAxMzguMjkxIDE1MS4wMTggMTM4LjUyOCAxNTEuMDE4QzEzOC44MyAxNTEuMDE4IDEzOS4wOTEgMTUxLjA3IDEzOS4zMDkgMTUxLjE3NEMxMzkuNTMxIDE1MS4yNzggMTM5LjcxMiAxNTEuNDI0IDEzOS44NTIgMTUxLjYxMUMxMzkuOTkzIDE1MS43OTYgMTQwLjA5NyAxNTIuMDE1IDE0MC4xNjUgMTUyLjI2OEMxNDAuMjMyIDE1Mi41MTggMTQwLjI2NiAxNTIuNzkxIDE0MC4yNjYgMTUzLjA4OFYxNTMuNDEySDEzNy4xMVYxNTIuODIySDEzOS41NDRWMTUyLjc2OEMxMzkuNTMzIDE1Mi41OCAxMzkuNDk0IDE1Mi4zOTggMTM5LjQyNiAxNTIuMjIxQzEzOS4zNjEgMTUyLjA0NCAxMzkuMjU3IDE1MS44OTggMTM5LjExNCAxNTEuNzgzQzEzOC45NzEgMTUxLjY2OSAxMzguNzc1IDE1MS42MTEgMTM4LjUyOCAxNTEuNjExQzEzOC4zNjQgMTUxLjYxMSAxMzguMjEzIDE1MS42NDYgMTM4LjA3NSAxNTEuNzE3QzEzNy45MzcgMTUxLjc4NSAxMzcuODE4IDE1MS44ODYgMTM3LjcxOSAxNTIuMDIxQzEzNy42MiAxNTIuMTU3IDEzNy41NDQgMTUyLjMyMiAxMzcuNDg5IDE1Mi41MThDMTM3LjQzNCAxNTIuNzEzIDEzNy40MDcgMTUyLjkzOCAxMzcuNDA3IDE1My4xOTNWMTUzLjM1N0MxMzcuNDA3IDE1My41NTggMTM3LjQzNCAxNTMuNzQ3IDEzNy40ODkgMTUzLjkyNEMxMzcuNTQ2IDE1NC4wOTggMTM3LjYyOCAxNTQuMjUyIDEzNy43MzUgMTU0LjM4NUMxMzcuODQ0IDE1NC41MTggMTM3Ljk3NiAxNTQuNjIyIDEzOC4xMyAxNTQuNjk3QzEzOC4yODYgMTU0Ljc3MyAxMzguNDYzIDE1NC44MTEgMTM4LjY2MSAxNTQuODExQzEzOC45MTYgMTU0LjgxMSAxMzkuMTMyIDE1NC43NTggMTM5LjMwOSAxNTQuNjU0QzEzOS40ODYgMTU0LjU1IDEzOS42NDEgMTU0LjQxMSAxMzkuNzc0IDE1NC4yMzZMMTQwLjIxMiAxNTQuNTg0QzE0MC4xMiAxNTQuNzIyIDE0MC4wMDUgMTU0Ljg1NCAxMzkuODY0IDE1NC45NzlDMTM5LjcyMyAxNTUuMTA0IDEzOS41NSAxNTUuMjA1IDEzOS4zNDQgMTU1LjI4M0MxMzkuMTQxIDE1NS4zNjEgMTM4LjkgMTU1LjQgMTM4LjYyMiAxNTUuNFpNMTQxLjE4OSAxNDkuMzIySDE0MS45MTVWMTU0LjUwMkwxNDEuODUzIDE1NS4zMjJIMTQxLjE4OVYxNDkuMzIyWk0xNDQuNzcxIDE1My4xNzRWMTUzLjI1NkMxNDQuNzcxIDE1My41NjMgMTQ0LjczNCAxNTMuODQ4IDE0NC42NjEgMTU0LjExMUMxNDQuNTg5IDE1NC4zNzIgMTQ0LjQ4MiAxNTQuNTk4IDE0NC4zNDEgMTU0Ljc5MUMxNDQuMjAxIDE1NC45ODQgMTQ0LjAyOSAxNTUuMTMzIDE0My44MjYgMTU1LjI0QzE0My42MjIgMTU1LjM0NyAxNDMuMzg5IDE1NS40IDE0My4xMjYgMTU1LjRDMTQyLjg1OCAxNTUuNCAxNDIuNjIyIDE1NS4zNTUgMTQyLjQxOSAxNTUuMjY0QzE0Mi4yMTkgMTU1LjE3IDE0Mi4wNDkgMTU1LjAzNiAxNDEuOTExIDE1NC44NjFDMTQxLjc3MyAxNTQuNjg3IDE0MS42NjMgMTU0LjQ3NiAxNDEuNTc5IDE1NC4yMjlDMTQxLjQ5OSAxNTMuOTgxIDE0MS40NDMgMTUzLjcwMiAxNDEuNDExIDE1My4zOTNWMTUzLjAzM0MxNDEuNDQzIDE1Mi43MjEgMTQxLjQ5OSAxNTIuNDQxIDE0MS41NzkgMTUyLjE5M0MxNDEuNjYzIDE1MS45NDYgMTQxLjc3MyAxNTEuNzM1IDE0MS45MTEgMTUxLjU2MUMxNDIuMDQ5IDE1MS4zODMgMTQyLjIxOSAxNTEuMjQ5IDE0Mi40MTkgMTUxLjE1OEMxNDIuNjIgMTUxLjA2NCAxNDIuODUzIDE1MS4wMTggMTQzLjExOCAxNTEuMDE4QzE0My4zODQgMTUxLjAxOCAxNDMuNjIgMTUxLjA3IDE0My44MjYgMTUxLjE3NEMxNDQuMDMxIDE1MS4yNzUgMTQ0LjIwMyAxNTEuNDIxIDE0NC4zNDEgMTUxLjYxMUMxNDQuNDgyIDE1MS44MDEgMTQ0LjU4OSAxNTIuMDI5IDE0NC42NjEgMTUyLjI5NUMxNDQuNzM0IDE1Mi41NTggMTQ0Ljc3MSAxNTIuODUxIDE0NC43NzEgMTUzLjE3NFpNMTQ0LjA0NCAxNTMuMjU2VjE1My4xNzRDMTQ0LjA0NCAxNTIuOTYzIDE0NC4wMjUgMTUyLjc2NSAxNDMuOTg2IDE1Mi41OEMxNDMuOTQ3IDE1Mi4zOTMgMTQzLjg4NCAxNTIuMjI5IDE0My43OTggMTUyLjA4OEMxNDMuNzEyIDE1MS45NDUgMTQzLjU5OSAxNTEuODMzIDE0My40NTggMTUxLjc1MkMxNDMuMzE4IDE1MS42NjkgMTQzLjE0NSAxNTEuNjI3IDE0Mi45MzkgMTUxLjYyN0MxNDIuNzU3IDE1MS42MjcgMTQyLjU5OCAxNTEuNjU4IDE0Mi40NjIgMTUxLjcyMUMxNDIuMzI5IDE1MS43ODMgMTQyLjIxNiAxNTEuODY4IDE0Mi4xMjIgMTUxLjk3NUMxNDIuMDI5IDE1Mi4wNzkgMTQxLjk1MiAxNTIuMTk5IDE0MS44OTIgMTUyLjMzNEMxNDEuODM1IDE1Mi40NjcgMTQxLjc5MiAxNTIuNjA1IDE0MS43NjMgMTUyLjc0OFYxNTMuNjg5QzE0MS44MDUgMTUzLjg3MiAxNDEuODcyIDE1NC4wNDggMTQxLjk2NiAxNTQuMjE3QzE0Mi4wNjIgMTU0LjM4MyAxNDIuMTkgMTU0LjUyIDE0Mi4zNDkgMTU0LjYyN0MxNDIuNTEgMTU0LjczNCAxNDIuNzEgMTU0Ljc4NyAxNDIuOTQ3IDE1NC43ODdDMTQzLjE0MiAxNTQuNzg3IDE0My4zMDkgMTU0Ljc0OCAxNDMuNDQ3IDE1NC42N0MxNDMuNTg3IDE1NC41ODkgMTQzLjcwMSAxNTQuNDc5IDE0My43ODYgMTU0LjMzOEMxNDMuODc1IDE1NC4xOTcgMTQzLjk0IDE1NC4wMzUgMTQzLjk4MiAxNTMuODVDMTQ0LjAyMyAxNTMuNjY1IDE0NC4wNDQgMTUzLjQ2NyAxNDQuMDQ0IDE1My4yNTZaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjU0Ii8+CjwvZz4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXA0XzQ0MzdfOTY4NTgpIj4KPGxpbmUgeDE9IjE2OC4wNSIgeTE9IjE0Ny4wNzIiIHgyPSIxNjguMDUiIHkyPSIxNDYuMjUiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS1vcGFjaXR5PSIwLjUiIHN0cm9rZS13aWR0aD0iMC41IiBzdHJva2UtbGluZWNhcD0ic3F1YXJlIi8+CjxwYXRoIGQ9Ik0xNjAuMTA5IDE1Mi4wMjVWMTUyLjg5M0MxNjAuMTA5IDE1My4zNTkgMTYwLjA2NyAxNTMuNzUyIDE1OS45ODQgMTU0LjA3MkMxNTkuOTAxIDE1NC4zOTMgMTU5Ljc4MSAxNTQuNjUgMTU5LjYyNSAxNTQuODQ2QzE1OS40NjkgMTU1LjA0MSAxNTkuMjggMTU1LjE4MyAxNTkuMDU4IDE1NS4yNzFDMTU4Ljg0IDE1NS4zNTcgMTU4LjU5MiAxNTUuNCAxNTguMzE2IDE1NS40QzE1OC4wOTcgMTU1LjQgMTU3Ljg5NiAxNTUuMzczIDE1Ny43MTEgMTU1LjMxOEMxNTcuNTI2IDE1NS4yNjQgMTU3LjM1OSAxNTUuMTc2IDE1Ny4yMTEgMTU1LjA1N0MxNTcuMDY1IDE1NC45MzQgMTU2Ljk0IDE1NC43NzUgMTU2LjgzNiAxNTQuNThDMTU2LjczMiAxNTQuMzg1IDE1Ni42NTIgMTU0LjE0OCAxNTYuNTk3IDE1My44NjlDMTU2LjU0MyAxNTMuNTkgMTU2LjUxNSAxNTMuMjY1IDE1Ni41MTUgMTUyLjg5M1YxNTIuMDI1QzE1Ni41MTUgMTUxLjU1OSAxNTYuNTU3IDE1MS4xNjkgMTU2LjY0IDE1MC44NTRDMTU2LjcyNiAxNTAuNTM4IDE1Ni44NDcgMTUwLjI4NiAxNTcuMDA0IDE1MC4wOTZDMTU3LjE2IDE0OS45MDMgMTU3LjM0NyAxNDkuNzY1IDE1Ny41NjYgMTQ5LjY4MkMxNTcuNzg4IDE0OS41OTggMTU4LjAzNSAxNDkuNTU3IDE1OC4zMDggMTQ5LjU1N0MxNTguNTMgMTQ5LjU1NyAxNTguNzMzIDE0OS41ODQgMTU4LjkxOCAxNDkuNjM5QzE1OS4xMDUgMTQ5LjY5MSAxNTkuMjcyIDE0OS43NzUgMTU5LjQxOCAxNDkuODkzQzE1OS41NjQgMTUwLjAwNyAxNTkuNjg3IDE1MC4xNjEgMTU5Ljc4OSAxNTAuMzU0QzE1OS44OTMgMTUwLjU0NCAxNTkuOTcyIDE1MC43NzcgMTYwLjAyNyAxNTEuMDUzQzE2MC4wODIgMTUxLjMyOSAxNjAuMTA5IDE1MS42NTMgMTYwLjEwOSAxNTIuMDI1Wk0xNTkuMzgzIDE1My4wMVYxNTEuOTA0QzE1OS4zODMgMTUxLjY0OSAxNTkuMzY3IDE1MS40MjUgMTU5LjMzNiAxNTEuMjMyQzE1OS4zMDcgMTUxLjAzNyAxNTkuMjY0IDE1MC44NyAxNTkuMjA3IDE1MC43MzJDMTU5LjE1IDE1MC41OTQgMTU5LjA3NyAxNTAuNDgyIDE1OC45ODggMTUwLjM5NkMxNTguOTAyIDE1MC4zMTEgMTU4LjgwMiAxNTAuMjQ4IDE1OC42ODcgMTUwLjIwOUMxNTguNTc1IDE1MC4xNjcgMTU4LjQ0OSAxNTAuMTQ2IDE1OC4zMDggMTUwLjE0NkMxNTguMTM2IDE1MC4xNDYgMTU3Ljk4NCAxNTAuMTc5IDE1Ny44NTEgMTUwLjI0NEMxNTcuNzE5IDE1MC4zMDcgMTU3LjYwNyAxNTAuNDA3IDE1Ny41MTUgMTUwLjU0NUMxNTcuNDI3IDE1MC42ODMgMTU3LjM1OSAxNTAuODY0IDE1Ny4zMTIgMTUxLjA4OEMxNTcuMjY1IDE1MS4zMTIgMTU3LjI0MiAxNTEuNTg0IDE1Ny4yNDIgMTUxLjkwNFYxNTMuMDFDMTU3LjI0MiAxNTMuMjY1IDE1Ny4yNTYgMTUzLjQ5IDE1Ny4yODUgMTUzLjY4NkMxNTcuMzE2IDE1My44ODEgMTU3LjM2MiAxNTQuMDUgMTU3LjQyMiAxNTQuMTkzQzE1Ny40ODIgMTU0LjMzNCAxNTcuNTU0IDE1NC40NSAxNTcuNjQgMTU0LjU0MUMxNTcuNzI2IDE1NC42MzIgMTU3LjgyNSAxNTQuNyAxNTcuOTM3IDE1NC43NDRDMTU4LjA1MiAxNTQuNzg2IDE1OC4xNzggMTU0LjgwNyAxNTguMzE2IDE1NC44MDdDMTU4LjQ5MyAxNTQuODA3IDE1OC42NDggMTU0Ljc3MyAxNTguNzgxIDE1NC43MDVDMTU4LjkxNCAxNTQuNjM3IDE1OS4wMjUgMTU0LjUzMiAxNTkuMTEzIDE1NC4zODlDMTU5LjIwNCAxNTQuMjQzIDE1OS4yNzIgMTU0LjA1NyAxNTkuMzE2IDE1My44M0MxNTkuMzYgMTUzLjYwMSAxNTkuMzgzIDE1My4zMjcgMTU5LjM4MyAxNTMuMDFaTTE2Mi4wMzYgMTUyLjYxNUwxNjEuNDU3IDE1Mi40NjdMMTYxLjc0MyAxNDkuNjM1SDE2NC42NjFWMTUwLjMwM0gxNjIuMzU2TDE2Mi4xODQgMTUxLjg1QzE2Mi4yODggMTUxLjc5IDE2Mi40MiAxNTEuNzM0IDE2Mi41NzkgMTUxLjY4MkMxNjIuNzQgMTUxLjYzIDE2Mi45MjUgMTUxLjYwNCAxNjMuMTMzIDE1MS42MDRDMTYzLjM5NiAxNTEuNjA0IDE2My42MzIgMTUxLjY0OSAxNjMuODQgMTUxLjc0QzE2NC4wNDkgMTUxLjgyOSAxNjQuMjI2IDE1MS45NTYgMTY0LjM3MSAxNTIuMTIzQzE2NC41MiAxNTIuMjkgMTY0LjYzMyAxNTIuNDkgMTY0LjcxMSAxNTIuNzI1QzE2NC43ODkgMTUyLjk1OSAxNjQuODI5IDE1My4yMjEgMTY0LjgyOSAxNTMuNTFDMTY0LjgyOSAxNTMuNzgzIDE2NC43OTEgMTU0LjAzNSAxNjQuNzE1IDE1NC4yNjRDMTY0LjY0MiAxNTQuNDkzIDE2NC41MzIgMTU0LjY5MyAxNjQuMzgzIDE1NC44NjVDMTY0LjIzNSAxNTUuMDM1IDE2NC4wNDcgMTU1LjE2NiAxNjMuODIxIDE1NS4yNkMxNjMuNTk3IDE1NS4zNTQgMTYzLjMzMiAxNTUuNCAxNjMuMDI4IDE1NS40QzE2Mi43OTkgMTU1LjQgMTYyLjU4MSAxNTUuMzY5IDE2Mi4zNzUgMTU1LjMwN0MxNjIuMTcyIDE1NS4yNDIgMTYxLjk5IDE1NS4xNDQgMTYxLjgyOSAxNTUuMDE0QzE2MS42NyAxNTQuODgxIDE2MS41MzkgMTU0LjcxNyAxNjEuNDM4IDE1NC41MjFDMTYxLjMzOSAxNTQuMzI0IDE2MS4yNzYgMTU0LjA5MiAxNjEuMjUgMTUzLjgyNkgxNjEuOTM4QzE2MS45NjkgMTU0LjA0IDE2Mi4wMzIgMTU0LjIxOSAxNjIuMTI1IDE1NC4zNjVDMTYyLjIxOSAxNTQuNTExIDE2Mi4zNDIgMTU0LjYyMiAxNjIuNDkzIDE1NC42OTdDMTYyLjY0NiAxNTQuNzcgMTYyLjgyNSAxNTQuODA3IDE2My4wMjggMTU0LjgwN0MxNjMuMiAxNTQuODA3IDE2My4zNTIgMTU0Ljc3NyAxNjMuNDg1IDE1NC43MTdDMTYzLjYxOCAxNTQuNjU3IDE2My43MyAxNTQuNTcxIDE2My44MjEgMTU0LjQ1OUMxNjMuOTEyIDE1NC4zNDcgMTYzLjk4MSAxNTQuMjEyIDE2NC4wMjggMTU0LjA1M0MxNjQuMDc3IDE1My44OTQgMTY0LjEwMiAxNTMuNzE1IDE2NC4xMDIgMTUzLjUxOEMxNjQuMTAyIDE1My4zMzggMTY0LjA3NyAxNTMuMTcxIDE2NC4wMjggMTUzLjAxOEMxNjMuOTc4IDE1Mi44NjQgMTYzLjkwNCAxNTIuNzMgMTYzLjgwNSAxNTIuNjE1QzE2My43MDkgMTUyLjUwMSAxNjMuNTkgMTUyLjQxMiAxNjMuNDUgMTUyLjM1QzE2My4zMDkgMTUyLjI4NSAxNjMuMTQ4IDE1Mi4yNTIgMTYyLjk2NSAxNTIuMjUyQzE2Mi43MjMgMTUyLjI1MiAxNjIuNTM5IDE1Mi4yODUgMTYyLjQxNCAxNTIuMzVDMTYyLjI5MiAxNTIuNDE1IDE2Mi4xNjYgMTUyLjUwMyAxNjIuMDM2IDE1Mi42MTVaTTE2OC43MTMgMTQ5LjYzNVYxNTUuMzIySDE2Ny45NTlWMTQ5LjYzNUgxNjguNzEzWk0xNzEuMDk1IDE1Mi4xOTNWMTUyLjgxMUgxNjguNTQ4VjE1Mi4xOTNIMTcxLjA5NVpNMTcxLjQ4MiAxNDkuNjM1VjE1MC4yNTJIMTY4LjU0OFYxNDkuNjM1SDE3MS40ODJaTTE3NC4wMjIgMTU1LjRDMTczLjcyNyAxNTUuNCAxNzMuNDYgMTU1LjM1MSAxNzMuMjIxIDE1NS4yNTJDMTcyLjk4NCAxNTUuMTUgMTcyLjc4IDE1NS4wMDggMTcyLjYwOCAxNTQuODI2QzE3Mi40MzggMTU0LjY0NCAxNzIuMzA4IDE1NC40MjggMTcyLjIxNyAxNTQuMTc4QzE3Mi4xMjYgMTUzLjkyOCAxNzIuMDggMTUzLjY1NCAxNzIuMDggMTUzLjM1N1YxNTMuMTkzQzE3Mi4wOCAxNTIuODUgMTcyLjEzMSAxNTIuNTQ0IDE3Mi4yMzMgMTUyLjI3NUMxNzIuMzM0IDE1Mi4wMDUgMTcyLjQ3MiAxNTEuNzc1IDE3Mi42NDcgMTUxLjU4OEMxNzIuODIxIDE1MS40IDE3My4wMTkgMTUxLjI1OCAxNzMuMjQgMTUxLjE2MkMxNzMuNDYyIDE1MS4wNjYgMTczLjY5MSAxNTEuMDE4IDE3My45MjggMTUxLjAxOEMxNzQuMjMgMTUxLjAxOCAxNzQuNDkgMTUxLjA3IDE3NC43MDkgMTUxLjE3NEMxNzQuOTMxIDE1MS4yNzggMTc1LjExMiAxNTEuNDI0IDE3NS4yNTIgMTUxLjYxMUMxNzUuMzkzIDE1MS43OTYgMTc1LjQ5NyAxNTIuMDE1IDE3NS41NjUgMTUyLjI2OEMxNzUuNjMyIDE1Mi41MTggMTc1LjY2NiAxNTIuNzkxIDE3NS42NjYgMTUzLjA4OFYxNTMuNDEySDE3Mi41MVYxNTIuODIySDE3NC45NDRWMTUyLjc2OEMxNzQuOTMzIDE1Mi41OCAxNzQuODk0IDE1Mi4zOTggMTc0LjgyNiAxNTIuMjIxQzE3NC43NjEgMTUyLjA0NCAxNzQuNjU3IDE1MS44OTggMTc0LjUxNCAxNTEuNzgzQzE3NC4zNzEgMTUxLjY2OSAxNzQuMTc1IDE1MS42MTEgMTczLjkyOCAxNTEuNjExQzE3My43NjQgMTUxLjYxMSAxNzMuNjEzIDE1MS42NDYgMTczLjQ3NSAxNTEuNzE3QzE3My4zMzcgMTUxLjc4NSAxNzMuMjE4IDE1MS44ODYgMTczLjExOSAxNTIuMDIxQzE3My4wMiAxNTIuMTU3IDE3Mi45NDQgMTUyLjMyMiAxNzIuODg5IDE1Mi41MThDMTcyLjgzNCAxNTIuNzEzIDE3Mi44MDcgMTUyLjkzOCAxNzIuODA3IDE1My4xOTNWMTUzLjM1N0MxNzIuODA3IDE1My41NTggMTcyLjgzNCAxNTMuNzQ3IDE3Mi44ODkgMTUzLjkyNEMxNzIuOTQ2IDE1NC4wOTggMTczLjAyOCAxNTQuMjUyIDE3My4xMzUgMTU0LjM4NUMxNzMuMjQ0IDE1NC41MTggMTczLjM3NiAxNTQuNjIyIDE3My41MyAxNTQuNjk3QzE3My42ODYgMTU0Ljc3MyAxNzMuODYzIDE1NC44MTEgMTc0LjA2MSAxNTQuODExQzE3NC4zMTYgMTU0LjgxMSAxNzQuNTMyIDE1NC43NTggMTc0LjcwOSAxNTQuNjU0QzE3NC44ODYgMTU0LjU1IDE3NS4wNDEgMTU0LjQxMSAxNzUuMTc0IDE1NC4yMzZMMTc1LjYxMiAxNTQuNTg0QzE3NS41MiAxNTQuNzIyIDE3NS40MDUgMTU0Ljg1NCAxNzUuMjY0IDE1NC45NzlDMTc1LjEyMyAxNTUuMTA0IDE3NC45NSAxNTUuMjA1IDE3NC43NDQgMTU1LjI4M0MxNzQuNTQxIDE1NS4zNjEgMTc0LjMgMTU1LjQgMTc0LjAyMiAxNTUuNFpNMTc2LjU4OSAxNDkuMzIySDE3Ny4zMTVWMTU0LjUwMkwxNzcuMjUzIDE1NS4zMjJIMTc2LjU4OVYxNDkuMzIyWk0xODAuMTcxIDE1My4xNzRWMTUzLjI1NkMxODAuMTcxIDE1My41NjMgMTgwLjEzNCAxNTMuODQ4IDE4MC4wNjEgMTU0LjExMUMxNzkuOTg4IDE1NC4zNzIgMTc5Ljg4MiAxNTQuNTk4IDE3OS43NDEgMTU0Ljc5MUMxNzkuNiAxNTQuOTg0IDE3OS40MjkgMTU1LjEzMyAxNzkuMjI1IDE1NS4yNEMxNzkuMDIyIDE1NS4zNDcgMTc4Ljc4OSAxNTUuNCAxNzguNTI2IDE1NS40QzE3OC4yNTggMTU1LjQgMTc4LjAyMiAxNTUuMzU1IDE3Ny44MTkgMTU1LjI2NEMxNzcuNjE5IDE1NS4xNyAxNzcuNDQ5IDE1NS4wMzYgMTc3LjMxMSAxNTQuODYxQzE3Ny4xNzMgMTU0LjY4NyAxNzcuMDYzIDE1NC40NzYgMTc2Ljk3OSAxNTQuMjI5QzE3Ni44OTkgMTUzLjk4MSAxNzYuODQzIDE1My43MDIgMTc2LjgxMSAxNTMuMzkzVjE1My4wMzNDMTc2Ljg0MyAxNTIuNzIxIDE3Ni44OTkgMTUyLjQ0MSAxNzYuOTc5IDE1Mi4xOTNDMTc3LjA2MyAxNTEuOTQ2IDE3Ny4xNzMgMTUxLjczNSAxNzcuMzExIDE1MS41NjFDMTc3LjQ0OSAxNTEuMzgzIDE3Ny42MTkgMTUxLjI0OSAxNzcuODE5IDE1MS4xNThDMTc4LjAyIDE1MS4wNjQgMTc4LjI1MyAxNTEuMDE4IDE3OC41MTggMTUxLjAxOEMxNzguNzg0IDE1MS4wMTggMTc5LjAyIDE1MS4wNyAxNzkuMjI1IDE1MS4xNzRDMTc5LjQzMSAxNTEuMjc1IDE3OS42MDMgMTUxLjQyMSAxNzkuNzQxIDE1MS42MTFDMTc5Ljg4MiAxNTEuODAxIDE3OS45ODggMTUyLjAyOSAxODAuMDYxIDE1Mi4yOTVDMTgwLjEzNCAxNTIuNTU4IDE4MC4xNzEgMTUyLjg1MSAxODAuMTcxIDE1My4xNzRaTTE3OS40NDQgMTUzLjI1NlYxNTMuMTc0QzE3OS40NDQgMTUyLjk2MyAxNzkuNDI1IDE1Mi43NjUgMTc5LjM4NiAxNTIuNThDMTc5LjM0NyAxNTIuMzkzIDE3OS4yODQgMTUyLjIyOSAxNzkuMTk4IDE1Mi4wODhDMTc5LjExMiAxNTEuOTQ1IDE3OC45OTkgMTUxLjgzMyAxNzguODU4IDE1MS43NTJDMTc4LjcxOCAxNTEuNjY5IDE3OC41NDQgMTUxLjYyNyAxNzguMzM5IDE1MS42MjdDMTc4LjE1NiAxNTEuNjI3IDE3Ny45OTggMTUxLjY1OCAxNzcuODYyIDE1MS43MjFDMTc3LjcyOSAxNTEuNzgzIDE3Ny42MTYgMTUxLjg2OCAxNzcuNTIyIDE1MS45NzVDMTc3LjQyOSAxNTIuMDc5IDE3Ny4zNTIgMTUyLjE5OSAxNzcuMjkyIDE1Mi4zMzRDMTc3LjIzNSAxNTIuNDY3IDE3Ny4xOTIgMTUyLjYwNSAxNzcuMTYzIDE1Mi43NDhWMTUzLjY4OUMxNzcuMjA1IDE1My44NzIgMTc3LjI3MiAxNTQuMDQ4IDE3Ny4zNjYgMTU0LjIxN0MxNzcuNDYyIDE1NC4zODMgMTc3LjU5IDE1NC41MiAxNzcuNzQ5IDE1NC42MjdDMTc3LjkxIDE1NC43MzQgMTc4LjExIDE1NC43ODcgMTc4LjM0NyAxNTQuNzg3QzE3OC41NDIgMTU0Ljc4NyAxNzguNzA4IDE1NC43NDggMTc4Ljg0NyAxNTQuNjdDMTc4Ljk4NyAxNTQuNTg5IDE3OS4xIDE1NC40NzkgMTc5LjE4NiAxNTQuMzM4QzE3OS4yNzUgMTU0LjE5NyAxNzkuMzQgMTU0LjAzNSAxNzkuMzgyIDE1My44NUMxNzkuNDIzIDE1My42NjUgMTc5LjQ0NCAxNTMuNDY3IDE3OS40NDQgMTUzLjI1NloiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNTQiLz4KPC9nPgo8cGF0aCBkPSJNMTQuNSAxNy41SDI3SDYwLjVDNjEuMDUyMyAxNy41IDYxLjUgMTcuOTQ3NyA2MS41IDE4LjVWMjZDNjEuNSAyNi41NTIzIDYxLjk0NzcgMjcgNjIuNSAyN0g5NkM5Ni41NTIzIDI3IDk3IDI2LjU1MjMgOTcgMjZWMTIuNUM5NyAxMS45NDc3IDk3LjQ0NzcgMTEuNSA5OCAxMS41SDEzMkMxMzIuNTUyIDExLjUgMTMzIDExLjk0NzcgMTMzIDEyLjVWMzIuNUMxMzMgMzMuMDUyMyAxMzMuNDQ4IDMzLjUgMTM0IDMzLjVIMTY3LjVDMTY4LjA1MiAzMy41IDE2OC41IDMzLjA1MjMgMTY4LjUgMzIuNVYyNEMxNjguNSAyMy40NDc3IDE2OC45NDggMjMgMTY5LjUgMjNIMTg2IiBzdHJva2U9IiNGRkMxMDciIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CjxwYXRoIGQ9Ik0xMSAxMTdIMjdWMTI3LjVINjJWNzkuMTYxMUg5N1YxMDFIMTMyVjg5LjI2NzVIMTY4LjVWMTIxLjVIMTg2IiBzdHJva2U9IiM0Q0FGNTAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CjxwYXRoIGQ9Ik0xMiA2MC41SDI4VjY5LjVINjIuNVY0N0w5NyA0Ny4wMDAyVjU3LjAwMDJMMTMzIDU3VjY5LjVIMTY4SDE4NS41IiBzdHJva2U9IiMyMTk2RjMiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNDQzN185Njg1OCI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTYwIiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8Y2xpcFBhdGggaWQ9ImNsaXAxXzQ0MzdfOTY4NTgiPgo8cmVjdCB3aWR0aD0iMzUuNCIgaGVpZ2h0PSIxMC4zMjIiIGZpbGw9IndoaXRlIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg0NC4zOTk5IDE0NikiLz4KPC9jbGlwUGF0aD4KPGNsaXBQYXRoIGlkPSJjbGlwMl80NDM3Xzk2ODU4Ij4KPHJlY3Qgd2lkdGg9IjM1LjQiIGhlaWdodD0iMTAuMzIyIiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNzkuNzk5OCAxNDYpIi8+CjwvY2xpcFBhdGg+CjxjbGlwUGF0aCBpZD0iY2xpcDNfNDQzN185Njg1OCI+CjxyZWN0IHdpZHRoPSIzNS40IiBoZWlnaHQ9IjEwLjMyMiIgZmlsbD0id2hpdGUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDExNS4yIDE0NikiLz4KPC9jbGlwUGF0aD4KPGNsaXBQYXRoIGlkPSJjbGlwNF80NDM3Xzk2ODU4Ij4KPHJlY3Qgd2lkdGg9IjM1LjQiIGhlaWdodD0iMTAuMzIyIiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTUwLjYgMTQ2KSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=", "description": "Displays changes to the state of the entity over time. For example, online and offline.", "descriptor": { "type": "timeseries", "sizeX": 8, "sizeY": 5, "resources": [], - "templateHtml": "\n", - "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.flotWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.flotWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.flotWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.flotWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.flotWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n hasAdditionalLatestDataKeys: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n\n", + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.timeSeriesChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n chartType: 'state',\n previewWidth: '80%',\n embedTitlePanel: true,\n hasAdditionalLatestDataKeys: true,\n dataKeySettingsFunction: TbTimeSeriesChart.dataKeySettings('state'),\n defaultDataKeysFunction: function() {\n return [{ name: 'state', label: 'State', type: 'timeseries', units: '', decimals: 0 }];\n }\n };\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", - "settingsDirective": "tb-flot-line-widget-settings", - "dataKeySettingsDirective": "tb-flot-line-key-settings", - "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", + "latestDataKeySettingsSchema": "{}", + "settingsDirective": "tb-time-series-chart-widget-settings", + "dataKeySettingsDirective": "tb-time-series-chart-key-settings", + "latestDataKeySettingsDirective": "", "hasBasicMode": true, - "basicModeDirective": "tb-flot-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":0,\"max\":1.2,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"timeForComparison\":\"previousInterval\",\"comparisonCustomIntervalValue\":7200000,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false,\"dataKeysListForLabels\":[]},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"waterfall_chart\",\"iconColor\":\"#1F6BDD\"}" + "basicModeDirective": "tb-time-series-chart-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"yAxisId\":\"default\",\"showInLegend\":true,\"dataHiddenByDefault\":false,\"type\":\"line\",\"lineSettings\":{\"showLine\":true,\"step\":true,\"stepType\":\"end\",\"smooth\":false,\"lineType\":\"solid\",\"lineWidth\":2,\"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)\",\"enablePointLabelBackground\":false,\"pointLabelBackground\":\"rgba(255,255,255,0.56)\",\"pointShape\":\"circle\",\"pointSize\":12,\"fillAreaSettings\":{\"type\":\"opacity\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"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)\",\"enableLabelBackground\":false,\"labelBackground\":\"rgba(255,255,255,0.56)\",\"backgroundSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"tooltipValueFormatter\":\"\"},\"_hash\":0.676226248393859,\"funcBody\":\"return Math.random() > 0.5 ? true : false;\",\"decimals\":0,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null,\"units\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#FFC107\",\"settings\":{\"yAxisId\":\"default\",\"showInLegend\":true,\"dataHiddenByDefault\":false,\"type\":\"line\",\"lineSettings\":{\"showLine\":true,\"step\":true,\"stepType\":\"end\",\"smooth\":false,\"lineType\":\"solid\",\"lineWidth\":2,\"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)\",\"enablePointLabelBackground\":false,\"pointLabelBackground\":\"rgba(255,255,255,0.56)\",\"pointShape\":\"circle\",\"pointSize\":12,\"fillAreaSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"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)\",\"enableLabelBackground\":false,\"labelBackground\":\"rgba(255,255,255,0.56)\",\"backgroundSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"tooltipValueFormatter\":null},\"_hash\":0.1106990458957191,\"funcBody\":\"return Math.random() <= 0.5 ? true : false;\",\"decimals\":0,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null,\"units\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":null}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":0,\"realtime\":{\"realtimeType\":0,\"timewindowMs\":60000,\"quickInterval\":\"CURRENT_DAY\",\"interval\":1000},\"aggregation\":{\"type\":\"NONE\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"yAxes\":{\"default\":{\"units\":null,\"decimals\":0,\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormatter\":\"\",\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\",\"id\":\"default\",\"order\":0,\"interval\":null,\"splitNumber\":null,\"min\":null,\"max\":null,\"ticksGenerator\":\"\"}},\"thresholds\":[],\"dataZoom\":true,\"stack\":false,\"xAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"bottom\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":10,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormat\":{},\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"noAggregationBarWidthSettings\":{\"strategy\":\"group\",\"groupWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000},\"barWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000}},\"showLegend\":true,\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"showTooltip\":true,\"tooltipTrigger\":\"axis\",\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipValueFormatter\":\"\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":false,\"custom\":false,\"auto\":true,\"autoDateFormatSettings\":{\"millisecond\":\"MMM dd yyyy HH:mm:ss\"}},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipDateInterval\":true,\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"animation\":{\"animation\":true,\"animationThreshold\":2000,\"animationDuration\":500,\"animationEasing\":\"cubicOut\",\"animationDelay\":0,\"animationDurationUpdate\":300,\"animationEasingUpdate\":\"cubicOut\",\"animationDelayUpdate\":0},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"12px\",\"states\":[{\"label\":\"Off\",\"value\":0,\"sourceType\":\"constant\",\"sourceValue\":false},{\"label\":\"On\",\"value\":1,\"sourceType\":\"constant\",\"sourceValue\":true}]},\"title\":\"State chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"0px\"}" }, - "externalId": null, - "tags": null + "tags": [ + "chart", + "time series", + "time-series", + "state", + "state chart", + "online", + "offline" + ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/state_chart_deprecated.json b/application/src/main/data/json/system/widget_types/state_chart_deprecated.json new file mode 100644 index 0000000000..90fc886d62 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/state_chart_deprecated.json @@ -0,0 +1,25 @@ +{ + "fqn": "charts.state_chart", + "name": "State Chart", + "deprecated": true, + "image": "tb-image:c3RhdGVfY2hhcnRfc3lzdGVtX3dpZGdldF9pbWFnZS5wbmc=:IlN0YXRlIENoYXJ0IiBzeXN0ZW0gd2lkZ2V0IGltYWdl;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAB9VBMVEUAAAAhlvMilvMymeE+nNVDoetInslNq/VZqOdpuPd3d3d5p5Z5suB6enp8fHyBgYGDg4ODqoqEhISIiIiKioqKuN2MjIyNjY2Ojo6QkJCRkZGSkpKUlJSVlZWWlpaXl5eYmJiZmZmampqcnJydnZ2enp6goKCgr3KhoaGioqKisXSjo6OjvsmkpKSlpaWlwdempqanp6eoqKiosGOo1vqpqampsGOqqqqqsWOrq6usrKysw9Wurq6wsLCysrK0tLS1tbW1wbK2tra3t7e4wau5ubm6urq7u7u8vLy9vb2+vr6/xbTAwMDAxbjCwsLC3ejDw8PExMTFxcXGxsbHx8fIv3jIyMjJycnKysrK5vzMzc7Nzc3Ozs7Pz8/QuDnRzcHS0tLT09PT39HU1NTU6/3VzLLV1dXV3sjW1tbX19fYuTHY2NjZ2dna0Ira2trb29vc3Nzex4De3t7f39/guyvhvCfhvCvh4eHh6Nbi4uLjvCTj4+PkyXbk5OTlvCTm5ubm693o6Ojpxl7q6urr6+vswTTs7Ozt7e3uvhju7u7vvhjv7+/wvxjw8PDx8fHyvxX0yTv09PT19fX29vb39/f4wyL4+Pj5+fn6+vr75J37+/v8whP8/Pz9/f39/v/+/v7/wQf/xRb/3HT/5JH/9tz/++/////APs7XAAAAAWJLR0Smt7AblQAAA1RJREFUeNrt3dlT01AUBvAEd8UNl2oLrdrFolZArUul1hWlVhQXFAUF1xaxIqi4FUQUrDsUClZi4nb+Th96S9NQkjDOOKZ+3wuZw8md++M25OXMlKOccJQnTUYocdRqdw8UAqTfOjG4lvr7uowOqbtAtG6o5OiGFoNDapuJHO8tFK4xOKS9msTVgoUiaQjPclnSl01snZ/y4ly2yBNZbRarbc+3yvdpt/hLawOPJp9ur7O0mRiEmzFkY1M6N/4I8qJpulzR2sBx1sgRjQuyA2pk+STdylw2VqR/FPFfWdcCvjhduiM7kVF2db1Nuspu7JGes+I76SRba7f0Wfnn/6F6It+mfo7m8TvYvh7KTiT3PQIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggPwXkFa7e7AQIP3WiUFzdl7LuBDFvJZxIYp5LeNCFPNaBn7Yc+e1KlheSYcrFCniL7LZqPl8cbp0TjavNcqu9rZJB9gdPdIJVnwjbWG1ndI95UzWM9V5rS9Ti3P4VWy1+5jXAgQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAMK+FeS3Ma2FeC/NamNfCCxEQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBA/hZkTKSkwSGn7VUJOmOLV241NiRSRZ2uYcvt+Io+Y0N83USm+MqGWEnU2JCqXqI1ZE+QhYiIy36tXR5INpOQbB5kftcmK85mtdey2iJekeWqX6/3kc+TLCQTrq6eEuZCgKTsbnNXBsIZOERJMedNQnry73XpW8UAKVhIyBPScVfQE9RqEaP7iMT6g+pdw7vKbxKl3IJqV8K7OUqXPHvG9UMGykk2lz1d4g5yvNXo6QqZiA41aHT5OwQTUWBJSrWrvlMwj1jFU2f1Q8K1dCysCUmVhdenNLvMRKZt3hGNriEnxfxlGqt9aPYRkb9bP6Q1SMFrmltMukKuMT2QpckWjc/WhP2lYB/TgjzdHyCKVM/gGXnsp+qY5hbba+hIVA/ETL0+9SepsoNiTs/igGpXZIhKqVv9QVJARIfXIWqfiM1nS+qBnHdae1V7Qss8ngEijRO5a6sMCAtdqv+HfgPwpNPbU6ipOwAAAABJRU5ErkJggg==", + "description": "Displays changes to the state of the entity over time. For example, online and offline.", + "descriptor": { + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "resources": [], + "templateHtml": "\n", + "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.flotWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.flotWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.flotWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.flotWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.flotWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n hasAdditionalLatestDataKeys: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}", + "settingsDirective": "tb-flot-line-widget-settings", + "dataKeySettingsDirective": "tb-flot-line-key-settings", + "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-flot-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":0,\"max\":1.2,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"timeForComparison\":\"previousInterval\",\"comparisonCustomIntervalValue\":7200000,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false,\"dataKeysListForLabels\":[]},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"waterfall_chart\",\"iconColor\":\"#1F6BDD\"}" + }, + "tags": null +} \ No newline at end of file diff --git a/application/src/main/data/upgrade/3.6.3/schema_update.sql b/application/src/main/data/upgrade/3.6.3/schema_update.sql index e2d454353e..3e0fdc3b4b 100644 --- a/application/src/main/data/upgrade/3.6.3/schema_update.sql +++ b/application/src/main/data/upgrade/3.6.3/schema_update.sql @@ -14,95 +14,14 @@ -- limitations under the License. -- +-- NOTIFICATIONS UPDATE START --- create new attribute_kv table schema -DO -$$ - BEGIN - -- in case of running the upgrade script a second time: - IF EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'attribute_kv' and column_name='entity_type') THEN - DROP VIEW IF EXISTS device_info_view; - DROP VIEW IF EXISTS device_info_active_attribute_view; - ALTER INDEX IF EXISTS idx_attribute_kv_by_key_and_last_update_ts RENAME TO idx_attribute_kv_by_key_and_last_update_ts_old; - IF EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'attribute_kv_pkey') THEN - ALTER TABLE attribute_kv RENAME CONSTRAINT attribute_kv_pkey TO attribute_kv_pkey_old; - END IF; - ALTER TABLE attribute_kv RENAME TO attribute_kv_old; - CREATE TABLE IF NOT EXISTS attribute_kv - ( - entity_id uuid, - attribute_type int, - attribute_key int, - bool_v boolean, - str_v varchar(10000000), - long_v bigint, - dbl_v double precision, - json_v json, - last_update_ts bigint, - CONSTRAINT attribute_kv_pkey PRIMARY KEY (entity_id, attribute_type, attribute_key) - ); - END IF; - END; -$$; +ALTER TABLE notification ADD COLUMN IF NOT EXISTS delivery_method VARCHAR(50) NOT NULL default 'WEB'; --- rename ts_kv_dictionary table to key_dictionary or create table if not exists -DO -$$ - BEGIN - IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'ts_kv_dictionary') THEN - ALTER TABLE ts_kv_dictionary RENAME CONSTRAINT ts_key_id_pkey TO key_dictionary_id_pkey; - ALTER TABLE ts_kv_dictionary RENAME TO key_dictionary; - ELSE CREATE TABLE IF NOT EXISTS key_dictionary( - key varchar(255) NOT NULL, - key_id serial UNIQUE, - CONSTRAINT key_dictionary_id_pkey PRIMARY KEY (key) - ); - END IF; - END; -$$; +DROP INDEX IF EXISTS idx_notification_recipient_id_created_time; +DROP INDEX IF EXISTS idx_notification_recipient_id_unread; --- insert keys into key_dictionary -DO -$$ - BEGIN - IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'attribute_kv_old') THEN - INSERT INTO key_dictionary(key) SELECT DISTINCT attribute_key FROM attribute_kv_old ON CONFLICT DO NOTHING; - END IF; - END; -$$; +CREATE INDEX IF NOT EXISTS idx_notification_delivery_method_recipient_id_created_time ON notification(delivery_method, recipient_id, created_time DESC); +CREATE INDEX IF NOT EXISTS idx_notification_delivery_method_recipient_id_unread ON notification(delivery_method, recipient_id) WHERE status <> 'READ'; --- migrate attributes from attribute_kv_old to attribute_kv -DO -$$ -DECLARE - row_num_old integer; - row_num integer; -BEGIN - IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'attribute_kv_old') THEN - INSERT INTO attribute_kv(entity_id, attribute_type, attribute_key, bool_v, str_v, long_v, dbl_v, json_v, last_update_ts) - SELECT a.entity_id, CASE - WHEN a.attribute_type = 'CLIENT_SCOPE' THEN 1 - WHEN a.attribute_type = 'SERVER_SCOPE' THEN 2 - WHEN a.attribute_type = 'SHARED_SCOPE' THEN 3 - ELSE 0 - END, - k.key_id, a.bool_v, a.str_v, a.long_v, a.dbl_v, a.json_v, a.last_update_ts - FROM attribute_kv_old a INNER JOIN key_dictionary k ON (a.attribute_key = k.key); - SELECT COUNT(*) INTO row_num_old FROM attribute_kv_old; - SELECT COUNT(*) INTO row_num FROM attribute_kv; - RAISE NOTICE 'Migrated % of % rows', row_num, row_num_old; - - IF row_num != 0 THEN - DROP TABLE IF EXISTS attribute_kv_old; - ELSE - RAISE EXCEPTION 'Table attribute_kv is empty'; - END IF; - - CREATE INDEX IF NOT EXISTS idx_attribute_kv_by_key_and_last_update_ts ON attribute_kv(entity_id, attribute_key, last_update_ts desc); - END IF; -EXCEPTION - WHEN others THEN - ROLLBACK; - RAISE EXCEPTION 'Error during COPY: %', SQLERRM; -END -$$; \ No newline at end of file +-- NOTIFICATIONS UPDATE END diff --git a/application/src/main/data/upgrade/3.6.4/schema_update.sql b/application/src/main/data/upgrade/3.6.4/schema_update.sql new file mode 100644 index 0000000000..d7d8887d13 --- /dev/null +++ b/application/src/main/data/upgrade/3.6.4/schema_update.sql @@ -0,0 +1,136 @@ +-- +-- Copyright © 2016-2024 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. +-- + +-- create new attribute_kv table schema +DO +$$ + BEGIN + -- in case of running the upgrade script a second time: + IF EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'attribute_kv' and column_name='entity_type') THEN + DROP VIEW IF EXISTS device_info_view; + DROP VIEW IF EXISTS device_info_active_attribute_view; + ALTER INDEX IF EXISTS idx_attribute_kv_by_key_and_last_update_ts RENAME TO idx_attribute_kv_by_key_and_last_update_ts_old; + IF EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'attribute_kv_pkey') THEN + ALTER TABLE attribute_kv RENAME CONSTRAINT attribute_kv_pkey TO attribute_kv_pkey_old; + END IF; + ALTER TABLE attribute_kv RENAME TO attribute_kv_old; + CREATE TABLE IF NOT EXISTS attribute_kv + ( + entity_id uuid, + attribute_type int, + attribute_key int, + bool_v boolean, + str_v varchar(10000000), + long_v bigint, + dbl_v double precision, + json_v json, + last_update_ts bigint, + CONSTRAINT attribute_kv_pkey PRIMARY KEY (entity_id, attribute_type, attribute_key) + ); + END IF; + END; +$$; + +-- rename ts_kv_dictionary table to key_dictionary or create table if not exists +DO +$$ + BEGIN + IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'ts_kv_dictionary') THEN + ALTER TABLE ts_kv_dictionary RENAME CONSTRAINT ts_key_id_pkey TO key_dictionary_id_pkey; + ALTER TABLE ts_kv_dictionary RENAME TO key_dictionary; + ELSE CREATE TABLE IF NOT EXISTS key_dictionary( + key varchar(255) NOT NULL, + key_id serial UNIQUE, + CONSTRAINT key_dictionary_id_pkey PRIMARY KEY (key) + ); + END IF; + END; +$$; + +-- insert keys into key_dictionary +DO +$$ + BEGIN + IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'attribute_kv_old') THEN + INSERT INTO key_dictionary(key) SELECT DISTINCT attribute_key FROM attribute_kv_old ON CONFLICT DO NOTHING; + END IF; + END; +$$; + +-- migrate attributes from attribute_kv_old to attribute_kv +DO +$$ +DECLARE + row_num_old integer; + row_num integer; +BEGIN + IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'attribute_kv_old') THEN + INSERT INTO attribute_kv(entity_id, attribute_type, attribute_key, bool_v, str_v, long_v, dbl_v, json_v, last_update_ts) + SELECT a.entity_id, CASE + WHEN a.attribute_type = 'CLIENT_SCOPE' THEN 1 + WHEN a.attribute_type = 'SERVER_SCOPE' THEN 2 + WHEN a.attribute_type = 'SHARED_SCOPE' THEN 3 + ELSE 0 + END, + k.key_id, a.bool_v, a.str_v, a.long_v, a.dbl_v, a.json_v, a.last_update_ts + FROM attribute_kv_old a INNER JOIN key_dictionary k ON (a.attribute_key = k.key); + SELECT COUNT(*) INTO row_num_old FROM attribute_kv_old; + SELECT COUNT(*) INTO row_num FROM attribute_kv; + RAISE NOTICE 'Migrated % of % rows', row_num, row_num_old; + + IF row_num != 0 THEN + DROP TABLE IF EXISTS attribute_kv_old; + ELSE + RAISE EXCEPTION 'Table attribute_kv is empty'; + END IF; + + CREATE INDEX IF NOT EXISTS idx_attribute_kv_by_key_and_last_update_ts ON attribute_kv(entity_id, attribute_key, last_update_ts desc); + END IF; +EXCEPTION + WHEN others THEN + ROLLBACK; + RAISE EXCEPTION 'Error during COPY: %', SQLERRM; +END +$$; + +-- OAUTH2 PARAMS ALTER TABLE START + +ALTER TABLE oauth2_params + ADD COLUMN IF NOT EXISTS edge_enabled boolean DEFAULT false; + +-- OAUTH2 PARAMS ALTER TABLE END + +-- QUEUE STATS UPDATE START + +CREATE TABLE IF NOT EXISTS queue_stats ( + id uuid NOT NULL CONSTRAINT queue_stats_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid NOT NULL, + queue_name varchar(255) NOT NULL, + service_id varchar(255) NOT NULL, + CONSTRAINT queue_stats_name_unq_key UNIQUE (tenant_id, queue_name, service_id) +); + +INSERT INTO queue_stats + SELECT id, created_time, tenant_id, substring(name FROM 1 FOR position('_' IN name) - 1) AS queue_name, + substring(name FROM position('_' IN name) + 1) AS service_id + FROM asset + WHERE type = 'TbServiceQueue' and name LIKE '%\_%'; + +DELETE FROM asset WHERE type='TbServiceQueue'; +DELETE FROM asset_profile WHERE name ='TbServiceQueue'; + +-- QUEUE STATS UPDATE END 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 6a10791091..f411344e3f 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -19,6 +19,8 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import jakarta.annotation.Nullable; +import jakarta.annotation.PostConstruct; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -79,6 +81,7 @@ import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; +import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; @@ -115,9 +118,6 @@ import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import org.thingsboard.server.service.transport.TbCoreToTransportService; -import jakarta.annotation.Nullable; -import jakarta.annotation.PostConstruct; - import java.io.PrintWriter; import java.io.StringWriter; import java.util.concurrent.ConcurrentHashMap; @@ -447,6 +447,11 @@ public class ActorSystemContext { @Getter private QueueService queueService; + @Lazy + @Autowired(required = false) + @Getter + private QueueStatsService queueStatsService; + @Lazy @Autowired(required = false) @Getter diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index d6b5da4ddf..87d5c8a63a 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -21,6 +21,7 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import jakarta.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.thingsboard.common.util.JacksonUtil; @@ -42,7 +43,6 @@ import org.thingsboard.server.common.data.id.RpcId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKey; import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; @@ -68,14 +68,11 @@ import org.thingsboard.server.common.msg.rule.engine.DeviceEdgeUpdateMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceNameOrTypeUpdateMsg; import org.thingsboard.server.common.msg.timeout.DeviceActorServerSideRpcTimeoutMsg; import org.thingsboard.server.common.util.KvProtoUtil; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; import org.thingsboard.server.gen.transport.TransportProtos.DeviceSessionsCacheEntry; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; -import org.thingsboard.server.gen.transport.TransportProtos.KeyValueProto; -import org.thingsboard.server.gen.transport.TransportProtos.KeyValueType; import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseNotificationProto; import org.thingsboard.server.gen.transport.TransportProtos.SessionEvent; import org.thingsboard.server.gen.transport.TransportProtos.SessionEventMsg; @@ -97,7 +94,6 @@ import org.thingsboard.server.service.rpc.RpcSubmitStrategy; import org.thingsboard.server.service.state.DefaultDeviceStateService; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; -import jakarta.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -255,14 +251,12 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso } private boolean isSendNewRpcAvailable() { - switch (rpcSubmitStrategy) { - case SEQUENTIAL_ON_ACK_FROM_DEVICE: - return toDeviceRpcPendingMap.values().stream().filter(md -> !md.isDelivered()).findAny().isEmpty(); - case SEQUENTIAL_ON_RESPONSE_FROM_DEVICE: - return toDeviceRpcPendingMap.values().stream().filter(ToDeviceRpcRequestMetadata::isDelivered).findAny().isEmpty(); - default: - return true; - } + return switch (rpcSubmitStrategy) { + case SEQUENTIAL_ON_ACK_FROM_DEVICE -> toDeviceRpcPendingMap.values().stream().filter(md -> !md.isDelivered()).findAny().isEmpty(); + case SEQUENTIAL_ON_RESPONSE_FROM_DEVICE -> + toDeviceRpcPendingMap.values().stream().filter(ToDeviceRpcRequestMetadata::isDelivered).findAny().isEmpty(); + default -> true; + }; } private void createRpc(ToDeviceRpcRequest request, RpcStatus status) { @@ -466,7 +460,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso handleSessionActivity(sessionInfo, msg.getSubscriptionInfo()); } if (msg.hasClaimDevice()) { - handleClaimDeviceMsg(msg.getClaimDevice()); + handleClaimDeviceMsg(sessionInfo, msg.getClaimDevice()); } if (msg.hasRpcResponseStatusMsg()) { processRpcResponseStatus(sessionInfo, msg.getRpcResponseStatusMsg()); @@ -491,9 +485,22 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso }); } - private void handleClaimDeviceMsg(ClaimDeviceMsg msg) { + private void handleClaimDeviceMsg(SessionInfoProto sessionInfo, ClaimDeviceMsg msg) { + UUID sessionId = getSessionId(sessionInfo); DeviceId deviceId = new DeviceId(new UUID(msg.getDeviceIdMSB(), msg.getDeviceIdLSB())); - systemContext.getClaimDevicesService().registerClaimingInfo(tenantId, deviceId, msg.getSecretKey(), msg.getDurationMs()); + ListenableFuture registrationFuture = systemContext.getClaimDevicesService() + .registerClaimingInfo(tenantId, deviceId, msg.getSecretKey(), msg.getDurationMs()); + Futures.addCallback(registrationFuture, new FutureCallback<>() { + @Override + public void onSuccess(Void result) { + log.debug("[{}][{}] Successfully processed register claiming info request!", sessionId, deviceId); + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}][{}] Failed to process register claiming info request due to: ", sessionId, deviceId, t); + } + }, MoreExecutors.directExecutor()); } private void reportSessionOpen() { @@ -928,34 +935,6 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso return systemContext.getEdgeEventService().saveAsync(edgeEvent); } - private KeyValueProto toKeyValueProto(KvEntry kvEntry) { - KeyValueProto.Builder builder = KeyValueProto.newBuilder(); - builder.setKey(kvEntry.getKey()); - switch (kvEntry.getDataType()) { - case BOOLEAN: - builder.setType(KeyValueType.BOOLEAN_V); - builder.setBoolV(kvEntry.getBooleanValue().get()); - break; - case DOUBLE: - builder.setType(KeyValueType.DOUBLE_V); - builder.setDoubleV(kvEntry.getDoubleValue().get()); - break; - case LONG: - builder.setType(KeyValueType.LONG_V); - builder.setLongV(kvEntry.getLongValue().get()); - break; - case STRING: - builder.setType(KeyValueType.STRING_V); - builder.setStringV(kvEntry.getStrValue().get()); - break; - case JSON: - builder.setType(KeyValueType.JSON_V); - builder.setJsonV(kvEntry.getJsonValue().get()); - break; - } - return builder.build(); - } - void restoreSessions() { if (systemContext.isLocalCacheType()) { return; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 79dddfdbe4..0dc9d68984 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -96,6 +96,7 @@ import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; +import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; @@ -768,6 +769,11 @@ class DefaultTbContext implements TbContext { return mainCtx.getQueueService(); } + @Override + public QueueStatsService getQueueStatsService() { + return mainCtx.getQueueStatsService(); + } + @Override public EventLoopGroup getSharedEventLoop() { return mainCtx.getSharedEventLoopGroupService().getSharedEventLoopGroup(); diff --git a/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java b/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java index 93e8c0be90..3df3d6887a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java @@ -122,15 +122,9 @@ public abstract class AbstractRpcController extends BaseController { logRpcCall(rpcRequest, rpcError, null); RpcError error = rpcError.get(); switch (error) { - case TIMEOUT: - responseWriter.setResult(new ResponseEntity<>(timeoutStatus)); - break; - case NO_ACTIVE_CONNECTION: - responseWriter.setResult(new ResponseEntity<>(noActiveConnectionStatus)); - break; - default: - responseWriter.setResult(new ResponseEntity<>(timeoutStatus)); - break; + case TIMEOUT -> responseWriter.setResult(new ResponseEntity<>(timeoutStatus)); + case NO_ACTIVE_CONNECTION -> responseWriter.setResult(new ResponseEntity<>(noActiveConnectionStatus)); + default -> responseWriter.setResult(new ResponseEntity<>(timeoutStatus)); } } else { Optional responseData = response.getResponse(); diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 9c437821c6..0942329f40 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -506,7 +506,7 @@ public abstract class BaseController { TenantProfile checkTenantProfileId(TenantProfileId tenantProfileId, Operation operation) throws ThingsboardException { try { - validateId(tenantProfileId, "Incorrect tenantProfileId " + tenantProfileId); + validateId(tenantProfileId, id -> "Incorrect tenantProfileId " + id); TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(getTenantId(), tenantProfileId); checkNotNull(tenantProfile, "Tenant profile with id [" + tenantProfileId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, operation); @@ -541,7 +541,7 @@ public abstract class BaseController { if (entityId == null) { throw new ThingsboardException("Parameter entityId can't be empty!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } - validateId(entityId.getId(), "Incorrect entityId " + entityId); + validateId(entityId.getId(), id -> "Incorrect entityId " + id); switch (entityId.getEntityType()) { case ALARM: checkAlarmId(new AlarmId(entityId.getId()), operation); @@ -668,7 +668,7 @@ public abstract class BaseController { AlarmComment checkAlarmCommentId(AlarmCommentId alarmCommentId, AlarmId alarmId) throws ThingsboardException { try { - validateId(alarmCommentId, "Incorrect alarmCommentId " + alarmCommentId); + validateId(alarmCommentId, id -> "Incorrect alarmCommentId " + id); AlarmComment alarmComment = alarmCommentService.findAlarmCommentByIdAsync(getCurrentUser().getTenantId(), alarmCommentId).get(); checkNotNull(alarmComment, "Alarm comment with id [" + alarmCommentId + "] is not found"); if (!alarmId.equals(alarmComment.getAlarmId())) { @@ -736,7 +736,7 @@ public abstract class BaseController { } protected RuleNode checkRuleNode(RuleNodeId ruleNodeId, Operation operation) throws ThingsboardException { - validateId(ruleNodeId, "Incorrect ruleNodeId " + ruleNodeId); + validateId(ruleNodeId, id -> "Incorrect ruleNodeId " + id); RuleNode ruleNode = ruleChainService.findRuleNodeById(getTenantId(), ruleNodeId); checkNotNull(ruleNode, "Rule node with id [" + ruleNodeId + "] is not found"); checkRuleChain(ruleNode.getRuleChainId(), operation); diff --git a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java index 595d6feef1..5271de2329 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java @@ -68,7 +68,7 @@ public class ComponentDescriptorController extends BaseController { @RequestMapping(value = "/components/{componentType}", method = RequestMethod.GET) @ResponseBody public List getComponentDescriptorsByType( - @Parameter(description = "Type of the Rule Node", schema = @Schema(allowableValues = "ENRICHMENT,FILTER,TRANSFORMATION,ACTION,EXTERNAL", required = true)) + @Parameter(description = "Type of the Rule Node", schema = @Schema(allowableValues = "ENRICHMENT,FILTER,TRANSFORMATION,ACTION,EXTERNAL", requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("componentType") String strComponentType, @Parameter(description = "Type of the Rule Chain", schema = @Schema(allowableValues = "CORE,EDGE")) @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java index e6e57e8b8c..505244539b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java @@ -20,7 +20,6 @@ import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java index 55f4e6d9fa..96f38daf35 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -105,6 +106,8 @@ public class NotificationController extends BaseController { private final NotificationCenter notificationCenter; private final NotificationSettingsService notificationSettingsService; + private static final String DELIVERY_METHOD_ALLOWABLE_VALUES = "WEB,MOBILE_APP"; + @ApiOperation(value = "Get notifications (getNotifications)", notes = "Returns the page of notifications for current user." + NEW_LINE + PAGE_DATA_PARAMETERS + @@ -172,10 +175,23 @@ public class NotificationController extends BaseController { @RequestParam(required = false) String sortOrder, @Parameter(description = "To search for unread notifications only") @RequestParam(defaultValue = "false") boolean unreadOnly, + @Parameter(description = "Delivery method", schema = @Schema(allowableValues = {DELIVERY_METHOD_ALLOWABLE_VALUES})) + @RequestParam(defaultValue = "WEB") NotificationDeliveryMethod deliveryMethod, @AuthenticationPrincipal SecurityUser user) throws ThingsboardException { // no permissions PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return notificationService.findNotificationsByRecipientIdAndReadStatus(user.getTenantId(), user.getId(), unreadOnly, pageLink); + return notificationService.findNotificationsByRecipientIdAndReadStatus(user.getTenantId(), deliveryMethod, user.getId(), unreadOnly, pageLink); + } + + @ApiOperation(value = "Get unread notifications count (getUnreadNotificationsCount)", + notes = "Returns unread notifications count for chosen delivery method." + + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @GetMapping("/notifications/unread/count") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + public Integer getUnreadNotificationsCount(@Parameter(description = "Delivery method", schema = @Schema(allowableValues = {DELIVERY_METHOD_ALLOWABLE_VALUES})) + @RequestParam(defaultValue = "MOBILE_APP") NotificationDeliveryMethod deliveryMethod, + @AuthenticationPrincipal SecurityUser user) { + return notificationService.countUnreadNotificationsByRecipientId(user.getTenantId(), deliveryMethod, user.getId()); } @ApiOperation(value = "Mark notification as read (markNotificationAsRead)", @@ -195,9 +211,11 @@ public class NotificationController extends BaseController { AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PutMapping("/notifications/read") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - public void markAllNotificationsAsRead(@AuthenticationPrincipal SecurityUser user) { + public void markAllNotificationsAsRead(@Parameter(description = "Delivery method", schema = @Schema(allowableValues = {DELIVERY_METHOD_ALLOWABLE_VALUES})) + @RequestParam(defaultValue = "WEB") NotificationDeliveryMethod deliveryMethod, + @AuthenticationPrincipal SecurityUser user) { // no permissions - notificationCenter.markAllNotificationsAsRead(user.getTenantId(), user.getId()); + notificationCenter.markAllNotificationsAsRead(user.getTenantId(), deliveryMethod, user.getId()); } @ApiOperation(value = "Delete notification (deleteNotification)", diff --git a/application/src/main/java/org/thingsboard/server/controller/QueueController.java b/application/src/main/java/org/thingsboard/server/controller/QueueController.java index 089ba9bcca..6b90f8f8fd 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QueueController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QueueController.java @@ -67,7 +67,7 @@ public class QueueController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/queues", params = {"serviceType", "pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public PageData getTenantQueuesByServiceType(@Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, required = true)) + public PageData getTenantQueuesByServiceType(@Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, requiredMode = Schema.RequiredMode.REQUIRED)) @RequestParam String serviceType, @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -126,7 +126,7 @@ public class QueueController extends BaseController { @ResponseBody public Queue saveQueue(@Parameter(description = "A JSON value representing the queue.") @RequestBody Queue queue, - @Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, required = true)) + @Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, requiredMode = Schema.RequiredMode.REQUIRED)) @RequestParam String serviceType) throws ThingsboardException { checkParameter("serviceType", serviceType); queue.setTenantId(getCurrentUser().getTenantId()); diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index 8d51daab8b..680633e103 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -47,8 +47,8 @@ import org.thingsboard.server.actors.tenant.DebugTbRateLimits; import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; @@ -80,6 +80,7 @@ import org.thingsboard.server.service.security.permission.Resource; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; @@ -340,18 +341,8 @@ public class RuleChainController extends BaseController { RuleNodeId ruleNodeId = new RuleNodeId(toUUID(strRuleNodeId)); checkRuleNode(ruleNodeId, Operation.READ); TenantId tenantId = getCurrentUser().getTenantId(); - List events = eventService.findLatestEvents(tenantId, ruleNodeId, EventType.DEBUG_RULE_NODE, 2); - JsonNode result = null; - if (events != null) { - for (EventInfo event : events) { - JsonNode body = event.getBody(); - if (body.has("type") && body.get("type").asText().equals("IN")) { - result = body; - break; - } - } - } - return result; + return Optional.ofNullable(eventService.findLatestDebugRuleNodeInEvent(tenantId, ruleNodeId)) + .map(EventInfo::getBody).orElse(null); } @ApiOperation(value = "Is TBEL script executor enabled", diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java index 1908459200..e646c34e4f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -268,9 +268,9 @@ public class TbResourceController extends BaseController { "You can specify parameters to filter the results. " + LWM2M_OBJECT_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @GetMapping(value = "/resource/lwm2m") - public List getLwm2mListObjects(@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}, required = true)) + public List getLwm2mListObjects(@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}, requiredMode = Schema.RequiredMode.REQUIRED)) @RequestParam String sortOrder, - @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"id", "name"}, required = true)) + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"id", "name"}, requiredMode = Schema.RequiredMode.REQUIRED)) @RequestParam String sortProperty, @Parameter(description = "LwM2M Object ids.", required = true) @RequestParam(required = false) String[] objectIds) throws ThingsboardException { @@ -315,4 +315,4 @@ public class TbResourceController extends BaseController { .body(resource); } -} \ No newline at end of file +} diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index c73ac09cf1..cea449af31 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -25,7 +25,6 @@ import com.google.gson.JsonElement; import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -36,7 +35,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -49,9 +47,6 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.server.common.data.kv.AggregationParams; -import org.thingsboard.server.common.data.kv.IntervalType; -import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; @@ -65,6 +60,7 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.kv.Aggregation; +import org.thingsboard.server.common.data.kv.AggregationParams; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; @@ -74,6 +70,7 @@ import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.IntervalType; import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; @@ -239,7 +236,7 @@ public class TelemetryController extends BaseController { public DeferredResult getAttributesByScope( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, required = true)) @PathVariable("scope") AttributeScope scope, + @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { SecurityUser user = getCurrentUser(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, @@ -356,7 +353,7 @@ public class TelemetryController extends BaseController { @ResponseBody public DeferredResult saveDeviceAttributes( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("deviceId") String deviceIdStr, - @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, required = true)) @PathVariable("scope") AttributeScope scope, + @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); return saveAttributes(getTenantId(), entityId, scope, request); @@ -379,7 +376,7 @@ public class TelemetryController extends BaseController { public DeferredResult saveEntityAttributesV1( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"})) @PathVariable("scope")AttributeScope scope, + @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"})) @PathVariable("scope") AttributeScope scope, @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveAttributes(getTenantId(), entityId, scope, request); @@ -402,7 +399,7 @@ public class TelemetryController extends BaseController { public DeferredResult saveEntityAttributesV2( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, required = true)) @PathVariable("scope")AttributeScope scope, + @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope")AttributeScope scope, @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveAttributes(getTenantId(), entityId, scope, request); @@ -553,7 +550,7 @@ public class TelemetryController extends BaseController { @ResponseBody public DeferredResult deleteDeviceAttributes( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(DEVICE_ID) String deviceIdStr, - @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, required = true)) @PathVariable("scope")AttributeScope scope, + @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope")AttributeScope scope, @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION, required = true)@RequestParam(name = "keys")String keysStr) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); return deleteAttributes(entityId, scope, keysStr); @@ -918,4 +915,5 @@ public class TelemetryController extends BaseController { } return entry.getValue(); } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java index 7468e683ab..01487b20a0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java @@ -100,7 +100,7 @@ public class TwoFactorAuthConfigController extends BaseController { ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PostMapping("/account/config/generate") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - public TwoFaAccountConfig generateTwoFaAccountConfig(@Parameter(description = "2FA provider type to generate new account config for", schema = @Schema(defaultValue = "TOTP", required = true)) + public TwoFaAccountConfig generateTwoFaAccountConfig(@Parameter(description = "2FA provider type to generate new account config for", schema = @Schema(defaultValue = "TOTP", requiredMode = Schema.RequiredMode.REQUIRED)) @RequestParam TwoFaProviderType providerType) throws Exception { SecurityUser user = getCurrentUser(); return twoFactorAuthService.generateNewAccountConfig(user, providerType); diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index f999d31088..eb9d908bab 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -128,8 +128,11 @@ public class ThingsboardInstallService { databaseEntitiesUpgradeService.upgradeDatabase("3.6.2"); systemDataLoaderService.updateDefaultNotificationConfigs(); case "3.6.3": - log.info("Upgrading ThingsBoard from version 3.6.3 to 3.7.0 ..."); + log.info("Upgrading ThingsBoard from version 3.6.3 to 3.6.4 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.6.3"); + case "3.6.4": + log.info("Upgrading ThingsBoard from version 3.6.4 to 3.7.0 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.6.4"); //TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache break; default: diff --git a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java index db051ca0da..ab6d32fb44 100644 --- a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java +++ b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java @@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; @@ -127,20 +128,20 @@ public class EntityActionService { } else { entityNode = JacksonUtil.newObjectNode(); if (actionType == ActionType.ATTRIBUTES_UPDATED) { - String scope = extractParameter(String.class, 0, additionalInfo); + AttributeScope scope = extractParameter(AttributeScope.class, 0, additionalInfo); @SuppressWarnings("unchecked") List attributes = extractParameter(List.class, 1, additionalInfo); - metaData.putValue(DataConstants.SCOPE, scope); + metaData.putValue(DataConstants.SCOPE, scope.name()); if (attributes != null) { for (AttributeKvEntry attr : attributes) { JacksonUtil.addKvEntry(entityNode, attr); } } } else if (actionType == ActionType.ATTRIBUTES_DELETED) { - String scope = extractParameter(String.class, 0, additionalInfo); + AttributeScope scope = extractParameter(AttributeScope.class, 0, additionalInfo); @SuppressWarnings("unchecked") List keys = extractParameter(List.class, 1, additionalInfo); - metaData.putValue(DataConstants.SCOPE, scope); + metaData.putValue(DataConstants.SCOPE, scope.name()); ArrayNode attrsArrayNode = entityNode.putArray("attributes"); if (keys != null) { keys.forEach(attrsArrayNode::add); @@ -249,7 +250,7 @@ public class EntityActionService { return result; } - private void addTimeseries(ObjectNode entityNode, List timeseries) throws Exception { + private void addTimeseries(ObjectNode entityNode, List timeseries) { if (timeseries != null && !timeseries.isEmpty()) { ArrayNode result = entityNode.putArray("timeseries"); Map> groupedTelemetry = timeseries.stream() diff --git a/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java index 974bc06a40..a77b7d0874 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java @@ -20,6 +20,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; +import jakarta.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -28,7 +29,6 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; -import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; @@ -51,9 +51,7 @@ import org.thingsboard.server.dao.device.claim.ReclaimResult; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; -import jakarta.annotation.Nullable; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Optional; @@ -89,16 +87,16 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService { Device device = deviceService.findDeviceById(tenantId, deviceId); Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE); List key = constructCacheKey(device.getId()); + String deviceName = device.getName(); if (isAllowedClaimingByDefault) { if (device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { persistInCache(secretKey, durationMs, cache, key); return Futures.immediateFuture(null); } - log.warn("The device [{}] has been already claimed!", device.getName()); - return Futures.immediateFailedFuture(new IllegalArgumentException()); + return Futures.immediateFailedFuture(new IllegalArgumentException("Device [" + deviceName + "] has been already claimed!")); } else { ListenableFuture> claimingAllowedFuture = attributesService.find(tenantId, device.getId(), - AttributeScope.SERVER_SCOPE, Collections.singletonList(CLAIM_ATTRIBUTE_NAME)); + AttributeScope.SERVER_SCOPE, List.of(CLAIM_ATTRIBUTE_NAME)); return Futures.transform(claimingAllowedFuture, list -> { if (list != null && !list.isEmpty()) { Optional claimingAllowedOptional = list.get(0).getBooleanValue(); @@ -108,8 +106,7 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService { return null; } } - log.warn("Failed to find claimingAllowed attribute for device or it is already claimed![{}]", device.getName()); - throw new IllegalArgumentException(); + throw new IllegalArgumentException("Failed to find claimingAllowed attribute for device [" + deviceName + "] or it is already claimed!"); }, MoreExecutors.directExecutor()); } } @@ -182,7 +179,7 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService { } SettableFuture result = SettableFuture.create(); telemetryService.saveAndNotify( - tenantId, savedDevice.getId(), AttributeScope.SERVER_SCOPE, Collections.singletonList( + tenantId, savedDevice.getId(), AttributeScope.SERVER_SCOPE, List.of( new BaseAttributeKvEntry(new BooleanDataEntry(CLAIM_ATTRIBUTE_NAME, true), System.currentTimeMillis()) ), new FutureCallback<>() { @@ -203,7 +200,7 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService { } private List constructCacheKey(DeviceId deviceId) { - return Collections.singletonList(deviceId); + return List.of(deviceId); } private void persistInCache(String secretKey, long durationMs, Cache cache, List key) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java index eede4a3afc..f29255e414 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java @@ -16,6 +16,8 @@ package org.thingsboard.server.service.edge; import com.fasterxml.jackson.databind.node.ObjectNode; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -42,6 +44,8 @@ import org.thingsboard.server.service.edge.rpc.processor.device.DeviceEdgeProces import org.thingsboard.server.service.edge.rpc.processor.device.profile.DeviceProfileEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.edge.EdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.notification.NotificationEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.oauth2.OAuth2EdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.ota.OtaPackageEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.queue.QueueEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgeProcessor; @@ -53,8 +57,6 @@ import org.thingsboard.server.service.edge.rpc.processor.user.UserEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetBundleEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetTypeEdgeProcessor; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; @@ -126,6 +128,12 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService { @Autowired private ResourceEdgeProcessor resourceEdgeProcessor; + @Autowired + private NotificationEdgeProcessor notificationEdgeProcessor; + + @Autowired + private OAuth2EdgeProcessor oAuth2EdgeProcessor; + @Autowired protected ApplicationEventPublisher eventPublisher; @@ -171,68 +179,30 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService { } EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); switch (type) { - case EDGE: - edgeProcessor.processEdgeNotification(tenantId, edgeNotificationMsg); - break; - case ASSET: - assetProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case ASSET_PROFILE: - assetProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case DEVICE: - deviceProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case DEVICE_PROFILE: - deviceProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case ENTITY_VIEW: - entityViewProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case DASHBOARD: - dashboardProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case RULE_CHAIN: - ruleChainProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case USER: - userProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case CUSTOMER: - customerProcessor.processCustomerNotification(tenantId, edgeNotificationMsg); - break; - case OTA_PACKAGE: - otaPackageProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case WIDGETS_BUNDLE: - widgetBundleProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case WIDGET_TYPE: - widgetTypeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case QUEUE: - queueProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case ALARM: - alarmProcessor.processAlarmNotification(tenantId, edgeNotificationMsg); - break; - case ALARM_COMMENT: - alarmProcessor.processAlarmCommentNotification(tenantId, edgeNotificationMsg); - break; - case RELATION: - relationProcessor.processRelationNotification(tenantId, edgeNotificationMsg); - break; - case TENANT: - tenantEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case TENANT_PROFILE: - tenantProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case TB_RESOURCE: - resourceEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - default: - log.warn("[{}] Edge event type [{}] is not designed to be pushed to edge", tenantId, type); + case EDGE -> edgeProcessor.processEdgeNotification(tenantId, edgeNotificationMsg); + case ASSET -> assetProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case ASSET_PROFILE -> assetProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case DEVICE -> deviceProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case DEVICE_PROFILE -> deviceProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case ENTITY_VIEW -> entityViewProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case DASHBOARD -> dashboardProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case RULE_CHAIN -> ruleChainProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case USER -> userProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case CUSTOMER -> customerProcessor.processCustomerNotification(tenantId, edgeNotificationMsg); + case OTA_PACKAGE -> otaPackageProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case WIDGETS_BUNDLE -> widgetBundleProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case WIDGET_TYPE -> widgetTypeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case QUEUE -> queueProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case ALARM -> alarmProcessor.processAlarmNotification(tenantId, edgeNotificationMsg); + case ALARM_COMMENT -> alarmProcessor.processAlarmCommentNotification(tenantId, edgeNotificationMsg); + case RELATION -> relationProcessor.processRelationNotification(tenantId, edgeNotificationMsg); + case TENANT -> tenantEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case TENANT_PROFILE -> tenantProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case NOTIFICATION_RULE, NOTIFICATION_TARGET, NOTIFICATION_TEMPLATE -> + notificationEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case TB_RESOURCE -> resourceEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case OAUTH2 -> oAuth2EdgeProcessor.processOAuth2Notification(tenantId, edgeNotificationMsg); + default -> log.warn("[{}] Edge event type [{}] is not designed to be pushed to edge", tenantId, type); } } catch (Exception e) { callBackFailure(tenantId, edgeNotificationMsg, callback, e); @@ -248,4 +218,5 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService { log.error("[{}] Can't push to edge updates, edgeNotificationMsg [{}]", tenantId, edgeNotificationMsg, throwable); callback.onFailure(throwable); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index f5b769e568..489fc6b6b8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -32,6 +32,10 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.notification.NotificationRuleService; +import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.dao.notification.NotificationTemplateService; +import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.resource.ResourceService; @@ -61,6 +65,8 @@ import org.thingsboard.server.service.edge.rpc.processor.device.profile.DevicePr import org.thingsboard.server.service.edge.rpc.processor.edge.EdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewProcessorFactory; +import org.thingsboard.server.service.edge.rpc.processor.notification.NotificationEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.oauth2.OAuth2EdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.ota.OtaPackageEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.queue.QueueEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgeProcessor; @@ -151,6 +157,18 @@ public class EdgeContextComponent { @Autowired private ResourceService resourceService; + @Autowired + private NotificationRuleService notificationRuleService; + + @Autowired + private NotificationTargetService notificationTargetService; + + @Autowired + private NotificationTemplateService notificationTemplateService; + + @Autowired + private OAuth2Service oAuth2Service; + @Autowired private RateLimitService rateLimitService; @@ -220,6 +238,12 @@ public class EdgeContextComponent { @Autowired private ResourceEdgeProcessor resourceEdgeProcessor; + @Autowired + private NotificationEdgeProcessor notificationEdgeProcessor; + + @Autowired + private OAuth2EdgeProcessor oAuth2EdgeProcessor; + @Autowired private EdgeMsgConstructor edgeMsgConstructor; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index e54adb05fa..d975217e1f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.oauth2.OAuth2Info; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChain; @@ -159,43 +160,41 @@ public class EdgeEventSourcingListener { private boolean isValidSaveEntityEventForEdgeProcessing(SaveEntityEvent event) { Object entity = event.getEntity(); Object oldEntity = event.getOldEntity(); - switch (event.getEntityId().getEntityType()) { - case RULE_CHAIN: - if (entity instanceof RuleChain) { - RuleChain ruleChain = (RuleChain) entity; - return RuleChainType.EDGE.equals(ruleChain.getType()); - } - break; - case USER: - if (entity instanceof User) { - User user = (User) entity; - if (Authority.SYS_ADMIN.equals(user.getAuthority())) { - return false; + if (event.getEntityId() != null) { + switch (event.getEntityId().getEntityType()) { + case RULE_CHAIN: + if (entity instanceof RuleChain ruleChain) { + return RuleChainType.EDGE.equals(ruleChain.getType()); + } + break; + case USER: + if (entity instanceof User user) { + if (Authority.SYS_ADMIN.equals(user.getAuthority())) { + return false; + } + if (oldEntity != null) { + User oldUser = (User) oldEntity; + cleanUpUserAdditionalInfo(oldUser); + cleanUpUserAdditionalInfo(user); + return !user.equals(oldUser); + } } - if (oldEntity != null) { - User oldUser = (User) oldEntity; - cleanUpUserAdditionalInfo(oldUser); - cleanUpUserAdditionalInfo(user); - return !user.equals(oldUser); + break; + case OTA_PACKAGE: + if (entity instanceof OtaPackageInfo otaPackageInfo) { + return otaPackageInfo.hasUrl() || otaPackageInfo.isHasData(); + } + break; + case ALARM: + if (entity instanceof AlarmApiCallResult || entity instanceof Alarm) { + return false; } - } - break; - case OTA_PACKAGE: - if (entity instanceof OtaPackageInfo) { - OtaPackageInfo otaPackageInfo = (OtaPackageInfo) entity; - return otaPackageInfo.hasUrl() || otaPackageInfo.isHasData(); - } - break; - case ALARM: - if (entity instanceof AlarmApiCallResult || entity instanceof Alarm) { + break; + case TENANT: + return !event.getCreated(); + case API_USAGE_STATE, EDGE: return false; - } - break; - case TENANT: - return !event.getCreated(); - case API_USAGE_STATE: - case EDGE: - return false; + } } // Default: If the entity doesn't match any of the conditions, consider it as valid. return true; @@ -206,8 +205,7 @@ public class EdgeEventSourcingListener { if (user.getAdditionalInfo() instanceof NullNode) { user.setAdditionalInfo(null); } - if (user.getAdditionalInfo() instanceof ObjectNode) { - ObjectNode additionalInfo = ((ObjectNode) user.getAdditionalInfo()); + if (user.getAdditionalInfo() instanceof ObjectNode additionalInfo) { additionalInfo.remove(UserServiceImpl.FAILED_LOGIN_ATTEMPTS); additionalInfo.remove(UserServiceImpl.LAST_LOGIN_TS); if (additionalInfo.isEmpty()) { @@ -221,6 +219,8 @@ public class EdgeEventSourcingListener { private EdgeEventType getEdgeEventTypeForEntityEvent(Object entity) { if (entity instanceof AlarmComment) { return EdgeEventType.ALARM_COMMENT; + } else if (entity instanceof OAuth2Info) { + return EdgeEventType.OAUTH2; } return null; } @@ -228,6 +228,8 @@ public class EdgeEventSourcingListener { private String getBodyMsgForEntityEvent(Object entity) { if (entity instanceof AlarmComment) { return JacksonUtil.toString(entity); + } else if (entity instanceof OAuth2Info) { + return JacksonUtil.toString(entity); } return null; } @@ -238,4 +240,5 @@ public class EdgeEventSourcingListener { } return isCreated ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index a73d3a20ff..55e8e051ab 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -498,9 +498,9 @@ public final class EdgeGrpcSession implements Closeable { log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", this.tenantId, this.sessionId, copy.size()); for (DownlinkMsg downlinkMsg : copy) { if (this.clientMaxInboundMessageSize != 0 && downlinkMsg.getSerializedSize() > this.clientMaxInboundMessageSize) { - String error = String.format("Client max inbound message size [{%s}] is exceeded. Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE " + + String error = String.format("Client max inbound message size %s is exceeded. Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE " + "env variable on the edge and restart it.", this.clientMaxInboundMessageSize); - String message = String.format("Downlink msg size [{%s}] exceeds client max inbound message size [{%s}]. " + + String message = String.format("Downlink msg size %s exceeds client max inbound message size %s. " + "Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE env variable on the edge and restart it.", downlinkMsg.getSerializedSize(), this.clientMaxInboundMessageSize); log.error("[{}][{}][{}] {} Message {}", this.tenantId, edge.getId(), this.sessionId, message, downlinkMsg); ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId) @@ -551,35 +551,13 @@ public final class EdgeGrpcSession implements Closeable { DownlinkMsg downlinkMsg = null; try { switch (edgeEvent.getAction()) { - case UPDATED: - case ADDED: - case DELETED: - case ASSIGNED_TO_EDGE: - case UNASSIGNED_FROM_EDGE: - case ALARM_ACK: - case ALARM_CLEAR: - case ALARM_DELETE: - case CREDENTIALS_UPDATED: - case RELATION_ADD_OR_UPDATE: - case RELATION_DELETED: - case CREDENTIALS_REQUEST: - case RPC_CALL: - case ASSIGNED_TO_CUSTOMER: - case UNASSIGNED_FROM_CUSTOMER: - case ADDED_COMMENT: - case UPDATED_COMMENT: - case DELETED_COMMENT: + case UPDATED, ADDED, DELETED, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, ALARM_ACK, ALARM_CLEAR, ALARM_DELETE, CREDENTIALS_UPDATED, RELATION_ADD_OR_UPDATE, RELATION_DELETED, CREDENTIALS_REQUEST, RPC_CALL, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> { downlinkMsg = convertEntityEventToDownlink(edgeEvent); log.trace("[{}][{}] entity message processed [{}]", this.tenantId, this.sessionId, downlinkMsg); - break; - case ATTRIBUTES_UPDATED: - case POST_ATTRIBUTES: - case ATTRIBUTES_DELETED: - case TIMESERIES_UPDATED: - downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edgeEvent); - break; - default: - log.warn("[{}][{}] Unsupported action type [{}]", this.tenantId, this.sessionId, edgeEvent.getAction()); + } + case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED -> + downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent); + default -> log.warn("[{}][{}] Unsupported action type [{}]", this.tenantId, this.sessionId, edgeEvent.getAction()); } } catch (Exception e) { log.error("[{}][{}] Exception during converting edge event to downlink msg", this.tenantId, this.sessionId, e); @@ -705,6 +683,14 @@ public final class EdgeGrpcSession implements Closeable { return ctx.getTenantEdgeProcessor().convertTenantEventToDownlink(edgeEvent, this.edgeVersion); case TENANT_PROFILE: return ctx.getTenantProfileEdgeProcessor().convertTenantProfileEventToDownlink(edgeEvent, this.edgeVersion); + case NOTIFICATION_RULE: + return ctx.getNotificationEdgeProcessor().convertNotificationRuleToDownlink(edgeEvent); + case NOTIFICATION_TARGET: + return ctx.getNotificationEdgeProcessor().convertNotificationTargetToDownlink(edgeEvent); + case NOTIFICATION_TEMPLATE: + return ctx.getNotificationEdgeProcessor().convertNotificationTemplateToDownlink(edgeEvent); + case OAUTH2: + return ctx.getOAuth2EdgeProcessor().convertOAuth2EventToDownlink(edgeEvent); default: log.warn("[{}] Unsupported edge event type [{}]", this.tenantId, edgeEvent); return null; @@ -855,7 +841,7 @@ public final class EdgeGrpcSession implements Closeable { .build(); } String error = "Failed to validate the edge!"; - String failureMsg = String.format("{%s} Provided request secret: %s", error, request.getEdgeSecret()); + String failureMsg = String.format("%s Provided request secret: %s", error, request.getEdgeSecret()); ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build()); return ConnectResponseMsg.newBuilder() diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index 8583e8725e..6321991586 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -30,6 +30,10 @@ import org.thingsboard.server.service.edge.rpc.fetch.DeviceProfilesEdgeEventFetc import org.thingsboard.server.service.edge.rpc.fetch.DevicesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.EdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.EntityViewsEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.fetch.NotificationRuleEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.fetch.NotificationTargetEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.fetch.NotificationTemplateEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.fetch.OAuth2EdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.OtaPackagesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.QueuesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.RuleChainsEdgeEventFetcher; @@ -73,12 +77,16 @@ public class EdgeSyncCursor { fetchers.add(new AssetsEdgeEventFetcher(ctx.getAssetService())); fetchers.add(new EntityViewsEdgeEventFetcher(ctx.getEntityViewService())); if (fullSync) { + fetchers.add(new NotificationTemplateEdgeEventFetcher(ctx.getNotificationTemplateService())); + fetchers.add(new NotificationTargetEdgeEventFetcher(ctx.getNotificationTargetService())); + fetchers.add(new NotificationRuleEdgeEventFetcher(ctx.getNotificationRuleService())); fetchers.add(new SystemWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService())); fetchers.add(new TenantWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService())); fetchers.add(new SystemWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService())); fetchers.add(new TenantWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService())); fetchers.add(new OtaPackagesEdgeEventFetcher(ctx.getOtaPackageService())); fetchers.add(new TenantResourcesEdgeEventFetcher(ctx.getResourceService())); + fetchers.add(new OAuth2EdgeEventFetcher(ctx.getOAuth2Service())); } } @@ -98,4 +106,5 @@ public class EdgeSyncCursor { public int getCurrentIdx() { return currentIdx; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/BaseMsgConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/BaseMsgConstructorFactory.java index 5253e5ac33..dd76f97d8f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/BaseMsgConstructorFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/BaseMsgConstructorFactory.java @@ -31,16 +31,10 @@ public abstract class BaseMsgConstructorFactory v1Constructor; + default -> v2Constructor; + }; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/BaseDashboardMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/BaseDashboardMsgConstructor.java index a61cc1fee3..f1314d9587 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/BaseDashboardMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/BaseDashboardMsgConstructor.java @@ -28,4 +28,5 @@ public abstract class BaseDashboardMsgConstructor implements DashboardMsgConstru .setIdMSB(dashboardId.getId().getMostSignificantBits()) .setIdLSB(dashboardId.getId().getLeastSignificantBits()).build(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructor.java index 6e29ed1630..bdf4968692 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructor.java @@ -26,4 +26,5 @@ public interface DashboardMsgConstructor extends MsgConstructor { DashboardUpdateMsg constructDashboardUpdatedMsg(UpdateMsgType msgType, Dashboard dashboard); DashboardUpdateMsg constructDashboardDeleteMsg(DashboardId dashboardId); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorV2.java index 9af71e8150..d1fb5ae703 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorV2.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorV2.java @@ -32,4 +32,5 @@ public class DashboardMsgConstructorV2 extends BaseDashboardMsgConstructor { .setIdMSB(dashboard.getId().getId().getMostSignificantBits()) .setIdLSB(dashboard.getId().getId().getLeastSignificantBits()).build(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructor.java index 2afdeacc67..854d0c80a8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructor.java @@ -43,4 +43,5 @@ public interface DeviceMsgConstructor extends MsgConstructor { DeviceProfileUpdateMsg constructDeviceProfileDeleteMsg(DeviceProfileId deviceProfileId); DeviceRpcCallMsg constructDeviceRpcCallMsg(UUID deviceId, JsonNode body); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorV2.java index 3d4e20ab3c..cf951e39a9 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorV2.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorV2.java @@ -48,4 +48,5 @@ public class DeviceMsgConstructorV2 extends BaseDeviceMsgConstructor { .setIdMSB(deviceProfile.getId().getId().getMostSignificantBits()) .setIdLSB(deviceProfile.getId().getId().getLeastSignificantBits()).build(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/edge/EdgeMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/edge/EdgeMsgConstructor.java index 81b1df7ac9..4b45b0427f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/edge/EdgeMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/edge/EdgeMsgConstructor.java @@ -43,4 +43,5 @@ public class EdgeMsgConstructor { } return builder.build(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/BaseEntityViewMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/BaseEntityViewMsgConstructor.java index ec650849e1..7fa4a03c3e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/BaseEntityViewMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/BaseEntityViewMsgConstructor.java @@ -28,4 +28,5 @@ public abstract class BaseEntityViewMsgConstructor implements EntityViewMsgConst .setIdMSB(entityViewId.getId().getMostSignificantBits()) .setIdLSB(entityViewId.getId().getLeastSignificantBits()).build(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructor.java index 69ce0f6770..bec1d416d3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructor.java @@ -26,4 +26,5 @@ public interface EntityViewMsgConstructor extends MsgConstructor { EntityViewUpdateMsg constructEntityViewUpdatedMsg(UpdateMsgType msgType, EntityView entityView); EntityViewUpdateMsg constructEntityViewDeleteMsg(EntityViewId entityViewId); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV1.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV1.java index feb0ab1d35..1b3bba22c0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV1.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV1.java @@ -51,13 +51,11 @@ public class EntityViewMsgConstructorV1 extends BaseEntityViewMsgConstructor { } private EdgeEntityType checkEntityType(EntityType entityType) { - switch (entityType) { - case DEVICE: - return EdgeEntityType.DEVICE; - case ASSET: - return EdgeEntityType.ASSET; - default: - throw new RuntimeException("Unsupported entity type [" + entityType + "]"); - } + return switch (entityType) { + case DEVICE -> EdgeEntityType.DEVICE; + case ASSET -> EdgeEntityType.ASSET; + default -> throw new RuntimeException("Unsupported entity type [" + entityType + "]"); + }; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV2.java index ba793f621b..6db4818688 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV2.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV2.java @@ -32,4 +32,5 @@ public class EntityViewMsgConstructorV2 extends BaseEntityViewMsgConstructor { .setIdMSB(entityView.getId().getId().getMostSignificantBits()) .setIdLSB(entityView.getId().getId().getLeastSignificantBits()).build(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/notification/NotificationMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/notification/NotificationMsgConstructor.java new file mode 100644 index 0000000000..9c8f5a37e6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/notification/NotificationMsgConstructor.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.notification; + +import org.thingsboard.server.common.data.id.NotificationRuleId; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.NotificationTemplateId; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTargetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTemplateUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; + +public interface NotificationMsgConstructor { + + NotificationRuleUpdateMsg constructNotificationRuleUpdateMsg(UpdateMsgType msgType, NotificationRule notificationRule); + + NotificationRuleUpdateMsg constructNotificationRuleDeleteMsg(NotificationRuleId notificationRuleId); + + NotificationTargetUpdateMsg constructNotificationTargetUpdateMsg(UpdateMsgType msgType, NotificationTarget notificationTarget); + + NotificationTargetUpdateMsg constructNotificationTargetDeleteMsg(NotificationTargetId notificationTargetId); + + NotificationTemplateUpdateMsg constructNotificationTemplateUpdateMsg(UpdateMsgType msgType, NotificationTemplate notificationTemplate); + + NotificationTemplateUpdateMsg constructNotificationTemplateDeleteMsg(NotificationTemplateId notificationTemplateId); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/notification/NotificationMsgConstructorImpl.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/notification/NotificationMsgConstructorImpl.java new file mode 100644 index 0000000000..8340bee693 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/notification/NotificationMsgConstructorImpl.java @@ -0,0 +1,75 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.notification; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.NotificationRuleId; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.NotificationTemplateId; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTargetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTemplateUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class NotificationMsgConstructorImpl implements NotificationMsgConstructor { + + @Override + public NotificationRuleUpdateMsg constructNotificationRuleUpdateMsg(UpdateMsgType msgType, NotificationRule notificationRule) { + return NotificationRuleUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(notificationRule)).build(); + } + + @Override + public NotificationRuleUpdateMsg constructNotificationRuleDeleteMsg(NotificationRuleId notificationRuleId) { + return NotificationRuleUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(notificationRuleId.getId().getMostSignificantBits()) + .setIdLSB(notificationRuleId.getId().getLeastSignificantBits()).build(); + } + + @Override + public NotificationTargetUpdateMsg constructNotificationTargetUpdateMsg(UpdateMsgType msgType, NotificationTarget notificationTarget) { + return NotificationTargetUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(notificationTarget)).build(); + } + + @Override + public NotificationTargetUpdateMsg constructNotificationTargetDeleteMsg(NotificationTargetId notificationTargetId) { + return NotificationTargetUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(notificationTargetId.getId().getMostSignificantBits()) + .setIdLSB(notificationTargetId.getId().getLeastSignificantBits()).build(); + } + + @Override + public NotificationTemplateUpdateMsg constructNotificationTemplateUpdateMsg(UpdateMsgType msgType, NotificationTemplate notificationTemplate) { + return NotificationTemplateUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(notificationTemplate)).build(); + } + + @Override + public NotificationTemplateUpdateMsg constructNotificationTemplateDeleteMsg(NotificationTemplateId notificationTemplateId) { + return NotificationTemplateUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(notificationTemplateId.getId().getMostSignificantBits()) + .setIdLSB(notificationTemplateId.getId().getLeastSignificantBits()).build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/oauth2/OAuth2MsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/oauth2/OAuth2MsgConstructor.java new file mode 100644 index 0000000000..c628b042a2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/oauth2/OAuth2MsgConstructor.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.oauth2; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.oauth2.OAuth2Info; +import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class OAuth2MsgConstructor { + + public OAuth2UpdateMsg constructOAuth2UpdateMsg(OAuth2Info oAuth2Info) { + return OAuth2UpdateMsg.newBuilder().setEntity(JacksonUtil.toString(oAuth2Info)).build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java index 0e48077703..5554680dfe 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java @@ -62,4 +62,5 @@ public class AdminSettingsEdgeEventFetcher implements EdgeEventFetcher { } return result; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetProfilesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetProfilesEdgeEventFetcher.java index 240203a7e2..ec9e1f5bd9 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetProfilesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetProfilesEdgeEventFetcher.java @@ -35,7 +35,7 @@ public class AssetProfilesEdgeEventFetcher extends BasePageableEdgeEventFetcher< private final AssetProfileService assetProfileService; @Override - PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return assetProfileService.findAssetProfiles(tenantId, pageLink); } @@ -44,4 +44,5 @@ public class AssetProfilesEdgeEventFetcher extends BasePageableEdgeEventFetcher< return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ASSET_PROFILE, EdgeEventActionType.ADDED, assetProfile.getId(), null); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetsEdgeEventFetcher.java index 01765c4196..7f896deb50 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetsEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetsEdgeEventFetcher.java @@ -35,7 +35,7 @@ public class AssetsEdgeEventFetcher extends BasePageableEdgeEventFetcher private final AssetService assetService; @Override - PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return assetService.findAssetsByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java index 5a5f842eaa..747dc5722f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java @@ -36,17 +36,18 @@ public abstract class BasePageableEdgeEventFetcher implements EdgeEventFetche @Override public PageData fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) { log.trace("[{}] start fetching edge events [{}]", tenantId, edge.getId()); - PageData pageData = fetchPageData(tenantId, edge, pageLink); + PageData entities = fetchEntities(tenantId, edge, pageLink); List result = new ArrayList<>(); - if (!pageData.getData().isEmpty()) { - for (T entity : pageData.getData()) { + if (!entities.getData().isEmpty()) { + for (T entity : entities.getData()) { result.add(constructEdgeEvent(tenantId, edge, entity)); } } - return new PageData<>(result, pageData.getTotalPages(), pageData.getTotalElements(), pageData.hasNext()); + return new PageData<>(result, entities.getTotalPages(), entities.getTotalElements(), entities.hasNext()); } - abstract PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink); + abstract PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink); abstract EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, T entity); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseUsersEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseUsersEdgeEventFetcher.java index b39d7e1209..51970c8556 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseUsersEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseUsersEdgeEventFetcher.java @@ -35,7 +35,7 @@ public abstract class BaseUsersEdgeEventFetcher extends BasePageableEdgeEventFet protected final UserService userService; @Override - PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return findUsers(tenantId, pageLink); } @@ -46,4 +46,5 @@ public abstract class BaseUsersEdgeEventFetcher extends BasePageableEdgeEventFet } protected abstract PageData findUsers(TenantId tenantId, PageLink pageLink); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetTypesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetTypesEdgeEventFetcher.java index cd6cb3fe7f..969ef98c7f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetTypesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetTypesEdgeEventFetcher.java @@ -25,11 +25,8 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; -import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.widget.WidgetTypeService; -import org.thingsboard.server.dao.widget.WidgetsBundleService; @Slf4j @AllArgsConstructor @@ -38,7 +35,7 @@ public abstract class BaseWidgetTypesEdgeEventFetcher extends BasePageableEdgeEv protected final WidgetTypeService widgetTypeService; @Override - PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return findWidgetTypes(tenantId, pageLink); } @@ -49,4 +46,5 @@ public abstract class BaseWidgetTypesEdgeEventFetcher extends BasePageableEdgeEv } protected abstract PageData findWidgetTypes(TenantId tenantId, PageLink pageLink); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetsBundlesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetsBundlesEdgeEventFetcher.java index c14ad7821a..b12ec3fb79 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetsBundlesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetsBundlesEdgeEventFetcher.java @@ -35,7 +35,7 @@ public abstract class BaseWidgetsBundlesEdgeEventFetcher extends BasePageableEdg protected final WidgetsBundleService widgetsBundleService; @Override - PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return findWidgetsBundles(tenantId, pageLink); } @@ -46,4 +46,5 @@ public abstract class BaseWidgetsBundlesEdgeEventFetcher extends BasePageableEdg } protected abstract PageData findWidgetsBundles(TenantId tenantId, PageLink pageLink); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerEdgeEventFetcher.java index 57596a76de..fa9fcf0d8a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerEdgeEventFetcher.java @@ -46,7 +46,8 @@ public class CustomerEdgeEventFetcher implements EdgeEventFetcher { List result = new ArrayList<>(); result.add(EdgeUtils.constructEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.CUSTOMER, EdgeEventActionType.ADDED, customerId, null)); - // @voba - returns PageData object to be in sync with other fetchers + // returns PageData object to be in sync with other fetchers return new PageData<>(result, 1, result.size(), false); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerUsersEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerUsersEdgeEventFetcher.java index da278e8053..a1c1752496 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerUsersEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerUsersEdgeEventFetcher.java @@ -35,4 +35,5 @@ public class CustomerUsersEdgeEventFetcher extends BaseUsersEdgeEventFetcher { protected PageData findUsers(TenantId tenantId, PageLink pageLink) { return userService.findCustomerUsers(tenantId, customerId, pageLink); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DashboardsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DashboardsEdgeEventFetcher.java index 16f4749478..663ada149b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DashboardsEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DashboardsEdgeEventFetcher.java @@ -35,7 +35,7 @@ public class DashboardsEdgeEventFetcher extends BasePageableEdgeEventFetcher fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return dashboardService.findDashboardsByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); } @@ -44,4 +44,5 @@ public class DashboardsEdgeEventFetcher extends BasePageableEdgeEventFetcher(result, 1, result.size(), false); } -} \ No newline at end of file + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DeviceProfilesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DeviceProfilesEdgeEventFetcher.java index 8852850ca1..f86eb23f7f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DeviceProfilesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DeviceProfilesEdgeEventFetcher.java @@ -35,7 +35,7 @@ public class DeviceProfilesEdgeEventFetcher extends BasePageableEdgeEventFetcher private final DeviceProfileService deviceProfileService; @Override - PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return deviceProfileService.findDeviceProfiles(tenantId, pageLink); } @@ -44,4 +44,5 @@ public class DeviceProfilesEdgeEventFetcher extends BasePageableEdgeEventFetcher return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE_PROFILE, EdgeEventActionType.ADDED, deviceProfile.getId(), null); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DevicesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DevicesEdgeEventFetcher.java index 252373cfc1..f62f8edf97 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DevicesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DevicesEdgeEventFetcher.java @@ -35,7 +35,7 @@ public class DevicesEdgeEventFetcher extends BasePageableEdgeEventFetcher fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return deviceService.findDevicesByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); } @@ -44,4 +44,5 @@ public class DevicesEdgeEventFetcher extends BasePageableEdgeEventFetcher fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) throws Exception; + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/EntityViewsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/EntityViewsEdgeEventFetcher.java index 81e5c537dd..c32ab62229 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/EntityViewsEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/EntityViewsEdgeEventFetcher.java @@ -35,7 +35,7 @@ public class EntityViewsEdgeEventFetcher extends BasePageableEdgeEventFetcher fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return entityViewService.findEntityViewsByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); } @@ -44,4 +44,5 @@ public class EntityViewsEdgeEventFetcher extends BasePageableEdgeEventFetcher(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationRuleEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationRuleEdgeEventFetcher.java new file mode 100644 index 0000000000..de4c34f600 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationRuleEdgeEventFetcher.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.fetch; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.notification.NotificationRuleService; + +@AllArgsConstructor +@Slf4j +public class NotificationRuleEdgeEventFetcher extends BasePageableEdgeEventFetcher{ + + private NotificationRuleService notificationRuleService; + + @Override + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { + return notificationRuleService.findNotificationRulesByTenantId(tenantId, pageLink); + } + + @Override + EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, NotificationRule notificationRule) { + return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.NOTIFICATION_RULE, + EdgeEventActionType.ADDED, notificationRule.getId(), null); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationTargetEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationTargetEdgeEventFetcher.java new file mode 100644 index 0000000000..ed59b375ab --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationTargetEdgeEventFetcher.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.fetch; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.notification.NotificationTargetService; + +@AllArgsConstructor +@Slf4j +public class NotificationTargetEdgeEventFetcher extends BasePageableEdgeEventFetcher { + + private NotificationTargetService notificationTargetService; + + @Override + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { + return notificationTargetService.findNotificationTargetsByTenantId(tenantId, pageLink); + } + + @Override + EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, NotificationTarget notificationTarget) { + return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.NOTIFICATION_TARGET, + EdgeEventActionType.ADDED, notificationTarget.getId(), null); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationTemplateEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationTemplateEdgeEventFetcher.java new file mode 100644 index 0000000000..fefff805d3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationTemplateEdgeEventFetcher.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.fetch; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.notification.NotificationTemplateService; + +import java.util.List; + +@AllArgsConstructor +@Slf4j +public class NotificationTemplateEdgeEventFetcher extends BasePageableEdgeEventFetcher { + + private NotificationTemplateService notificationTemplateService; + + @Override + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { + return notificationTemplateService.findNotificationTemplatesByTenantIdAndNotificationTypes(tenantId, List.of(NotificationType.values()), pageLink); + } + + @Override + EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, NotificationTemplate notificationTemplate) { + return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.NOTIFICATION_TEMPLATE, + EdgeEventActionType.ADDED, notificationTemplate.getId(), null); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java new file mode 100644 index 0000000000..4412f91183 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.fetch; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.OAuth2Info; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.oauth2.OAuth2Service; + +import java.util.ArrayList; +import java.util.List; + +@AllArgsConstructor +@Slf4j +public class OAuth2EdgeEventFetcher implements EdgeEventFetcher { + + private final OAuth2Service oAuth2Service; + + @Override + public PageLink getPageLink(int pageSize) { + return null; + } + + @Override + public PageData fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) { + List result = new ArrayList<>(); + OAuth2Info oAuth2Info = oAuth2Service.findOAuth2Info(); + result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.OAUTH2, + EdgeEventActionType.ADDED, null, JacksonUtil.valueToTree(oAuth2Info))); + // returns PageData object to be in sync with other fetchers + return new PageData<>(result, 1, result.size(), false); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OtaPackagesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OtaPackagesEdgeEventFetcher.java index 0330a9fec9..9f60c1ebd5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OtaPackagesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OtaPackagesEdgeEventFetcher.java @@ -35,7 +35,7 @@ public class OtaPackagesEdgeEventFetcher extends BasePageableEdgeEventFetcher fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return otaPackageService.findTenantOtaPackagesByTenantId(tenantId, pageLink); } @@ -44,4 +44,5 @@ public class OtaPackagesEdgeEventFetcher extends BasePageableEdgeEventFetcher private final QueueService queueService; @Override - PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return queueService.findQueuesByTenantId(tenantId, pageLink); } @@ -44,4 +44,5 @@ public class QueuesEdgeEventFetcher extends BasePageableEdgeEventFetcher return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.QUEUE, EdgeEventActionType.ADDED, queue.getId(), null); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java index d6e48ec6bd..5569c30018 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java @@ -39,7 +39,7 @@ public class RuleChainsEdgeEventFetcher extends BasePageableEdgeEventFetcher fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return ruleChainService.findRuleChainsByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); } @@ -54,4 +54,5 @@ public class RuleChainsEdgeEventFetcher extends BasePageableEdgeEventFetcher private final TenantService tenantService; @Override - PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { Tenant tenant = tenantService.findTenantById(tenantId); // returns PageData object to be in sync with other fetchers return new PageData<>(List.of(tenant), 1, 1, false); @@ -48,4 +48,5 @@ public class TenantEdgeEventFetcher extends BasePageableEdgeEventFetcher return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.TENANT, EdgeEventActionType.UPDATED, entity.getId(), null); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantResourcesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantResourcesEdgeEventFetcher.java index eef428bf8c..5dcf8d4cec 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantResourcesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantResourcesEdgeEventFetcher.java @@ -35,7 +35,7 @@ public class TenantResourcesEdgeEventFetcher extends BasePageableEdgeEventFetche private final ResourceService resourceService; @Override - PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return resourceService.findAllTenantResources(tenantId, pageLink); } @@ -44,4 +44,5 @@ public class TenantResourcesEdgeEventFetcher extends BasePageableEdgeEventFetche return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.TB_RESOURCE, EdgeEventActionType.ADDED, tbResource.getId(), null); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index cd3eea4bef..f46c07ab5a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -25,7 +25,6 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Dashboard; -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.EdgeUtils; @@ -51,9 +50,8 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChain; @@ -75,6 +73,10 @@ import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.notification.NotificationRuleService; +import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.dao.notification.NotificationTemplateService; +import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; @@ -93,7 +95,6 @@ import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; -import org.thingsboard.server.service.entitiy.TbLogEntityActionService; import org.thingsboard.server.service.edge.rpc.constructor.alarm.AlarmMsgConstructorFactory; import org.thingsboard.server.service.edge.rpc.constructor.asset.AssetMsgConstructorFactory; import org.thingsboard.server.service.edge.rpc.constructor.customer.CustomerMsgConstructorFactory; @@ -101,6 +102,8 @@ import org.thingsboard.server.service.edge.rpc.constructor.dashboard.DashboardMs import org.thingsboard.server.service.edge.rpc.constructor.device.DeviceMsgConstructorFactory; import org.thingsboard.server.service.edge.rpc.constructor.edge.EdgeMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.entityview.EntityViewMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.notification.NotificationMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.oauth2.OAuth2MsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.ota.OtaPackageMsgConstructorFactory; import org.thingsboard.server.service.edge.rpc.constructor.queue.QueueMsgConstructorFactory; import org.thingsboard.server.service.edge.rpc.constructor.relation.RelationMsgConstructorFactory; @@ -114,6 +117,7 @@ import org.thingsboard.server.service.edge.rpc.constructor.widget.WidgetMsgConst import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmEdgeProcessorFactory; import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessorFactory; import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewProcessorFactory; +import org.thingsboard.server.service.entitiy.TbLogEntityActionService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; @@ -226,6 +230,18 @@ public abstract class BaseEdgeProcessor { @Autowired protected ResourceService resourceService; + @Autowired + protected NotificationRuleService notificationRuleService; + + @Autowired + protected NotificationTargetService notificationTargetService; + + @Autowired + protected NotificationTemplateService notificationTemplateService; + + @Autowired + protected OAuth2Service oAuth2Service; + @Autowired @Lazy protected TbQueueProducerProvider producerProvider; @@ -257,6 +273,13 @@ public abstract class BaseEdgeProcessor { @Autowired protected EntityDataMsgConstructor entityDataMsgConstructor; + @Autowired + protected NotificationMsgConstructor notificationMsgConstructor; + + @Autowired + protected OAuth2MsgConstructor oAuth2MsgConstructor; + + @Autowired protected RuleChainMsgConstructorFactory ruleChainMsgConstructorFactory; @@ -349,35 +372,16 @@ public abstract class BaseEdgeProcessor { private boolean doSaveIfEdgeIsOffline(EdgeEventType type, EdgeEventActionType action) { - switch (action) { - case TIMESERIES_UPDATED: - case ALARM_ACK: - case ALARM_CLEAR: - case ALARM_ASSIGNED: - case ALARM_UNASSIGNED: - case CREDENTIALS_REQUEST: - case ADDED_COMMENT: - case UPDATED_COMMENT: - return true; - } - switch (type) { - case ALARM: - case ALARM_COMMENT: - case RULE_CHAIN: - case RULE_CHAIN_METADATA: - case USER: - case CUSTOMER: - case TENANT: - case TENANT_PROFILE: - case WIDGETS_BUNDLE: - case WIDGET_TYPE: - case ADMIN_SETTINGS: - case OTA_PACKAGE: - case QUEUE: - case RELATION: - return true; - } - return false; + return switch (action) { + case TIMESERIES_UPDATED, ALARM_ACK, ALARM_CLEAR, ALARM_ASSIGNED, ALARM_UNASSIGNED, CREDENTIALS_REQUEST, ADDED_COMMENT, UPDATED_COMMENT -> + true; + default -> switch (type) { + case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE, WIDGETS_BUNDLE, WIDGET_TYPE, + ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, NOTIFICATION_TEMPLATE, NOTIFICATION_TARGET, NOTIFICATION_RULE -> + true; + default -> false; + }; + }; } private ListenableFuture doSaveEdgeEvent(TenantId tenantId, EdgeId edgeId, EdgeEventType type, EdgeEventActionType action, EntityId entityId, JsonNode body) { @@ -392,18 +396,13 @@ public abstract class BaseEdgeProcessor { protected ListenableFuture processActionForAllEdges(TenantId tenantId, EdgeEventType type, EdgeEventActionType actionType, EntityId entityId, - EdgeId sourceEdgeId) { + JsonNode body, EdgeId sourceEdgeId) { List> futures = new ArrayList<>(); if (TenantId.SYS_TENANT_ID.equals(tenantId)) { - PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); - PageData tenantsIds; - do { - tenantsIds = tenantService.findTenantsIds(pageLink); - for (TenantId tenantId1 : tenantsIds.getData()) { - futures.addAll(processActionForAllEdgesByTenantId(tenantId1, type, actionType, entityId, null, sourceEdgeId)); - } - pageLink = pageLink.nextPageLink(); - } while (tenantsIds.hasNext()); + PageDataIterable tenantIds = new PageDataIterable<>(link -> tenantService.findTenantsIds(link), 1024); + for (TenantId tenantId1 : tenantIds) { + futures.addAll(processActionForAllEdgesByTenantId(tenantId1, type, actionType, entityId, body, sourceEdgeId)); + } } else { futures = processActionForAllEdgesByTenantId(tenantId, type, actionType, entityId, null, sourceEdgeId); } @@ -416,22 +415,13 @@ public abstract class BaseEdgeProcessor { EntityId entityId, JsonNode body, EdgeId sourceEdgeId) { - PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); - PageData pageData; List> futures = new ArrayList<>(); - do { - pageData = edgeService.findEdgesByTenantId(tenantId, pageLink); - if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { - for (Edge edge : pageData.getData()) { - if (!edge.getId().equals(sourceEdgeId)) { - futures.add(saveEdgeEvent(tenantId, edge.getId(), type, actionType, entityId, body)); - } - } - if (pageData.hasNext()) { - pageLink = pageLink.nextPageLink(); - } + PageDataIterable edges = new PageDataIterable<>(link -> edgeService.findEdgesByTenantId(tenantId, link), 1024); + for (Edge edge : edges) { + if (!edge.getId().equals(sourceEdgeId)) { + futures.add(saveEdgeEvent(tenantId, edge.getId(), type, actionType, entityId, body)); } - } while (pageData != null && pageData.hasNext()); + } return futures; } @@ -442,31 +432,15 @@ public abstract class BaseEdgeProcessor { } protected UpdateMsgType getUpdateMsgType(EdgeEventActionType actionType) { - switch (actionType) { - case UPDATED: - case CREDENTIALS_UPDATED: - case ASSIGNED_TO_CUSTOMER: - case UNASSIGNED_FROM_CUSTOMER: - case UPDATED_COMMENT: - return UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE; - case ADDED: - case ASSIGNED_TO_EDGE: - case RELATION_ADD_OR_UPDATE: - case ADDED_COMMENT: - return UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE; - case DELETED: - case UNASSIGNED_FROM_EDGE: - case RELATION_DELETED: - case DELETED_COMMENT: - case ALARM_DELETE: - return UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE; - case ALARM_ACK: - return UpdateMsgType.ALARM_ACK_RPC_MESSAGE; - case ALARM_CLEAR: - return UpdateMsgType.ALARM_CLEAR_RPC_MESSAGE; - default: - throw new RuntimeException("Unsupported actionType [" + actionType + "]"); - } + return switch (actionType) { + case UPDATED, CREDENTIALS_UPDATED, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, UPDATED_COMMENT -> + UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE; + case ADDED, ASSIGNED_TO_EDGE, RELATION_ADD_OR_UPDATE, ADDED_COMMENT -> UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE; + case DELETED, UNASSIGNED_FROM_EDGE, RELATION_DELETED, DELETED_COMMENT, ALARM_DELETE -> UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE; + case ALARM_ACK -> UpdateMsgType.ALARM_ACK_RPC_MESSAGE; + case ALARM_CLEAR -> UpdateMsgType.ALARM_CLEAR_RPC_MESSAGE; + default -> throw new RuntimeException("Unsupported actionType [" + actionType + "]"); + }; } public ListenableFuture processEntityNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { @@ -539,74 +513,52 @@ public abstract class BaseEdgeProcessor { } private ListenableFuture updateDependentRuleChains(TenantId tenantId, RuleChainId processingRuleChainId, EdgeId edgeId) { - PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); - PageData pageData; List> futures = new ArrayList<>(); - do { - pageData = ruleChainService.findRuleChainsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); - if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { - for (RuleChain ruleChain : pageData.getData()) { - if (!ruleChain.getId().equals(processingRuleChainId)) { - List connectionInfos = - ruleChainService.loadRuleChainMetaData(ruleChain.getTenantId(), ruleChain.getId()).getRuleChainConnections(); - if (connectionInfos != null && !connectionInfos.isEmpty()) { - for (RuleChainConnectionInfo connectionInfo : connectionInfos) { - if (connectionInfo.getTargetRuleChainId().equals(processingRuleChainId)) { - futures.add(saveEdgeEvent(tenantId, - edgeId, - EdgeEventType.RULE_CHAIN_METADATA, - EdgeEventActionType.UPDATED, - ruleChain.getId(), - null)); - } - } - } + PageDataIterable ruleChains = new PageDataIterable<>(link -> ruleChainService.findRuleChainsByTenantIdAndEdgeId(tenantId, edgeId, link), 1024); + for (RuleChain ruleChain : ruleChains) { + List connectionInfos = + ruleChainService.loadRuleChainMetaData(ruleChain.getTenantId(), ruleChain.getId()).getRuleChainConnections(); + if (connectionInfos != null && !connectionInfos.isEmpty()) { + for (RuleChainConnectionInfo connectionInfo : connectionInfos) { + if (connectionInfo.getTargetRuleChainId().equals(processingRuleChainId)) { + futures.add(saveEdgeEvent(tenantId, + edgeId, + EdgeEventType.RULE_CHAIN_METADATA, + EdgeEventActionType.UPDATED, + ruleChain.getId(), + null)); } } - if (pageData.hasNext()) { - pageLink = pageLink.nextPageLink(); - } } - } while (pageData != null && pageData.hasNext()); + } return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); } private ListenableFuture processEntityNotificationForAllEdges(TenantId tenantId, EdgeEventType type, EdgeEventActionType actionType, EntityId entityId, EdgeId sourceEdgeId) { - switch (actionType) { - case ADDED: - case UPDATED: - case DELETED: - case CREDENTIALS_UPDATED: // used by USER entity - return processActionForAllEdges(tenantId, type, actionType, entityId, sourceEdgeId); - default: - return Futures.immediateFuture(null); - } + return switch (actionType) { + case ADDED, UPDATED, DELETED, CREDENTIALS_UPDATED -> // used by USER entity + processActionForAllEdges(tenantId, type, actionType, entityId, null, sourceEdgeId); + default -> Futures.immediateFuture(null); + }; } protected EntityId constructEntityId(String entityTypeStr, long entityIdMSB, long entityIdLSB) { EntityType entityType = EntityType.valueOf(entityTypeStr); - switch (entityType) { - case DEVICE: - return new DeviceId(new UUID(entityIdMSB, entityIdLSB)); - case ASSET: - return new AssetId(new UUID(entityIdMSB, entityIdLSB)); - case ENTITY_VIEW: - return new EntityViewId(new UUID(entityIdMSB, entityIdLSB)); - case DASHBOARD: - return new DashboardId(new UUID(entityIdMSB, entityIdLSB)); - case TENANT: - return TenantId.fromUUID(new UUID(entityIdMSB, entityIdLSB)); - case CUSTOMER: - return new CustomerId(new UUID(entityIdMSB, entityIdLSB)); - case USER: - return new UserId(new UUID(entityIdMSB, entityIdLSB)); - case EDGE: - return new EdgeId(new UUID(entityIdMSB, entityIdLSB)); - default: + return switch (entityType) { + case DEVICE -> new DeviceId(new UUID(entityIdMSB, entityIdLSB)); + case ASSET -> new AssetId(new UUID(entityIdMSB, entityIdLSB)); + case ENTITY_VIEW -> new EntityViewId(new UUID(entityIdMSB, entityIdLSB)); + case DASHBOARD -> new DashboardId(new UUID(entityIdMSB, entityIdLSB)); + case TENANT -> TenantId.fromUUID(new UUID(entityIdMSB, entityIdLSB)); + case CUSTOMER -> new CustomerId(new UUID(entityIdMSB, entityIdLSB)); + case USER -> new UserId(new UUID(entityIdMSB, entityIdLSB)); + case EDGE -> new EdgeId(new UUID(entityIdMSB, entityIdLSB)); + default -> { log.warn("Unsupported entity type [{}] during construct of entity id. entityIdMSB [{}], entityIdLSB [{}]", entityTypeStr, entityIdMSB, entityIdLSB); - return null; - } + yield null; + } + }; } protected UUID safeGetUUID(long mSB, long lSB) { @@ -623,26 +575,17 @@ public abstract class BaseEdgeProcessor { } protected boolean isEntityExists(TenantId tenantId, EntityId entityId) { - switch (entityId.getEntityType()) { - case TENANT: - return tenantService.findTenantById(tenantId) != null; - case DEVICE: - return deviceService.findDeviceById(tenantId, new DeviceId(entityId.getId())) != null; - case ASSET: - return assetService.findAssetById(tenantId, new AssetId(entityId.getId())) != null; - case ENTITY_VIEW: - return entityViewService.findEntityViewById(tenantId, new EntityViewId(entityId.getId())) != null; - case CUSTOMER: - return customerService.findCustomerById(tenantId, new CustomerId(entityId.getId())) != null; - case USER: - return userService.findUserById(tenantId, new UserId(entityId.getId())) != null; - case DASHBOARD: - return dashboardService.findDashboardById(tenantId, new DashboardId(entityId.getId())) != null; - case EDGE: - return edgeService.findEdgeById(tenantId, new EdgeId(entityId.getId())) != null; - default: - return false; - } + return switch (entityId.getEntityType()) { + case TENANT -> tenantService.findTenantById(tenantId) != null; + case DEVICE -> deviceService.findDeviceById(tenantId, new DeviceId(entityId.getId())) != null; + case ASSET -> assetService.findAssetById(tenantId, new AssetId(entityId.getId())) != null; + case ENTITY_VIEW -> entityViewService.findEntityViewById(tenantId, new EntityViewId(entityId.getId())) != null; + case CUSTOMER -> customerService.findCustomerById(tenantId, new CustomerId(entityId.getId())) != null; + case USER -> userService.findUserById(tenantId, new UserId(entityId.getId())) != null; + case DASHBOARD -> dashboardService.findDashboardById(tenantId, new DashboardId(entityId.getId())) != null; + case EDGE -> edgeService.findEdgeById(tenantId, new EdgeId(entityId.getId())) != null; + default -> false; + }; } protected void createRelationFromEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId) { @@ -681,37 +624,25 @@ public abstract class BaseEdgeProcessor { } protected AssetProfile checkIfAssetProfileDefaultFieldsAssignedToEdge(TenantId tenantId, EdgeId edgeId, AssetProfile assetProfile, EdgeVersion edgeVersion) { - switch (edgeVersion) { - case V_3_3_3: - case V_3_3_0: - case V_3_4_0: - if (assetProfile.getDefaultDashboardId() != null - && isEntityNotAssignedToEdge(tenantId, assetProfile.getDefaultDashboardId(), edgeId)) { - assetProfile.setDefaultDashboardId(null); - } - if (assetProfile.getDefaultEdgeRuleChainId() != null - && isEntityNotAssignedToEdge(tenantId, assetProfile.getDefaultEdgeRuleChainId(), edgeId)) { - assetProfile.setDefaultEdgeRuleChainId(null); - } - break; + if (EdgeVersion.V_3_3_0.equals(edgeVersion) || EdgeVersion.V_3_3_3.equals(edgeVersion) || EdgeVersion.V_3_4_0.equals(edgeVersion)) { + if (assetProfile.getDefaultDashboardId() != null && isEntityNotAssignedToEdge(tenantId, assetProfile.getDefaultDashboardId(), edgeId)) { + assetProfile.setDefaultDashboardId(null); + } + if (assetProfile.getDefaultEdgeRuleChainId() != null && isEntityNotAssignedToEdge(tenantId, assetProfile.getDefaultEdgeRuleChainId(), edgeId)) { + assetProfile.setDefaultEdgeRuleChainId(null); + } } return assetProfile; } protected DeviceProfile checkIfDeviceProfileDefaultFieldsAssignedToEdge(TenantId tenantId, EdgeId edgeId, DeviceProfile deviceProfile, EdgeVersion edgeVersion) { - switch (edgeVersion) { - case V_3_3_3: - case V_3_3_0: - case V_3_4_0: - if (deviceProfile.getDefaultDashboardId() != null - && isEntityNotAssignedToEdge(tenantId, deviceProfile.getDefaultDashboardId(), edgeId)) { - deviceProfile.setDefaultDashboardId(null); - } - if (deviceProfile.getDefaultEdgeRuleChainId() != null - && isEntityNotAssignedToEdge(tenantId, deviceProfile.getDefaultEdgeRuleChainId(), edgeId)) { - deviceProfile.setDefaultEdgeRuleChainId(null); - } - break; + if (EdgeVersion.V_3_3_0.equals(edgeVersion) || EdgeVersion.V_3_3_3.equals(edgeVersion) || EdgeVersion.V_3_4_0.equals(edgeVersion)) { + if (deviceProfile.getDefaultDashboardId() != null && isEntityNotAssignedToEdge(tenantId, deviceProfile.getDefaultDashboardId(), edgeId)) { + deviceProfile.setDefaultDashboardId(null); + } + if (deviceProfile.getDefaultEdgeRuleChainId() != null && isEntityNotAssignedToEdge(tenantId, deviceProfile.getDefaultEdgeRuleChainId(), edgeId)) { + deviceProfile.setDefaultEdgeRuleChainId(null); + } } return deviceProfile; } @@ -726,4 +657,5 @@ public abstract class BaseEdgeProcessor { } return true; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorFactory.java index a9ec97cf64..a8ca885c7b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorFactory.java @@ -31,16 +31,10 @@ public abstract class BaseEdgeProcessorFactory v1Processor; + default -> v2Processor; + }; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java index b578325961..22d76dc86e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java @@ -156,4 +156,5 @@ public abstract class AlarmEdgeProcessor extends BaseAlarmProcessor implements A } return futures; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessorV1.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessorV1.java index 9f8738b075..30ee4efe5f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessorV1.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessorV1.java @@ -58,15 +58,12 @@ public class AlarmEdgeProcessorV1 extends AlarmEdgeProcessor { } private EntityId getAlarmOriginator(TenantId tenantId, String entityName, EntityType entityType) { - switch (entityType) { - case DEVICE: - return deviceService.findDeviceByTenantIdAndName(tenantId, entityName).getId(); - case ASSET: - return assetService.findAssetByTenantIdAndName(tenantId, entityName).getId(); - case ENTITY_VIEW: - return entityViewService.findEntityViewByTenantIdAndName(tenantId, entityName).getId(); - default: - return null; - } + return switch (entityType) { + case DEVICE -> deviceService.findDeviceByTenantIdAndName(tenantId, entityName).getId(); + case ASSET -> assetService.findAssetByTenantIdAndName(tenantId, entityName).getId(); + case ENTITY_VIEW -> entityViewService.findEntityViewByTenantIdAndName(tenantId, entityName).getId(); + default -> null; + }; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessorV2.java index a1d06e1b3b..dd49af252d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessorV2.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessorV2.java @@ -40,4 +40,5 @@ public class AlarmEdgeProcessorV2 extends AlarmEdgeProcessor { protected Alarm constructAlarmFromUpdateMsg(TenantId tenantId, AlarmId alarmId, EntityId originatorId, AlarmUpdateMsg alarmUpdateMsg) { return JacksonUtil.fromString(alarmUpdateMsg.getEntity(), Alarm.class, true); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmProcessor.java index 938269462d..d4f820b6c3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmProcessor.java @@ -34,4 +34,5 @@ public interface AlarmProcessor extends EdgeProcessor { ListenableFuture processAlarmCommentMsgFromEdge(TenantId tenantId, EdgeId edgeId, AlarmCommentUpdateMsg alarmCommentUpdateMsg); DownlinkMsg convertAlarmCommentEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java index ff2c0ca5f6..f6a05e363c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java @@ -144,23 +144,20 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { AlarmId alarmId = new AlarmId(entityId); UpdateMsgType msgType = getUpdateMsgType(actionType); switch (actionType) { - case ADDED: - case UPDATED: - case ALARM_ACK: - case ALARM_CLEAR: + case ADDED, UPDATED, ALARM_ACK, ALARM_CLEAR -> { Alarm alarm = alarmService.findAlarmById(tenantId, alarmId); if (alarm != null) { return ((AlarmMsgConstructor) alarmMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) .constructAlarmUpdatedMsg(msgType, alarm, findOriginatorEntityName(tenantId, alarm)); } - break; - case ALARM_DELETE: - case DELETED: + } + case ALARM_DELETE, DELETED -> { Alarm deletedAlarm = JacksonUtil.convertValue(body, Alarm.class); if (deletedAlarm != null) { return ((AlarmMsgConstructor) alarmMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) .constructAlarmUpdatedMsg(msgType, deletedAlarm, findOriginatorEntityName(tenantId, deletedAlarm)); } + } } return null; } @@ -168,25 +165,26 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { private String findOriginatorEntityName(TenantId tenantId, Alarm alarm) { String entityName = null; switch (alarm.getOriginator().getEntityType()) { - case DEVICE: + case DEVICE -> { Device deviceById = deviceService.findDeviceById(tenantId, new DeviceId(alarm.getOriginator().getId())); if (deviceById != null) { entityName = deviceById.getName(); } - break; - case ASSET: + } + case ASSET -> { Asset assetById = assetService.findAssetById(tenantId, new AssetId(alarm.getOriginator().getId())); if (assetById != null) { entityName = assetById.getName(); } - break; - case ENTITY_VIEW: + } + case ENTITY_VIEW -> { EntityView entityViewById = entityViewService.findEntityViewById(tenantId, new EntityViewId(alarm.getOriginator().getId())); if (entityViewById != null) { entityName = entityViewById.getName(); } - break; + } } return entityName; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java index 203c6466e0..5d264cd83d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java @@ -32,7 +32,6 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.asset.BaseAssetService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; @@ -109,13 +108,9 @@ public abstract class AssetEdgeProcessor extends BaseAssetProcessor implements A AssetId assetId = new AssetId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - case ASSIGNED_TO_EDGE: - case ASSIGNED_TO_CUSTOMER: - case UNASSIGNED_FROM_CUSTOMER: + case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { Asset asset = assetService.findAssetById(edgeEvent.getTenantId(), assetId); - if (asset != null && !BaseAssetService.TB_SERVICE_QUEUE.equals(asset.getType())) { + if (asset != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); AssetUpdateMsg assetUpdateMsg = ((AssetMsgConstructor) assetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructAssetUpdatedMsg(msgType, asset); @@ -130,17 +125,17 @@ public abstract class AssetEdgeProcessor extends BaseAssetProcessor implements A } downlinkMsg = builder.build(); } - break; - case DELETED: - case UNASSIGNED_FROM_EDGE: + } + case DELETED, UNASSIGNED_FROM_EDGE -> { AssetUpdateMsg assetUpdateMsg = ((AssetMsgConstructor) assetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructAssetDeleteMsg(assetId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addAssetUpdateMsg(assetUpdateMsg) .build(); - break; + } } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorV1.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorV1.java index e84b37cc14..8cd2de1cab 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorV1.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorV1.java @@ -56,4 +56,5 @@ public class AssetEdgeProcessorV1 extends AssetEdgeProcessor { CustomerId customerUUID = safeGetCustomerId(assetUpdateMsg.getCustomerIdMSB(), assetUpdateMsg.getCustomerIdLSB()); asset.setCustomerId(customerUUID != null ? customerUUID : customerId); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorV2.java index a8c558b88e..021507f5d7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorV2.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorV2.java @@ -40,4 +40,5 @@ public class AssetEdgeProcessorV2 extends AssetEdgeProcessor { CustomerId customerUUID = asset.getCustomerId() != null ? asset.getCustomerId() : customerId; asset.setCustomerId(customerUUID); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProcessor.java index 4d2f6ff52b..2685274bbf 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProcessor.java @@ -30,4 +30,5 @@ public interface AssetProcessor extends EdgeProcessor { ListenableFuture processAssetMsgFromEdge(TenantId tenantId, Edge edge, AssetUpdateMsg assetUpdateMsg); DownlinkMsg convertAssetEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java index e84ffb730e..2588e52d5f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java @@ -72,4 +72,5 @@ public abstract class BaseAssetProcessor extends BaseEdgeProcessor { protected abstract Asset constructAssetFromUpdateMsg(TenantId tenantId, AssetId assetId, AssetUpdateMsg assetUpdateMsg); protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, Asset asset, AssetUpdateMsg assetUpdateMsg); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java index 9678dbcbad..b4d56ed20f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java @@ -97,8 +97,7 @@ public abstract class AssetProfileEdgeProcessor extends BaseAssetProfileProcesso AssetProfileId assetProfileId = new AssetProfileId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: + case ADDED, UPDATED -> { AssetProfile assetProfile = assetProfileService.findAssetProfileById(edgeEvent.getTenantId(), assetProfileId); if (assetProfile != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -110,16 +109,17 @@ public abstract class AssetProfileEdgeProcessor extends BaseAssetProfileProcesso .addAssetProfileUpdateMsg(assetProfileUpdateMsg) .build(); } - break; - case DELETED: + } + case DELETED -> { AssetProfileUpdateMsg assetProfileUpdateMsg = ((AssetMsgConstructor) assetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructAssetProfileDeleteMsg(assetProfileId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addAssetProfileUpdateMsg(assetProfileUpdateMsg) .build(); - break; + } } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java index 46daccfbe4..e13c4c487f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java @@ -29,8 +29,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; @@ -53,8 +52,7 @@ public class CustomerEdgeProcessor extends BaseEdgeProcessor { CustomerId customerId = new CustomerId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: + case ADDED, UPDATED -> { Customer customer = customerService.findCustomerById(edgeEvent.getTenantId(), customerId); if (customer != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -65,15 +63,15 @@ public class CustomerEdgeProcessor extends BaseEdgeProcessor { .addCustomerUpdateMsg(customerUpdateMsg) .build(); } - break; - case DELETED: + } + case DELETED -> { CustomerUpdateMsg customerUpdateMsg = ((CustomerMsgConstructor) customerMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructCustomerDeleteMsg(customerId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addCustomerUpdateMsg(customerUpdateMsg) .build(); - break; + } } return downlinkMsg; } @@ -85,20 +83,11 @@ public class CustomerEdgeProcessor extends BaseEdgeProcessor { CustomerId customerId = new CustomerId(EntityIdFactory.getByEdgeEventTypeAndUuid(type, uuid).getId()); switch (actionType) { case UPDATED: - PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); - PageData pageData; List> futures = new ArrayList<>(); - do { - pageData = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); - if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { - for (Edge edge : pageData.getData()) { - futures.add(saveEdgeEvent(tenantId, edge.getId(), type, actionType, customerId, null)); - } - if (pageData.hasNext()) { - pageLink = pageLink.nextPageLink(); - } - } - } while (pageData != null && pageData.hasNext()); + PageDataIterable edges = new PageDataIterable<>(link -> edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, link), 1024); + for (Edge edge : edges) { + futures.add(saveEdgeEvent(tenantId, edge.getId(), type, actionType, customerId, null)); + } return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); case DELETED: EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB())); @@ -107,4 +96,5 @@ public class CustomerEdgeProcessor extends BaseEdgeProcessor { return Futures.immediateFuture(null); } } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java index f50565b177..aab35f89d8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java @@ -101,11 +101,7 @@ public abstract class DashboardEdgeProcessor extends BaseDashboardProcessor impl DashboardId dashboardId = new DashboardId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - case ASSIGNED_TO_EDGE: - case ASSIGNED_TO_CUSTOMER: - case UNASSIGNED_FROM_CUSTOMER: + case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { Dashboard dashboard = dashboardService.findDashboardById(edgeEvent.getTenantId(), dashboardId); if (dashboard != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -116,16 +112,15 @@ public abstract class DashboardEdgeProcessor extends BaseDashboardProcessor impl .addDashboardUpdateMsg(dashboardUpdateMsg) .build(); } - break; - case DELETED: - case UNASSIGNED_FROM_EDGE: + } + case DELETED, UNASSIGNED_FROM_EDGE -> { DashboardUpdateMsg dashboardUpdateMsg = ((DashboardMsgConstructor) dashboardMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDashboardDeleteMsg(dashboardId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDashboardUpdateMsg(dashboardUpdateMsg) .build(); - break; + } } return downlinkMsg; } @@ -135,4 +130,5 @@ public abstract class DashboardEdgeProcessor extends BaseDashboardProcessor impl // do nothing on cloud return assignedCustomers; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index 6981ad5ff3..5e73034b1c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -286,4 +286,5 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements .addDeviceCredentialsRequestMsg(deviceCredentialsRequestMsg); return builder.build(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java index 23ad24bb62..464fa3ba54 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java @@ -97,8 +97,7 @@ public abstract class DeviceProfileEdgeProcessor extends BaseDeviceProfileProces DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: + case ADDED, UPDATED -> { DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), deviceProfileId); if (deviceProfile != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -110,16 +109,17 @@ public abstract class DeviceProfileEdgeProcessor extends BaseDeviceProfileProces .addDeviceProfileUpdateMsg(deviceProfileUpdateMsg) .build(); } - break; - case DELETED: + } + case DELETED -> { DeviceProfileUpdateMsg deviceProfileUpdateMsg = ((DeviceMsgConstructor) deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceProfileDeleteMsg(deviceProfileId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceProfileUpdateMsg(deviceProfileUpdateMsg) .build(); - break; + } } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java index e4a9189aad..7bb631b330 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java @@ -50,8 +50,7 @@ public class EdgeProcessor extends BaseEdgeProcessor { EdgeId edgeId = new EdgeId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ASSIGNED_TO_CUSTOMER: - case UNASSIGNED_FROM_CUSTOMER: + case ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { Edge edge = edgeService.findEdgeById(edgeEvent.getTenantId(), edgeId); if (edge != null) { EdgeConfiguration edgeConfigMsg = @@ -61,7 +60,7 @@ public class EdgeProcessor extends BaseEdgeProcessor { .setEdgeConfiguration(edgeConfigMsg) .build(); } - break; + } } return downlinkMsg; } @@ -112,4 +111,5 @@ public class EdgeProcessor extends BaseEdgeProcessor { return Futures.immediateFailedFuture(e); } } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java index b221b1576f..9c2b9a37bd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java @@ -64,4 +64,5 @@ public abstract class BaseEntityViewProcessor extends BaseEdgeProcessor { protected abstract EntityView constructEntityViewFromUpdateMsg(TenantId tenantId, EntityViewId entityViewId, EntityViewUpdateMsg entityViewUpdateMsg); protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, EntityView entityView, EntityViewUpdateMsg entityViewUpdateMsg); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java index c85eb95e98..70d9d9686f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java @@ -105,11 +105,7 @@ public abstract class EntityViewEdgeProcessor extends BaseEntityViewProcessor im EntityViewId entityViewId = new EntityViewId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - case ASSIGNED_TO_EDGE: - case ASSIGNED_TO_CUSTOMER: - case UNASSIGNED_FROM_CUSTOMER: + case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { EntityView entityView = entityViewService.findEntityViewById(edgeEvent.getTenantId(), entityViewId); if (entityView != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -120,17 +116,17 @@ public abstract class EntityViewEdgeProcessor extends BaseEntityViewProcessor im .addEntityViewUpdateMsg(entityViewUpdateMsg) .build(); } - break; - case DELETED: - case UNASSIGNED_FROM_EDGE: + } + case DELETED, UNASSIGNED_FROM_EDGE -> { EntityViewUpdateMsg entityViewUpdateMsg = ((EntityViewMsgConstructor) entityViewMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructEntityViewDeleteMsg(entityViewId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addEntityViewUpdateMsg(entityViewUpdateMsg) .build(); - break; + } } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessor.java index 80803a2cd4..061cd89bf0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessor.java @@ -29,4 +29,5 @@ public interface EntityViewProcessor extends EdgeProcessor { ListenableFuture processEntityViewMsgFromEdge(TenantId tenantId, Edge edge, EntityViewUpdateMsg entityViewUpdateMsg); DownlinkMsg convertEntityViewEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessorV1.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessorV1.java index 3d1171cbfd..acb8877c03 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessorV1.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessorV1.java @@ -62,4 +62,5 @@ public class EntityViewProcessorV1 extends EntityViewEdgeProcessor { CustomerId customerUUID = safeGetCustomerId(entityViewUpdateMsg.getCustomerIdMSB(), entityViewUpdateMsg.getCustomerIdLSB()); entityView.setCustomerId(customerUUID != null ? customerUUID : customerId); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessorV2.java index 72356276cf..fa4177552e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessorV2.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessorV2.java @@ -40,4 +40,5 @@ public class EntityViewProcessorV2 extends EntityViewEdgeProcessor { CustomerId customerUUID = entityView.getCustomerId() != null ? entityView.getCustomerId() : customerId; entityView.setCustomerId(customerUUID); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationEdgeProcessor.java new file mode 100644 index 0000000000..6af039341f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationEdgeProcessor.java @@ -0,0 +1,119 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.notification; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.NotificationRuleId; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.NotificationTemplateId; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTargetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTemplateUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Slf4j +@Component +@TbCoreComponent +public class NotificationEdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg convertNotificationRuleToDownlink(EdgeEvent edgeEvent) { + NotificationRuleId notificationRuleId = new NotificationRuleId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ADDED, UPDATED -> { + NotificationRule notificationRule = notificationRuleService.findNotificationRuleById(edgeEvent.getTenantId(), notificationRuleId); + if (notificationRule != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + NotificationRuleUpdateMsg notificationRuleUpdateMsg = notificationMsgConstructor.constructNotificationRuleUpdateMsg(msgType, notificationRule); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addNotificationRuleUpdateMsg(notificationRuleUpdateMsg) + .build(); + } + } + case DELETED -> { + NotificationRuleUpdateMsg notificationRuleUpdateMsg = notificationMsgConstructor.constructNotificationRuleDeleteMsg(notificationRuleId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addNotificationRuleUpdateMsg(notificationRuleUpdateMsg) + .build(); + } + } + return downlinkMsg; + } + + public DownlinkMsg convertNotificationTargetToDownlink(EdgeEvent edgeEvent) { + NotificationTargetId notificationTargetId = new NotificationTargetId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ADDED, UPDATED -> { + NotificationTarget notificationTarget = notificationTargetService.findNotificationTargetById(edgeEvent.getTenantId(), notificationTargetId); + if (notificationTarget != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + NotificationTargetUpdateMsg notificationTargetUpdateMsg = notificationMsgConstructor.constructNotificationTargetUpdateMsg(msgType, notificationTarget); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addNotificationTargetUpdateMsg(notificationTargetUpdateMsg) + .build(); + } + } + case DELETED -> { + NotificationTargetUpdateMsg notificationTargetUpdateMsg = notificationMsgConstructor.constructNotificationTargetDeleteMsg(notificationTargetId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addNotificationTargetUpdateMsg(notificationTargetUpdateMsg) + .build(); + } + } + return downlinkMsg; + } + + public DownlinkMsg convertNotificationTemplateToDownlink(EdgeEvent edgeEvent) { + NotificationTemplateId notificationTemplateId = new NotificationTemplateId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ADDED, UPDATED -> { + NotificationTemplate notificationTemplate = notificationTemplateService.findNotificationTemplateById(edgeEvent.getTenantId(), notificationTemplateId); + if (notificationTemplate != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = notificationMsgConstructor.constructNotificationTemplateUpdateMsg(msgType, notificationTemplate); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addNotificationTemplateUpdateMsg(notificationTemplateUpdateMsg) + .build(); + } + } + case DELETED -> { + NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = notificationMsgConstructor.constructNotificationTemplateDeleteMsg(notificationTemplateId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addNotificationTemplateUpdateMsg(notificationTemplateUpdateMsg) + .build(); + } + } + return downlinkMsg; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java new file mode 100644 index 0000000000..ce0600b491 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.oauth2; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.OAuth2Info; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Slf4j +@Component +@TbCoreComponent +public class OAuth2EdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg convertOAuth2EventToDownlink(EdgeEvent edgeEvent) { + DownlinkMsg downlinkMsg = null; + OAuth2Info oAuth2Info = JacksonUtil.convertValue(edgeEvent.getBody(), OAuth2Info.class); + if (oAuth2Info != null) { + OAuth2UpdateMsg oAuth2UpdateMsg = oAuth2MsgConstructor.constructOAuth2UpdateMsg(oAuth2Info); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addOAuth2UpdateMsg(oAuth2UpdateMsg) + .build(); + } + return downlinkMsg; + } + + public ListenableFuture processOAuth2Notification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + OAuth2Info oAuth2Info = JacksonUtil.fromString(edgeNotificationMsg.getBody(), OAuth2Info.class); + if (oAuth2Info == null) { + return Futures.immediateFuture(null); + } + EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); + EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); + return processActionForAllEdges(tenantId, type, actionType, null, JacksonUtil.toJsonNode(edgeNotificationMsg.getBody()), null); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java index e5c9ed1779..34a7724fee 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java @@ -38,8 +38,7 @@ public class OtaPackageEdgeProcessor extends BaseEdgeProcessor { OtaPackageId otaPackageId = new OtaPackageId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: + case ADDED, UPDATED -> { OtaPackage otaPackage = otaPackageService.findOtaPackageById(edgeEvent.getTenantId(), otaPackageId); if (otaPackage != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -50,16 +49,17 @@ public class OtaPackageEdgeProcessor extends BaseEdgeProcessor { .addOtaPackageUpdateMsg(otaPackageUpdateMsg) .build(); } - break; - case DELETED: + } + case DELETED -> { OtaPackageUpdateMsg otaPackageUpdateMsg = ((OtaPackageMsgConstructor) otaPackageMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructOtaPackageDeleteMsg(otaPackageId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addOtaPackageUpdateMsg(otaPackageUpdateMsg) .build(); - break; + } } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java index d529b44850..bfa81361fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java @@ -38,8 +38,7 @@ public class QueueEdgeProcessor extends BaseEdgeProcessor { QueueId queueId = new QueueId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: + case ADDED, UPDATED -> { Queue queue = queueService.findQueueById(edgeEvent.getTenantId(), queueId); if (queue != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -50,16 +49,17 @@ public class QueueEdgeProcessor extends BaseEdgeProcessor { .addQueueUpdateMsg(queueUpdateMsg) .build(); } - break; - case DELETED: + } + case DELETED -> { QueueUpdateMsg queueDeleteMsg = ((QueueMsgConstructor) queueMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructQueueDeleteMsg(queueId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addQueueUpdateMsg(queueDeleteMsg) .build(); - break; + } } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/BaseRelationProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/BaseRelationProcessor.java index 045e4148e6..a832f0b1e1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/BaseRelationProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/BaseRelationProcessor.java @@ -58,4 +58,5 @@ public abstract class BaseRelationProcessor extends BaseEdgeProcessor { } protected abstract EntityRelation constructEntityRelationFromUpdateMsg(RelationUpdateMsg relationUpdateMsg); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java index 4c2ba3d04f..780550ed95 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java @@ -91,4 +91,5 @@ public abstract class RelationEdgeProcessor extends BaseRelationProcessor implem } return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessorV1.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessorV1.java index 10bdd19acb..647cec120a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessorV1.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessorV1.java @@ -49,4 +49,5 @@ public class RelationEdgeProcessorV1 extends RelationEdgeProcessor { entityRelation.setAdditionalInfo(JacksonUtil.toJsonNode(relationUpdateMsg.getAdditionalInfo())); return entityRelation; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessorV2.java index 13e2c40f4f..7b31da421d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessorV2.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessorV2.java @@ -31,4 +31,5 @@ public class RelationEdgeProcessorV2 extends RelationEdgeProcessor { protected EntityRelation constructEntityRelationFromUpdateMsg(RelationUpdateMsg relationUpdateMsg) { return JacksonUtil.fromString(relationUpdateMsg.getEntity(), EntityRelation.class, true); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationProcessor.java index f48fbefff8..82a893e9ba 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationProcessor.java @@ -29,4 +29,5 @@ public interface RelationProcessor extends EdgeProcessor { ListenableFuture processRelationMsgFromEdge(TenantId tenantId, Edge edge, RelationUpdateMsg relationUpdateMsg); DownlinkMsg convertRelationEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java index 1b55658183..7384130baf 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java @@ -72,4 +72,5 @@ public abstract class BaseResourceProcessor extends BaseEdgeProcessor { } protected abstract TbResource constructResourceFromUpdateMsg(TenantId tenantId, TbResourceId tbResourceId, ResourceUpdateMsg resourceUpdateMsg); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java index b67ad78888..0a83e439b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java @@ -74,8 +74,7 @@ public abstract class ResourceEdgeProcessor extends BaseResourceProcessor implem TbResourceId tbResourceId = new TbResourceId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: + case ADDED, UPDATED -> { TbResource tbResource = resourceService.findResourceById(edgeEvent.getTenantId(), tbResourceId); if (tbResource != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -86,16 +85,17 @@ public abstract class ResourceEdgeProcessor extends BaseResourceProcessor implem .addResourceUpdateMsg(resourceUpdateMsg) .build() : null; } - break; - case DELETED: + } + case DELETED -> { ResourceUpdateMsg resourceUpdateMsg = ((ResourceMsgConstructor) resourceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructResourceDeleteMsg(tbResourceId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addResourceUpdateMsg(resourceUpdateMsg) .build(); - break; + } } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessorV1.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessorV1.java index 0dd87df856..f272d447a4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessorV1.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessorV1.java @@ -45,4 +45,5 @@ public class ResourceEdgeProcessorV1 extends ResourceEdgeProcessor { resource.setEtag(resourceUpdateMsg.hasEtag() ? resourceUpdateMsg.getEtag() : null); return resource; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessorV2.java index ccbc6eec95..108a6c8ecb 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessorV2.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessorV2.java @@ -33,4 +33,5 @@ public class ResourceEdgeProcessorV2 extends ResourceEdgeProcessor { protected TbResource constructResourceFromUpdateMsg(TenantId tenantId, TbResourceId tbResourceId, ResourceUpdateMsg resourceUpdateMsg) { return JacksonUtil.fromString(resourceUpdateMsg.getEntity(), TbResource.class, true); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceProcessor.java index 7b8d777313..a52d31a98b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceProcessor.java @@ -29,4 +29,5 @@ public interface ResourceProcessor extends EdgeProcessor { ListenableFuture processResourceMsgFromEdge(TenantId tenantId, Edge edge, ResourceUpdateMsg resourceUpdateMsg); DownlinkMsg convertResourceEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java index 118cc0ccd4..30f74ae42d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java @@ -43,16 +43,15 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - case ASSIGNED_TO_EDGE: + case ADDED, UPDATED, ASSIGNED_TO_EDGE -> { RuleChain ruleChain = ruleChainService.findRuleChainById(edgeEvent.getTenantId(), ruleChainId); if (ruleChain != null) { boolean isRoot = false; if (edgeEvent.getBody() != null && edgeEvent.getBody().get(EDGE_IS_ROOT_BODY_KEY) != null) { try { isRoot = Boolean.parseBoolean(edgeEvent.getBody().get(EDGE_IS_ROOT_BODY_KEY).asText()); - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } if (!isRoot) { Edge edge = edgeService.findEdgeById(edgeEvent.getTenantId(), edgeEvent.getEdgeId()); @@ -67,15 +66,12 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { .addRuleChainUpdateMsg(ruleChainUpdateMsg) .build(); } - break; - case DELETED: - case UNASSIGNED_FROM_EDGE: - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addRuleChainUpdateMsg(((RuleChainMsgConstructor) ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) - .constructRuleChainDeleteMsg(ruleChainId)) - .build(); - break; + } + case DELETED, UNASSIGNED_FROM_EDGE -> downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addRuleChainUpdateMsg(((RuleChainMsgConstructor) ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) + .constructRuleChainDeleteMsg(ruleChainId)) + .build(); } return downlinkMsg; } @@ -99,4 +95,5 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java index 1f2c767010..d9b0a85056 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java @@ -45,4 +45,5 @@ public class AdminSettingsEdgeProcessor extends BaseEdgeProcessor { .addAdminSettingsUpdateMsg(adminSettingsUpdateMsg) .build(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java index 2a9af53807..2656ef622f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java @@ -23,10 +23,13 @@ import com.google.common.util.concurrent.SettableFuture; import com.google.gson.Gson; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import jakarta.annotation.Nullable; +import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; @@ -53,7 +56,6 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.dao.model.ModelConstants; @@ -66,8 +68,6 @@ import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; -import jakarta.annotation.Nullable; -import jakarta.annotation.PostConstruct; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -138,41 +138,39 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { TbMsgMetaData metaData = new TbMsgMetaData(); CustomerId customerId = null; switch (entityId.getEntityType()) { - case DEVICE: + case DEVICE -> { Device device = deviceService.findDeviceById(tenantId, new DeviceId(entityId.getId())); if (device != null) { customerId = device.getCustomerId(); metaData.putValue("deviceName", device.getName()); metaData.putValue("deviceType", device.getType()); } - break; - case ASSET: + } + case ASSET -> { Asset asset = assetService.findAssetById(tenantId, new AssetId(entityId.getId())); if (asset != null) { customerId = asset.getCustomerId(); metaData.putValue("assetName", asset.getName()); metaData.putValue("assetType", asset.getType()); } - break; - case ENTITY_VIEW: + } + case ENTITY_VIEW -> { EntityView entityView = entityViewService.findEntityViewById(tenantId, new EntityViewId(entityId.getId())); if (entityView != null) { customerId = entityView.getCustomerId(); metaData.putValue("entityViewName", entityView.getName()); metaData.putValue("entityViewType", entityView.getType()); } - break; - case EDGE: + } + case EDGE -> { Edge edge = edgeService.findEdgeById(tenantId, new EdgeId(entityId.getId())); if (edge != null) { customerId = edge.getCustomerId(); metaData.putValue("edgeName", edge.getName()); metaData.putValue("edgeType", edge.getType()); } - break; - default: - log.debug("[{}] Using empty metadata for entityId [{}]", tenantId, entityId); - break; + } + default -> log.debug("[{}] Using empty metadata for entityId [{}]", tenantId, entityId); } return new ImmutablePair<>(metaData, customerId != null ? customerId : new CustomerId(ModelConstants.NULL_UUID)); } @@ -317,36 +315,22 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { JsonNode body) { EntityId entityId; switch (entityType) { - case DEVICE: - entityId = new DeviceId(entityUUID); - break; - case ASSET: - entityId = new AssetId(entityUUID); - break; - case ENTITY_VIEW: - entityId = new EntityViewId(entityUUID); - break; - case DASHBOARD: - entityId = new DashboardId(entityUUID); - break; - case TENANT: - entityId = TenantId.fromUUID(entityUUID); - break; - case CUSTOMER: - entityId = new CustomerId(entityUUID); - break; - case USER: - entityId = new UserId(entityUUID); - break; - case EDGE: - entityId = new EdgeId(entityUUID); - break; - default: + case DEVICE -> entityId = new DeviceId(entityUUID); + case ASSET -> entityId = new AssetId(entityUUID); + case ENTITY_VIEW -> entityId = new EntityViewId(entityUUID); + case DASHBOARD -> entityId = new DashboardId(entityUUID); + case TENANT -> entityId = TenantId.fromUUID(entityUUID); + case CUSTOMER -> entityId = new CustomerId(entityUUID); + case USER -> entityId = new UserId(entityUUID); + case EDGE -> entityId = new EdgeId(entityUUID); + default -> { log.warn("[{}] Unsupported edge event type [{}]", tenantId, entityType); return null; + } } String bodyJackson = JacksonUtil.toString(body); return bodyJackson == null ? null : entityDataMsgConstructor.constructEntityDataMsg(tenantId, entityId, actionType, JsonParser.parseString(bodyJackson)); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessor.java index e69c479d93..4e9a0b82d8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessor.java @@ -16,12 +16,18 @@ package org.thingsboard.server.service.edge.rpc.processor.telemetry; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EntityDataProto; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -31,18 +37,28 @@ import org.thingsboard.server.queue.util.TbCoreComponent; @TbCoreComponent public class TelemetryEdgeProcessor extends BaseTelemetryProcessor { + @Value("${edges.rpc.max_telemetry_message_size:0}") + private int maxTelemetryMessageSize; + + @Lazy + @Autowired + private NotificationRuleProcessor notificationRuleProcessor; + @Override protected String getMsgSourceKey() { return DataConstants.EDGE_MSG_SOURCE; } - public DownlinkMsg convertTelemetryEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertTelemetryEventToDownlink(Edge edge, EdgeEvent edgeEvent) { if (edgeEvent.getBody() != null) { String bodyStr = edgeEvent.getBody().toString(); - if (bodyStr.length() > 1000) { - log.debug("[{}][{}][{}] Conversion to a DownlinkMsg telemetry event failed due to a size limit violation. " + - "Current size is {}, but the limit is 1000. {}", edgeEvent.getTenantId(), edgeEvent.getEdgeId(), - edgeEvent.getEntityId(), bodyStr.length(), StringUtils.truncate(bodyStr, 100)); + if (maxTelemetryMessageSize > 0 && bodyStr.length() > maxTelemetryMessageSize) { + String error = "Conversion to a DownlinkMsg telemetry event failed due to a size limit violation."; + String message = String.format("%s Current size is %s, but the limit is %s", error, bodyStr.length(), maxTelemetryMessageSize); + log.debug("[{}][{}][{}] {}. {}", edgeEvent.getTenantId(), edgeEvent.getEdgeId(), + edgeEvent.getEntityId(), message, StringUtils.truncate(bodyStr, 100)); + notificationRuleProcessor.process(EdgeCommunicationFailureTrigger.builder().tenantId(edgeEvent.getTenantId()) + .edgeId(edgeEvent.getEdgeId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(message).error(error).build()); return null; } } @@ -55,4 +71,5 @@ public class TelemetryEdgeProcessor extends BaseTelemetryProcessor { .addEntityData(entityDataProto) .build(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java index 2c192fab1d..515dccb987 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java @@ -60,4 +60,5 @@ public class TenantEdgeProcessor extends BaseEdgeProcessor { } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java index 6b97fbcb37..6c3bc1733f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java @@ -53,4 +53,5 @@ public class TenantProfileEdgeProcessor extends BaseEdgeProcessor { } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index be3d8308a6..607c5e665c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -39,8 +39,7 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { UserId userId = new UserId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: + case ADDED, UPDATED -> { User user = userService.findUserById(edgeEvent.getTenantId(), userId); if (user != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -49,14 +48,12 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { .addUserUpdateMsg(((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserUpdatedMsg(msgType, user)) .build(); } - break; - case DELETED: - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addUserUpdateMsg(((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserDeleteMsg(userId)) - .build(); - break; - case CREDENTIALS_UPDATED: + } + case DELETED -> downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addUserUpdateMsg(((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserDeleteMsg(userId)) + .build(); + case CREDENTIALS_UPDATED -> { UserCredentials userCredentialsByUserId = userService.findUserCredentialsByUserId(edgeEvent.getTenantId(), userId); if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) { UserCredentialsUpdateMsg userCredentialsUpdateMsg = @@ -66,8 +63,9 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { .addUserCredentialsUpdateMsg(userCredentialsUpdateMsg) .build(); } - break; + } } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java index 4f1aa9afcc..662b3677cc 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java @@ -40,8 +40,7 @@ public class WidgetBundleEdgeProcessor extends BaseEdgeProcessor { WidgetsBundleId widgetsBundleId = new WidgetsBundleId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: + case ADDED, UPDATED -> { WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleById(edgeEvent.getTenantId(), widgetsBundleId); if (widgetsBundle != null) { List widgets = widgetTypeService.findWidgetFqnsByWidgetsBundleId(edgeEvent.getTenantId(), widgetsBundleId); @@ -53,16 +52,17 @@ public class WidgetBundleEdgeProcessor extends BaseEdgeProcessor { .addWidgetsBundleUpdateMsg(widgetsBundleUpdateMsg) .build(); } - break; - case DELETED: + } + case DELETED -> { WidgetsBundleUpdateMsg widgetsBundleUpdateMsg = ((WidgetMsgConstructor) widgetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructWidgetsBundleDeleteMsg(widgetsBundleId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addWidgetsBundleUpdateMsg(widgetsBundleUpdateMsg) .build(); - break; + } } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java index 552ae47f43..c1891b8fdb 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java @@ -38,8 +38,7 @@ public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor { WidgetTypeId widgetTypeId = new WidgetTypeId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: + case ADDED, UPDATED -> { WidgetTypeDetails widgetTypeDetails = widgetTypeService.findWidgetTypeDetailsById(edgeEvent.getTenantId(), widgetTypeId); if (widgetTypeDetails != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -50,16 +49,17 @@ public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor { .addWidgetTypeUpdateMsg(widgetTypeUpdateMsg) .build(); } - break; - case DELETED: + } + case DELETED -> { WidgetTypeUpdateMsg widgetTypeUpdateMsg = ((WidgetMsgConstructor) widgetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructWidgetTypeDeleteMsg(widgetTypeId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addWidgetTypeUpdateMsg(widgetTypeUpdateMsg) .build(); - break; + } } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index f76b9ec828..3f61734801 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -27,7 +27,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; @@ -110,9 +109,6 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { @Autowired private DbCallbackExecutorService dbCallbackExecutorService; - @Autowired - private TbClusterService tbClusterService; - @Override public ListenableFuture processRuleChainMetadataRequestMsg(TenantId tenantId, Edge edge, RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg) { log.trace("[{}] processRuleChainMetadataRequestMsg [{}][{}]", tenantId, edge.getName(), ruleChainMetadataRequestMsg); @@ -292,8 +288,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { return futureToSet; } - private ListenableFuture> findRelationByQuery(TenantId tenantId, Edge edge, - EntityId entityId, EntitySearchDirection direction) { + private ListenableFuture> findRelationByQuery(TenantId tenantId, Edge edge, EntityId entityId, EntitySearchDirection direction) { EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(new RelationsSearchParameters(entityId, direction, 1, false)); return relationService.findByQuery(tenantId, query); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java index 2b6a8ef323..4d4d0170bf 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -25,10 +25,9 @@ import org.springframework.context.annotation.Lazy; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.alarm.AlarmInfo; -import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; @@ -72,11 +71,11 @@ public abstract class AbstractTbEntityService { @Lazy private EntitiesVersionControlService vcService; - protected void removeAlarmsByEntityId(TenantId tenantId, EntityId entityId) { - PageData alarms = - alarmService.findAlarms(tenantId, new AlarmQuery(entityId, new TimePageLink(Integer.MAX_VALUE), null, null, null, false)); + protected void removeAlarmsByOriginatorId(TenantId tenantId, EntityId entityId) { + PageData alarms = + alarmService.findAlarmIdsByOriginatorId(tenantId, entityId, new TimePageLink(Integer.MAX_VALUE)); - alarms.getData().stream().map(AlarmInfo::getId).forEach(alarmId -> alarmService.delAlarm(tenantId, alarmId)); + alarms.getData().forEach(alarmId -> alarmService.delAlarm(tenantId, alarmId)); } protected T checkNotNull(T reference) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index c699cd6c02..bc7dc62e78 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -69,80 +69,71 @@ public class EntityStateSourcingListener { @TransactionalEventListener(fallbackExecution = true) public void handleEvent(SaveEntityEvent event) { - log.trace("[{}] SaveEntityEvent called: {}", event.getTenantId(), event); TenantId tenantId = event.getTenantId(); EntityId entityId = event.getEntityId(); EntityType entityType = entityId.getEntityType(); + log.debug("[{}][{}][{}] Handling entity save event: {}", tenantId, entityType, entityId, event); boolean isCreated = event.getCreated() != null && event.getCreated(); ComponentLifecycleEvent lifecycleEvent = isCreated ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED; switch (entityType) { - case ASSET: - case ASSET_PROFILE: - case ENTITY_VIEW: - case NOTIFICATION_RULE: + case ASSET, ASSET_PROFILE, ENTITY_VIEW, NOTIFICATION_RULE -> { tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, lifecycleEvent); - break; - case RULE_CHAIN: + } + case RULE_CHAIN -> { RuleChain ruleChain = (RuleChain) event.getEntity(); if (RuleChainType.CORE.equals(ruleChain.getType())) { tbClusterService.broadcastEntityStateChangeEvent(ruleChain.getTenantId(), ruleChain.getId(), lifecycleEvent); } - break; - case TENANT: + } + case TENANT -> { Tenant tenant = (Tenant) event.getEntity(); onTenantUpdate(tenant, lifecycleEvent); - break; - case TENANT_PROFILE: + } + case TENANT_PROFILE -> { TenantProfile tenantProfile = (TenantProfile) event.getEntity(); onTenantProfileUpdate(tenantProfile, lifecycleEvent); - break; - case DEVICE: + } + case DEVICE -> { onDeviceUpdate(event.getEntity(), event.getOldEntity()); - break; - case DEVICE_PROFILE: + } + case DEVICE_PROFILE -> { DeviceProfile deviceProfile = (DeviceProfile) event.getEntity(); onDeviceProfileUpdate(deviceProfile, event.getOldEntity(), isCreated); - break; - case EDGE: + } + case EDGE -> { handleEdgeEvent(tenantId, entityId, event.getEntity(), lifecycleEvent); - break; - case TB_RESOURCE: + } + case TB_RESOURCE -> { TbResource tbResource = (TbResource) event.getEntity(); tbClusterService.onResourceChange(tbResource, null); - break; - case API_USAGE_STATE: + } + case API_USAGE_STATE -> { ApiUsageState apiUsageState = (ApiUsageState) event.getEntity(); tbClusterService.onApiStateChange(apiUsageState, null); - break; - default: - break; + } + default -> {} } } @TransactionalEventListener(fallbackExecution = true) public void handleEvent(DeleteEntityEvent event) { - log.trace("[{}] DeleteEntityEvent called: {}", event.getTenantId(), event); TenantId tenantId = event.getTenantId(); EntityId entityId = event.getEntityId(); EntityType entityType = entityId.getEntityType(); + log.debug("[{}][{}][{}] Handling entity deletion event: {}", tenantId, entityType, entityId, event); switch (entityType) { - case ASSET: - case ASSET_PROFILE: - case ENTITY_VIEW: - case CUSTOMER: - case EDGE: - case NOTIFICATION_RULE: + case ASSET, ASSET_PROFILE, ENTITY_VIEW, CUSTOMER, EDGE, NOTIFICATION_RULE -> { tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, ComponentLifecycleEvent.DELETED); - break; - case NOTIFICATION_REQUEST: + } + case NOTIFICATION_REQUEST -> { NotificationRequest request = (NotificationRequest) event.getEntity(); if (request.isScheduled()) { tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, ComponentLifecycleEvent.DELETED); } - break; - case RULE_CHAIN: + } + case RULE_CHAIN -> { RuleChain ruleChain = (RuleChain) event.getEntity(); if (RuleChainType.CORE.equals(ruleChain.getType())) { Set referencingRuleChainIds = JacksonUtil.fromString(event.getBody(), new TypeReference<>() {}); @@ -152,29 +143,28 @@ public class EntityStateSourcingListener { } tbClusterService.broadcastEntityStateChangeEvent(tenantId, ruleChain.getId(), ComponentLifecycleEvent.DELETED); } - break; - case TENANT: + } + case TENANT -> { Tenant tenant = (Tenant) event.getEntity(); onTenantDeleted(tenant); - break; - case TENANT_PROFILE: + } + case TENANT_PROFILE -> { TenantProfile tenantProfile = (TenantProfile) event.getEntity(); tbClusterService.onTenantProfileDelete(tenantProfile, null); - break; - case DEVICE: + } + case DEVICE -> { Device device = (Device) event.getEntity(); tbClusterService.onDeviceDeleted(tenantId, device, null); - break; - case DEVICE_PROFILE: + } + case DEVICE_PROFILE -> { DeviceProfile deviceProfile = (DeviceProfile) event.getEntity(); onDeviceProfileDelete(event.getTenantId(), event.getEntityId(), deviceProfile); - break; - case TB_RESOURCE: + } + case TB_RESOURCE -> { TbResourceInfo tbResource = (TbResourceInfo) event.getEntity(); tbClusterService.onResourceDeleted(tbResource, null); - break; - default: - break; + } + default -> {} } } @@ -186,8 +176,7 @@ public class EntityStateSourcingListener { && event.getEntity() instanceof DeviceCredentials) { tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(event.getTenantId(), (DeviceId) event.getEntityId(), (DeviceCredentials) event.getEntity()), null); - } else if (ActionType.ASSIGNED_TO_TENANT.equals(event.getActionType()) && event.getEntity() instanceof Device) { - Device device = (Device) event.getEntity(); + } else if (ActionType.ASSIGNED_TO_TENANT.equals(event.getActionType()) && event.getEntity() instanceof Device device) { Tenant tenant = JacksonUtil.fromString(event.getBody(), Tenant.class); if (tenant != null) { tbClusterService.onDeviceAssignedToTenant(tenant.getId(), device); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java index 4247edf7a3..ddc9164575 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java @@ -22,10 +22,8 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; @@ -33,30 +31,18 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; -import org.thingsboard.server.service.profile.TbAssetProfileCache; - -import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE; @Service @AllArgsConstructor public class DefaultTbAssetService extends AbstractTbEntityService implements TbAssetService { private final AssetService assetService; - private final TbAssetProfileCache assetProfileCache; @Override public Asset save(Asset asset, User user) throws Exception { ActionType actionType = asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = asset.getTenantId(); try { - if (TB_SERVICE_QUEUE.equals(asset.getType())) { - throw new ThingsboardException("Unable to save asset with type " + TB_SERVICE_QUEUE, ThingsboardErrorCode.BAD_REQUEST_PARAMS); - } else if (asset.getAssetProfileId() != null) { - AssetProfile assetProfile = assetProfileCache.get(tenantId, asset.getAssetProfileId()); - if (assetProfile != null && TB_SERVICE_QUEUE.equals(assetProfile.getName())) { - throw new ThingsboardException("Unable to save asset with profile " + TB_SERVICE_QUEUE, ThingsboardErrorCode.BAD_REQUEST_PARAMS); - } - } Asset savedAsset = checkNotNull(assetService.saveAsset(asset)); autoCommit(user, savedAsset.getId()); logEntityActionService.logEntityAction(tenantId, savedAsset.getId(), savedAsset, asset.getCustomerId(), @@ -75,7 +61,7 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb TenantId tenantId = asset.getTenantId(); AssetId assetId = asset.getId(); try { - removeAlarmsByEntityId(tenantId, assetId); + removeAlarmsByOriginatorId(tenantId, assetId); assetService.deleteAsset(tenantId, assetId); logEntityActionService.logEntityAction(tenantId, assetId, asset, asset.getCustomerId(), actionType, user, assetId.toString()); } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java index cfda5f8814..03850880ae 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java @@ -22,7 +22,6 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.audit.ActionType; -import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.TenantId; @@ -30,8 +29,6 @@ import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; -import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE; - @Service @TbCoreComponent @AllArgsConstructor @@ -45,14 +42,6 @@ public class DefaultTbAssetProfileService extends AbstractTbEntityService implem ActionType actionType = assetProfile.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = assetProfile.getTenantId(); try { - if (TB_SERVICE_QUEUE.equals(assetProfile.getName())) { - throw new ThingsboardException("Unable to save asset profile with name " + TB_SERVICE_QUEUE, ThingsboardErrorCode.BAD_REQUEST_PARAMS); - } else if (assetProfile.getId() != null) { - AssetProfile foundAssetProfile = assetProfileService.findAssetProfileById(tenantId, assetProfile.getId()); - if (foundAssetProfile != null && TB_SERVICE_QUEUE.equals(foundAssetProfile.getName())) { - throw new ThingsboardException("Updating asset profile with name " + TB_SERVICE_QUEUE + " is prohibited!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); - } - } AssetProfile savedAssetProfile = checkNotNull(assetProfileService.saveAssetProfile(assetProfile)); autoCommit(user, savedAssetProfile.getId()); logEntityActionService.logEntityAction(tenantId, savedAssetProfile.getId(), savedAssetProfile, diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java index aea603c30d..de9e757166 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java @@ -94,7 +94,7 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T TenantId tenantId = device.getTenantId(); DeviceId deviceId = device.getId(); try { - removeAlarmsByEntityId(tenantId, deviceId); + removeAlarmsByOriginatorId(tenantId, deviceId); deviceService.deleteDevice(tenantId, deviceId); logEntityActionService.logEntityAction(tenantId, deviceId, device, device.getCustomerId(), actionType, user, deviceId.toString()); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java index 17d52329b2..8096722940 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java @@ -16,12 +16,10 @@ package org.thingsboard.server.service.entitiy.tenant; import lombok.RequiredArgsConstructor; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; @@ -45,25 +43,19 @@ public class DefaultTbTenantService extends AbstractTbEntityService implements T private final TbQueueService tbQueueService; private final TenantProfileService tenantProfileService; private final EntitiesVersionControlService versionControlService; - private final ApplicationEventPublisher eventPublisher; @Override public Tenant save(Tenant tenant) throws Exception { boolean created = tenant.getId() == null; Tenant oldTenant = !created ? tenantService.findTenantById(tenant.getId()) : null; - Tenant savedTenant = checkNotNull(tenantService.saveTenant(tenant, !created)); - if (created) { - installScripts.createDefaultRuleChains(savedTenant.getId()); - installScripts.createDefaultEdgeRuleChains(savedTenant.getId()); - installScripts.createDefaultTenantDashboards(savedTenant.getId(), null); - } + Tenant savedTenant = tenantService.saveTenant(tenant, tenantId -> { + installScripts.createDefaultRuleChains(tenantId); + installScripts.createDefaultEdgeRuleChains(tenantId); + installScripts.createDefaultTenantDashboards(tenantId, null); + }); tenantProfileCache.evict(savedTenant.getId()); - if (created) { - eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(TenantId.SYS_TENANT_ID).entityId(savedTenant.getId()).entity(savedTenant).created(true).build()); - } - TenantProfile oldTenantProfile = oldTenant != null ? tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, oldTenant.getTenantProfileId()) : null; TenantProfile newTenantProfile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, savedTenant.getTenantProfileId()); tbQueueService.updateQueuesByTenants(Collections.singletonList(savedTenant.getTenantId()), newTenantProfile, oldTenantProfile); diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index f5bb67bdce..999079e913 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -151,17 +151,18 @@ public class InstallScripts { } } - public void createDefaultRuleChains(TenantId tenantId) throws IOException { + public void createDefaultRuleChains(TenantId tenantId) { Path tenantChainsDir = getTenantRuleChainsDir(); loadRuleChainsFromPath(tenantId, tenantChainsDir); } - public void createDefaultEdgeRuleChains(TenantId tenantId) throws IOException { + public void createDefaultEdgeRuleChains(TenantId tenantId) { Path edgeChainsDir = getEdgeRuleChainsDir(); loadRuleChainsFromPath(tenantId, edgeChainsDir); } - private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) throws IOException { + @SneakyThrows + private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) { findRuleChainsFromPath(ruleChainsPath).forEach(path -> { try { createRuleChainFromFile(tenantId, path, null); @@ -329,17 +330,18 @@ public class InstallScripts { } } - public void loadDashboards(TenantId tenantId, CustomerId customerId) throws Exception { + public void loadDashboards(TenantId tenantId, CustomerId customerId) { Path dashboardsDir = Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR); loadDashboardsFromDir(tenantId, customerId, dashboardsDir); } - public void createDefaultTenantDashboards(TenantId tenantId, CustomerId customerId) throws Exception { + public void createDefaultTenantDashboards(TenantId tenantId, CustomerId customerId) { Path dashboardsDir = Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DASHBOARDS_DIR); loadDashboardsFromDir(tenantId, customerId, dashboardsDir); } - private void loadDashboardsFromDir(TenantId tenantId, CustomerId customerId, Path dashboardsDir) throws IOException { + @SneakyThrows + private void loadDashboardsFromDir(TenantId tenantId, CustomerId customerId, Path dashboardsDir) { try (DirectoryStream dirStream = Files.newDirectoryStream(dashboardsDir, path -> path.toString().endsWith(JSON_EXT))) { dirStream.forEach( path -> { @@ -414,7 +416,7 @@ public class InstallScripts { } ); } catch (Exception e) { - log.error("Unable to load resources lwm2m object model from file: [{}]", resourceLwm2mPath.toString()); + log.error("Unable to load resources lwm2m object model from file: [{}]", resourceLwm2mPath); throw new RuntimeException("resource lwm2m object model from file", e); } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index d2925870c4..86d922c305 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -116,7 +116,10 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService updateSchema("3.6.2", 3006002, "3.6.3", 3006003, null); break; case "3.6.3": - updateSchema("3.6.3", 3006003, "3.7.0", 3007000, null); + updateSchema("3.6.3", 3006003, "3.6.4", 3006004, null); + break; + case "3.6.4": + updateSchema("3.6.4", 3006004, "3.7.0", 3007000, null); break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java index 362d0d971e..e7964169fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java @@ -81,6 +81,8 @@ import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.WEB; + @Service @Slf4j @RequiredArgsConstructor @@ -191,7 +193,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple NotificationProcessingContext ctx = NotificationProcessingContext.builder() .tenantId(tenantId) .request(notificationRequest) - .deliveryMethods(Set.of(NotificationDeliveryMethod.WEB)) + .deliveryMethods(Set.of(WEB)) .template(template) .build(); @@ -247,7 +249,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple private void processForTarget(NotificationTarget target, NotificationProcessingContext ctx) { Iterable recipients; switch (target.getConfiguration().getType()) { - case PLATFORM_USERS: { + case PLATFORM_USERS -> { PlatformUsersNotificationTargetConfig targetConfig = (PlatformUsersNotificationTargetConfig) target.getConfiguration(); if (targetConfig.getUsersFilter().getType().isForRules() && ctx.getRequest().getInfo() instanceof RuleOriginatedNotificationInfo) { recipients = new PageDataIterable<>(pageLink -> { @@ -258,21 +260,16 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple return notificationTargetService.findRecipientsForNotificationTargetConfig(ctx.getTenantId(), targetConfig, pageLink); }, 256); } - break; } - case SLACK: { + case SLACK -> { SlackNotificationTargetConfig targetConfig = (SlackNotificationTargetConfig) target.getConfiguration(); recipients = List.of(targetConfig.getConversation()); - break; } - case MICROSOFT_TEAMS: { + case MICROSOFT_TEAMS -> { MicrosoftTeamsNotificationTargetConfig targetConfig = (MicrosoftTeamsNotificationTargetConfig) target.getConfiguration(); recipients = List.of(targetConfig); - break; - } - default: { - recipients = Collections.emptyList(); } + default -> recipients = Collections.emptyList(); } Set deliveryMethods = new HashSet<>(ctx.getDeliveryMethods()); @@ -322,6 +319,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple .requestId(request.getId()) .recipientId(recipient.getId()) .type(ctx.getNotificationType()) + .deliveryMethod(WEB) .subject(processedTemplate.getSubject()) .text(processedTemplate.getBody()) .additionalConfig(processedTemplate.getAdditionalConfig()) @@ -347,19 +345,22 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple boolean updated = notificationService.markNotificationAsRead(tenantId, recipientId, notificationId); if (updated) { log.trace("Marked notification {} as read (recipient id: {}, tenant id: {})", notificationId, recipientId, tenantId); - NotificationUpdate update = NotificationUpdate.builder() - .updated(true) - .notificationId(notificationId.getId()) - .newStatus(NotificationStatus.READ) - .build(); - onNotificationUpdate(tenantId, recipientId, update); + Notification notification = notificationService.findNotificationById(tenantId, notificationId); + if (notification.getDeliveryMethod() == WEB) { + NotificationUpdate update = NotificationUpdate.builder() + .updated(true) + .notificationId(notificationId.getId()) + .newStatus(NotificationStatus.READ) + .build(); + onNotificationUpdate(tenantId, recipientId, update); + } } } @Override - public void markAllNotificationsAsRead(TenantId tenantId, UserId recipientId) { - int updatedCount = notificationService.markAllNotificationsAsRead(tenantId, recipientId); - if (updatedCount > 0) { + public void markAllNotificationsAsRead(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId) { + int updatedCount = notificationService.markAllNotificationsAsRead(tenantId, deliveryMethod, recipientId); + if (updatedCount > 0 && deliveryMethod == WEB) { log.trace("Marked all notifications as read (recipient id: {}, tenant id: {})", recipientId, tenantId); NotificationUpdate update = NotificationUpdate.builder() .updated(true) @@ -374,7 +375,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple public void deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId) { Notification notification = notificationService.findNotificationById(tenantId, notificationId); boolean deleted = notificationService.deleteNotification(tenantId, recipientId, notificationId); - if (deleted) { + if (deleted && notification.getDeliveryMethod() == WEB) { NotificationUpdate update = NotificationUpdate.builder() .deleted(true) .notification(notification) @@ -451,7 +452,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple @Override public NotificationDeliveryMethod getDeliveryMethod() { - return NotificationDeliveryMethod.WEB; + return WEB; } @Override @@ -462,7 +463,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple @Autowired public void setChannels(List channels, NotificationCenter webNotificationChannel) { this.channels = channels.stream().collect(Collectors.toMap(NotificationChannel::getDeliveryMethod, c -> c)); - this.channels.put(NotificationDeliveryMethod.WEB, (NotificationChannel) webNotificationChannel); + this.channels.put(WEB, (NotificationChannel) webNotificationChannel); } } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java index 646556372a..f796e2b440 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.notification.channels; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.base.Strings; import com.google.firebase.messaging.FirebaseMessagingException; import com.google.firebase.messaging.MessagingErrorCode; @@ -26,11 +27,15 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.notification.FirebaseService; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.NotificationRequest; +import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.info.NotificationInfo; import org.thingsboard.server.common.data.notification.settings.MobileAppNotificationDeliveryMethodConfig; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.template.MobileAppDeliveryMethodNotificationTemplate; +import org.thingsboard.server.dao.notification.NotificationService; import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.notification.NotificationProcessingContext; @@ -41,6 +46,8 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.MOBILE_APP; + @Component @RequiredArgsConstructor @Slf4j @@ -48,25 +55,54 @@ public class MobileAppNotificationChannel implements NotificationChannel validTokens = new HashSet<>(mobileSessions.keySet()); String subject = processedTemplate.getSubject(); String body = processedTemplate.getBody(); Map data = getNotificationData(processedTemplate, ctx); + int unreadCount = notificationService.countUnreadNotificationsByRecipientId(ctx.getTenantId(), MOBILE_APP, recipient.getId()); for (String token : mobileSessions.keySet()) { try { - firebaseService.sendMessage(ctx.getTenantId(), credentials, token, subject, body, data); + firebaseService.sendMessage(ctx.getTenantId(), credentials, token, subject, body, data, unreadCount); } catch (FirebaseMessagingException e) { MessagingErrorCode errorCode = e.getMessagingErrorCode(); if (errorCode == MessagingErrorCode.UNREGISTERED || errorCode == MessagingErrorCode.INVALID_ARGUMENT) { @@ -92,12 +128,6 @@ public class MobileAppNotificationChannel implements NotificationChannel { data.put("stateEntityId", stateEntityId.getId().toString()); data.put("stateEntityType", stateEntityId.getEntityType().name()); - if (!"true".equals(data.get("onClick.enabled")) && info.getDashboardId() != null) { - data.put("onClick.enabled", "true"); - data.put("onClick.linkType", "DASHBOARD"); - data.put("onClick.setEntityIdInState", "true"); - data.put("onClick.dashboardId", info.getDashboardId().toString()); - } }); data.put("notificationType", ctx.getNotificationType().name()); switch (ctx.getNotificationType()) { @@ -116,14 +146,14 @@ public class MobileAppNotificationChannel implements NotificationChannel data) throws FirebaseMessagingException { + public void sendMessage(TenantId tenantId, String credentials, String fcmToken, String title, String body, + Map data, Integer badge) throws FirebaseMessagingException { FirebaseContext firebaseContext = contexts.asMap().compute(tenantId.toString(), (key, context) -> { if (context == null) { return new FirebaseContext(key, credentials); @@ -64,6 +65,12 @@ public class DefaultFirebaseService implements FirebaseService { } }); + Aps.Builder apsConfig = Aps.builder() + .setSound("default"); + if (badge != null) { + apsConfig.setBadge(badge); + } + Message message = Message.builder() .setToken(fcmToken) .setNotification(Notification.builder() @@ -74,14 +81,17 @@ public class DefaultFirebaseService implements FirebaseService { .setPriority(AndroidConfig.Priority.HIGH) .build()) .setApnsConfig(ApnsConfig.builder() - .setAps(Aps.builder() - .setContentAvailable(true) - .build()) + .setAps(apsConfig.build()) .build()) .putAllData(data) .build(); - firebaseContext.getMessaging().send(message); - log.trace("[{}] Sent message for FCM token {}", tenantId, fcmToken); + try { + firebaseContext.getMessaging().send(message); + log.trace("[{}] Sent message for FCM token {}", tenantId, fcmToken); + } catch (Throwable t) { + log.debug("[{}] Failed to send message for FCM token {}", tenantId, fcmToken, t); + throw t; + } } public static class FirebaseContext { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index fd08b9da4d..cdb3f19601 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -18,6 +18,8 @@ package org.thingsboard.server.service.queue; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -50,9 +52,9 @@ import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequestActorMsg; import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.common.util.KvProtoUtil; import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.resource.ImageCacheKey; -import org.thingsboard.server.common.util.KvProtoUtil; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto; @@ -101,8 +103,6 @@ import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWra import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -583,7 +583,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService findLwM2mObject(TenantId tenantId, String sortOrder, String sortProperty, String[] objectIds) { log.trace("Executing findByTenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); List resources = resourceService.findTenantResourcesByResourceTypeAndObjectIds(tenantId, ResourceType.LWM2M_MODEL, objectIds); return resources.stream() @@ -107,7 +107,7 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements @Override public List findLwM2mObjectPage(TenantId tenantId, String sortProperty, String sortOrder, PageLink pageLink) { log.trace("Executing findByTenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); PageData resourcePageData = resourceService.findTenantResourcesByResourceTypeAndPageLink(tenantId, ResourceType.LWM2M_MODEL, pageLink); return resourcePageData.getData().stream() .flatMap(s -> Stream.ofNullable(toLwM2mObject(s, false))) diff --git a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java index 1542f4095e..ebbd29e589 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java @@ -208,52 +208,22 @@ public class AccessValidator { public void validate(SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) { switch (entityId.getEntityType()) { - case DEVICE: - validateDevice(currentUser, operation, entityId, callback); - return; - case DEVICE_PROFILE: - validateDeviceProfile(currentUser, operation, entityId, callback); - return; - case ASSET: - validateAsset(currentUser, operation, entityId, callback); - return; - case ASSET_PROFILE: - validateAssetProfile(currentUser, operation, entityId, callback); - return; - case RULE_CHAIN: - validateRuleChain(currentUser, operation, entityId, callback); - return; - case CUSTOMER: - validateCustomer(currentUser, operation, entityId, callback); - return; - case TENANT: - validateTenant(currentUser, operation, entityId, callback); - return; - case TENANT_PROFILE: - validateTenantProfile(currentUser, operation, entityId, callback); - return; - case USER: - validateUser(currentUser, operation, entityId, callback); - return; - case ENTITY_VIEW: - validateEntityView(currentUser, operation, entityId, callback); - return; - case EDGE: - validateEdge(currentUser, operation, entityId, callback); - return; - case API_USAGE_STATE: - validateApiUsageState(currentUser, operation, entityId, callback); - return; - case TB_RESOURCE: - validateResource(currentUser, operation, entityId, callback); - return; - case OTA_PACKAGE: - validateOtaPackage(currentUser, operation, entityId, callback); - return; - case RPC: - validateRpc(currentUser, operation, entityId, callback); - return; - default: + case DEVICE -> validateDevice(currentUser, operation, entityId, callback); + case DEVICE_PROFILE -> validateDeviceProfile(currentUser, operation, entityId, callback); + case ASSET -> validateAsset(currentUser, operation, entityId, callback); + case ASSET_PROFILE -> validateAssetProfile(currentUser, operation, entityId, callback); + case RULE_CHAIN -> validateRuleChain(currentUser, operation, entityId, callback); + case CUSTOMER -> validateCustomer(currentUser, operation, entityId, callback); + case TENANT -> validateTenant(currentUser, operation, entityId, callback); + case TENANT_PROFILE -> validateTenantProfile(currentUser, operation, entityId, callback); + case USER -> validateUser(currentUser, operation, entityId, callback); + case ENTITY_VIEW -> validateEntityView(currentUser, operation, entityId, callback); + case EDGE -> validateEdge(currentUser, operation, entityId, callback); + case API_USAGE_STATE -> validateApiUsageState(currentUser, operation, entityId, callback); + case TB_RESOURCE -> validateResource(currentUser, operation, entityId, callback); + case OTA_PACKAGE -> validateOtaPackage(currentUser, operation, entityId, callback); + case RPC -> validateRpc(currentUser, operation, entityId, callback); + default -> //TODO: add support of other entities throw new IllegalStateException("Not Implemented!"); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java index 5d001ccf6a..dbb930755d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java @@ -32,12 +32,12 @@ public class LoginRequest { this.password = password; } - @Schema(required = true, description = "User email", example = "tenant@thingsboard.org") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "User email", example = "tenant@thingsboard.org") public String getUsername() { return username; } - @Schema(required = true, description = "User password", example = "tenant") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "User password", example = "tenant") public String getPassword() { return password; } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginResponse.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginResponse.java index 239186cf45..503767106a 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginResponse.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginResponse.java @@ -22,11 +22,11 @@ import lombok.Data; @Data public class LoginResponse { - @Schema(required = true, description = "JWT token", + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JWT token", example = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZW5hbnRAdGhpbmdzYm9hcmQub3JnIi...") private String token; - @Schema(required = true, description = "Refresh token", + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Refresh token", example = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZW5hbnRAdGhpbmdzYm9hcmQub3JnIi...") private String refreshToken; diff --git a/application/src/main/java/org/thingsboard/server/service/stats/DefaultRuleEngineStatisticsService.java b/application/src/main/java/org/thingsboard/server/service/stats/DefaultRuleEngineStatisticsService.java index 64ca544495..03747d3062 100644 --- a/application/src/main/java/org/thingsboard/server/service/stats/DefaultRuleEngineStatisticsService.java +++ b/application/src/main/java/org/thingsboard/server/service/stats/DefaultRuleEngineStatisticsService.java @@ -21,15 +21,15 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.QueueStatsId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.queue.QueueStats; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.util.TbRuleEngineComponent; @@ -52,7 +52,6 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class DefaultRuleEngineStatisticsService implements RuleEngineStatisticsService { - public static final String TB_SERVICE_QUEUE = "TbServiceQueue"; public static final String RULE_ENGINE_EXCEPTION = "ruleEngineException"; public static final FutureCallback CALLBACK = new FutureCallback() { @Override @@ -68,10 +67,10 @@ public class DefaultRuleEngineStatisticsService implements RuleEngineStatisticsS private final TbServiceInfoProvider serviceInfoProvider; private final TelemetrySubscriptionService tsService; - private final AssetService assetService; + private final QueueStatsService queueStatsService; private final ApiLimitService apiLimitService; private final Lock lock = new ReentrantLock(); - private final ConcurrentMap tenantQueueAssets = new ConcurrentHashMap<>(); + private final ConcurrentMap tenantQueueStats = new ConcurrentHashMap<>(); @Value("${queue.rule-engine.stats.max-error-message-length:4096}") private int maxErrorMessageLength; @@ -82,7 +81,7 @@ public class DefaultRuleEngineStatisticsService implements RuleEngineStatisticsS ruleEngineStats.getTenantStats().forEach((id, stats) -> { try { TenantId tenantId = TenantId.fromUUID(id); - AssetId serviceAssetId = getServiceAssetId(tenantId, queueName); + QueueStatsId queueStatsId = getQueueStatsId(tenantId, queueName); if (stats.getTotalMsgCounter().get() > 0) { List tsList = stats.getCounters().entrySet().stream() .map(kv -> new BasicTsKvEntry(ts, new LongDataEntry(kv.getKey(), (long) kv.getValue().get()))) @@ -90,7 +89,7 @@ public class DefaultRuleEngineStatisticsService implements RuleEngineStatisticsS if (!tsList.isEmpty()) { long ttl = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getQueueStatsTtlDays); ttl = TimeUnit.DAYS.toSeconds(ttl); - tsService.saveAndNotifyInternal(tenantId, serviceAssetId, tsList, ttl, CALLBACK); + tsService.saveAndNotifyInternal(tenantId, queueStatsId, tsList, ttl, CALLBACK); } } } catch (Exception e) { @@ -104,7 +103,7 @@ public class DefaultRuleEngineStatisticsService implements RuleEngineStatisticsS TsKvEntry tsKv = new BasicTsKvEntry(e.getTs(), new JsonDataEntry(RULE_ENGINE_EXCEPTION, e.toJsonString(maxErrorMessageLength))); long ttl = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getRuleEngineExceptionsTtlDays); ttl = TimeUnit.DAYS.toSeconds(ttl); - tsService.saveAndNotifyInternal(tenantId, getServiceAssetId(tenantId, queueName), Collections.singletonList(tsKv), ttl, CALLBACK); + tsService.saveAndNotifyInternal(tenantId, getQueueStatsId(tenantId, queueName), Collections.singletonList(tsKv), ttl, CALLBACK); } catch (Exception e2) { if (!"Asset is referencing to non-existent tenant!".equalsIgnoreCase(e2.getMessage())) { log.debug("[{}] Failed to store the statistics", tenantId, e2); @@ -113,30 +112,30 @@ public class DefaultRuleEngineStatisticsService implements RuleEngineStatisticsS }); } - private AssetId getServiceAssetId(TenantId tenantId, String queueName) { + private QueueStatsId getQueueStatsId(TenantId tenantId, String queueName) { TenantQueueKey key = new TenantQueueKey(tenantId, queueName); - AssetId assetId = tenantQueueAssets.get(key); - if (assetId == null) { + QueueStatsId queueStatsId = tenantQueueStats.get(key); + if (queueStatsId == null) { lock.lock(); try { - assetId = tenantQueueAssets.get(key); - if (assetId == null) { - Asset asset = assetService.findAssetByTenantIdAndName(tenantId, queueName + "_" + serviceInfoProvider.getServiceId()); - if (asset == null) { - asset = new Asset(); - asset.setTenantId(tenantId); - asset.setName(queueName + "_" + serviceInfoProvider.getServiceId()); - asset.setType(TB_SERVICE_QUEUE); - asset = assetService.saveAsset(asset); + queueStatsId = tenantQueueStats.get(key); + if (queueStatsId == null) { + QueueStats queueStats = queueStatsService.findByTenantIdAndNameAndServiceId(tenantId, queueName , serviceInfoProvider.getServiceId()); + if (queueStats == null) { + queueStats = new QueueStats(); + queueStats.setTenantId(tenantId); + queueStats.setQueueName(queueName); + queueStats.setServiceId(serviceInfoProvider.getServiceId()); + queueStats = queueStatsService.save(tenantId, queueStats); } - assetId = asset.getId(); - tenantQueueAssets.put(key, assetId); + queueStatsId = queueStats.getId(); + tenantQueueStats.put(key, queueStatsId); } } finally { lock.unlock(); } } - return assetId; + return queueStatsId; } @Data diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java index d48851847c..2d8440fa20 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java @@ -23,19 +23,9 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; -import org.thingsboard.server.common.data.kv.BasicTsKvEntry; -import org.thingsboard.server.common.data.kv.BooleanDataEntry; -import org.thingsboard.server.common.data.kv.DataType; -import org.thingsboard.server.common.data.kv.DoubleDataEntry; -import org.thingsboard.server.common.data.kv.JsonDataEntry; -import org.thingsboard.server.common.data.kv.KvEntry; -import org.thingsboard.server.common.data.kv.LongDataEntry; -import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.gen.transport.TransportProtos.KeyValueProto; import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionMgrMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.TbAlarmDeleteProto; import org.thingsboard.server.gen.transport.TransportProtos.TbAlarmUpdateProto; @@ -46,29 +36,23 @@ import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesDeletePr import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesUpdateProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; -import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.UUID; -public class TbSubscriptionUtils { - - private static final DataType[] dataTypeByProtoNumber; +import static org.thingsboard.server.common.util.KvProtoUtil.fromTsValueProtoList; +import static org.thingsboard.server.common.util.KvProtoUtil.toTsKvProtoBuilder; +import static org.thingsboard.server.common.util.KvProtoUtil.toTsValueProto; - static { - int arraySize = Arrays.stream(DataType.values()).mapToInt(DataType::getProtoNumber).max().orElse(0); - dataTypeByProtoNumber = new DataType[arraySize + 1]; - Arrays.stream(DataType.values()).forEach(dataType -> dataTypeByProtoNumber[dataType.getProtoNumber()] = dataType); - } +public class TbSubscriptionUtils { public static ToCoreMsg toSubEventProto(String serviceId, TbEntitySubEvent event) { SubscriptionMgrMsgProto.Builder msgBuilder = SubscriptionMgrMsgProto.newBuilder(); @@ -189,7 +173,7 @@ public class TbSubscriptionUtils { builder.setEntityIdLSB(entityId.getId().getLeastSignificantBits()); builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); - ts.forEach(v -> builder.addData(toKeyValueProto(v.getTs(), v).build())); + ts.forEach(v -> builder.addData(toTsKvProtoBuilder(v.getTs(), v).build())); SubscriptionMgrMsgProto.Builder msgBuilder = SubscriptionMgrMsgProto.newBuilder(); msgBuilder.setTsUpdate(builder); return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); @@ -216,7 +200,7 @@ public class TbSubscriptionUtils { builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); builder.setScope(scope); - attributes.forEach(v -> builder.addData(toKeyValueProto(v.getLastUpdateTs(), v).build())); + attributes.forEach(v -> builder.addData(toTsKvProtoBuilder(v.getLastUpdateTs(), v).build())); SubscriptionMgrMsgProto.Builder msgBuilder = SubscriptionMgrMsgProto.newBuilder(); msgBuilder.setAttrUpdate(builder); @@ -239,121 +223,10 @@ public class TbSubscriptionUtils { return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); } - private static TsKvProto.Builder toKeyValueProto(long ts, KvEntry attr) { - KeyValueProto.Builder dataBuilder = KeyValueProto.newBuilder(); - dataBuilder.setKey(attr.getKey()); - dataBuilder.setType(toProto(attr.getDataType())); - switch (attr.getDataType()) { - case BOOLEAN: - attr.getBooleanValue().ifPresent(dataBuilder::setBoolV); - break; - case LONG: - attr.getLongValue().ifPresent(dataBuilder::setLongV); - break; - case DOUBLE: - attr.getDoubleValue().ifPresent(dataBuilder::setDoubleV); - break; - case JSON: - attr.getJsonValue().ifPresent(dataBuilder::setJsonV); - break; - case STRING: - attr.getStrValue().ifPresent(dataBuilder::setStringV); - break; - } - return TsKvProto.newBuilder().setTs(ts).setKv(dataBuilder); - } - - private static TransportProtos.TsValueProto toTsValueProto(long ts, KvEntry attr) { - TransportProtos.TsValueProto.Builder dataBuilder = TransportProtos.TsValueProto.newBuilder(); - dataBuilder.setTs(ts); - dataBuilder.setType(toProto(attr.getDataType())); - switch (attr.getDataType()) { - case BOOLEAN: - attr.getBooleanValue().ifPresent(dataBuilder::setBoolV); - break; - case LONG: - attr.getLongValue().ifPresent(dataBuilder::setLongV); - break; - case DOUBLE: - attr.getDoubleValue().ifPresent(dataBuilder::setDoubleV); - break; - case JSON: - attr.getJsonValue().ifPresent(dataBuilder::setJsonV); - break; - case STRING: - attr.getStrValue().ifPresent(dataBuilder::setStringV); - break; - } - return dataBuilder.build(); - } - - public static EntityId toEntityId(String entityType, long entityIdMSB, long entityIdLSB) { return EntityIdFactory.getByTypeAndUuid(entityType, new UUID(entityIdMSB, entityIdLSB)); } - public static List toTsKvEntityList(List dataList) { - List result = new ArrayList<>(dataList.size()); - dataList.forEach(proto -> result.add(new BasicTsKvEntry(proto.getTs(), getKvEntry(proto.getKv())))); - return result; - } - - public static List toAttributeKvList(List dataList) { - List result = new ArrayList<>(dataList.size()); - dataList.forEach(proto -> result.add(new BaseAttributeKvEntry(getKvEntry(proto.getKv()), proto.getTs()))); - return result; - } - - private static KvEntry getKvEntry(KeyValueProto proto) { - KvEntry entry = null; - switch (fromProto(proto.getType())) { - case BOOLEAN: - entry = new BooleanDataEntry(proto.getKey(), proto.getBoolV()); - break; - case LONG: - entry = new LongDataEntry(proto.getKey(), proto.getLongV()); - break; - case DOUBLE: - entry = new DoubleDataEntry(proto.getKey(), proto.getDoubleV()); - break; - case STRING: - entry = new StringDataEntry(proto.getKey(), proto.getStringV()); - break; - case JSON: - entry = new JsonDataEntry(proto.getKey(), proto.getJsonV()); - break; - } - return entry; - } - - public static List toTsKvEntityList(String key, List dataList) { - List result = new ArrayList<>(dataList.size()); - dataList.forEach(proto -> result.add(new BasicTsKvEntry(proto.getTs(), getKvEntry(key, proto)))); - return result; - } - - private static KvEntry getKvEntry(String key, TransportProtos.TsValueProto proto) { - KvEntry entry = null; - switch (fromProto(proto.getType())) { - case BOOLEAN: - entry = new BooleanDataEntry(key, proto.getBoolV()); - break; - case LONG: - entry = new LongDataEntry(key, proto.getLongV()); - break; - case DOUBLE: - entry = new DoubleDataEntry(key, proto.getDoubleV()); - break; - case STRING: - entry = new StringDataEntry(key, proto.getStringV()); - break; - case JSON: - entry = new JsonDataEntry(key, proto.getJsonV()); - break; - } - return entry; - } - public static ToCoreMsg toAlarmUpdateProto(TenantId tenantId, EntityId entityId, AlarmInfo alarm) { TbAlarmUpdateProto.Builder builder = TbAlarmUpdateProto.newBuilder(); builder.setEntityType(entityId.getEntityType().name()); @@ -411,7 +284,7 @@ public class TbSubscriptionUtils { public static List fromProto(TransportProtos.TbSubUpdateProto proto) { List result = new ArrayList<>(); for (var p : proto.getDataList()) { - result.addAll(toTsKvEntityList(p.getKey(), p.getTsValueList())); + result.addAll(fromTsValueProtoList(p.getKey(), p.getTsValueList())); } return result; } @@ -453,12 +326,4 @@ public class TbSubscriptionUtils { return ToCoreNotificationMsg.newBuilder().setToLocalSubscriptionServiceMsg(result).build(); } - public static TransportProtos.KeyValueType toProto(DataType dataType) { - return TransportProtos.KeyValueType.forNumber(dataType.getProtoNumber()); - } - - public static DataType fromProto(TransportProtos.KeyValueType keyValueType) { - return dataTypeByProtoNumber[keyValueType.getNumber()]; - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index 0aa71917d4..6df897c179 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -101,7 +101,11 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS ctx.putInternalId(exportData.getExternalId(), importResult.getSavedEntity().getId()); ctx.addReferenceCallback(exportData.getExternalId(), importResult.getSaveReferencesCallback()); - ctx.addEventCallback(importResult.getSendEventsCallback()); + if (ctx.isRollbackOnError()) { + ctx.addEventCallback(importResult.getSendEventsCallback()); + } else { + importResult.getSendEventsCallback().run(); + } return importResult; } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java index d4c80f730a..3890e5791d 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java @@ -236,14 +236,14 @@ public abstract class AbstractBulkImportService new JsonPrimitive((String) value); + case LONG -> new JsonPrimitive((Long) value); + case DOUBLE -> new JsonPrimitive((Double) value); + case BOOLEAN -> new JsonPrimitive((Boolean) value); + default -> null; + }; } public String stringValue() { 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 18969e4e0b..f81629c2fb 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 @@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportSettings; @@ -89,6 +90,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.function.Function; @@ -246,16 +248,18 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont switch (request.getType()) { case SINGLE_ENTITY: { SingleEntityVersionLoadRequest versionLoadRequest = (SingleEntityVersionLoadRequest) request; + ctx.setRollbackOnError(true); VersionLoadConfig config = versionLoadRequest.getConfig(); ListenableFuture future = gitServiceQueue.getEntity(user.getTenantId(), request.getVersionId(), versionLoadRequest.getExternalEntityId()); DonAsynchron.withCallback(future, - entityData -> doInTemplate(ctx, request, c -> loadSingleEntity(c, config, entityData)), + entityData -> load(ctx, request, c -> loadSingleEntity(c, config, entityData)), e -> processLoadError(ctx, e), executor); break; } case ENTITY_TYPE: { EntityTypeVersionLoadRequest versionLoadRequest = (EntityTypeVersionLoadRequest) request; - executor.submit(() -> doInTemplate(ctx, request, c -> loadMultipleEntities(c, versionLoadRequest))); + ctx.setRollbackOnError(versionLoadRequest.isRollbackOnError()); + executor.submit(() -> load(ctx, request, c -> loadMultipleEntities(c, versionLoadRequest))); break; } default: @@ -265,19 +269,24 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont return ctx.getRequestId(); } - private VersionLoadResult doInTemplate(EntitiesImportCtx ctx, VersionLoadRequest request, Function function) { + private VersionLoadResult load(EntitiesImportCtx ctx, VersionLoadRequest request, Function loadFunction) { try { - VersionLoadResult result = transactionTemplate.execute(status -> { - try { - return function.apply(ctx); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException(e); // to prevent UndeclaredThrowableException + VersionLoadResult result; + if (ctx.isRollbackOnError()) { + result = transactionTemplate.execute(status -> { + try { + return loadFunction.apply(ctx); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); // to prevent UndeclaredThrowableException + } + }); + for (ThrowingRunnable eventCallback : ctx.getEventCallbacks()) { + eventCallback.run(); } - }); - for (ThrowingRunnable throwingRunnable : ctx.getEventCallbacks()) { - throwingRunnable.run(); + } else { + result = loadFunction.apply(ctx); } result.setDone(true); return cachePut(ctx.getRequestId(), result); @@ -324,7 +333,6 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont sw.startNew("Entities " + entityType.name()); ctx.setSettings(getEntityImportSettings(request, entityType)); importEntities(ctx, entityType); - persistToCache(ctx); } sw.startNew("Reimport"); @@ -336,7 +344,6 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont .filter(entityType -> request.getEntityTypes().get(entityType).isRemoveOtherEntities()) .sorted(exportImportService.getEntityTypeComparatorForImport().reversed()) .forEach(entityType -> removeOtherEntities(ctx, entityType)); - persistToCache(ctx); sw.startNew("References and Relations"); exportImportService.saveReferencesAndRelations(ctx); @@ -389,6 +396,8 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont ctx.getImportedEntities().computeIfAbsent(entityType, t -> new HashSet<>()) .add(importResult.getSavedEntity().getId()); } + + persistToCache(ctx); log.debug("Imported {} pack ({}) for tenant {}", entityType, entityDataList.size(), ctx.getTenantId()); offset += limit; } while (entityDataList.size() == limit); @@ -413,17 +422,34 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont } private void removeOtherEntities(EntitiesImportCtx ctx, EntityType entityType) { - DaoUtil.processInBatches(pageLink -> { - return exportableEntitiesService.findEntitiesByTenantId(ctx.getTenantId(), entityType, pageLink); - }, 100, entity -> { - if (ctx.getImportedEntities().get(entityType) == null || !ctx.getImportedEntities().get(entityType).contains(entity.getId())) { - exportableEntitiesService.removeById(ctx.getTenantId(), entity.getId()); - - ctx.addEventCallback(() -> logEntityActionService.logEntityAction(ctx.getTenantId(), entity.getId(), entity, null, - ActionType.DELETED, ctx.getUser())); - ctx.registerDeleted(entityType); + var entities = new PageDataIterable<>(link -> exportableEntitiesService.findEntitiesIdsByTenantId(ctx.getTenantId(), entityType, link), 100); + Set toRemove = new HashSet<>(); + for (EntityId entityId : entities) { + if (ctx.getImportedEntities().get(entityType) == null || !ctx.getImportedEntities().get(entityType).contains(entityId)) { + toRemove.add(entityId); } - }); + } + + for (EntityId entityId : toRemove) { + ExportableEntity entity = exportableEntitiesService.findEntityById(entityId); + exportableEntitiesService.removeById(ctx.getTenantId(), entityId); + + ThrowingRunnable callback = () -> { + logEntityActionService.logEntityAction(ctx.getTenantId(), entity.getId(), entity, null, + ActionType.DELETED, ctx.getUser()); + }; + if (ctx.isRollbackOnError()) { + ctx.addEventCallback(callback); + } else { + try { + callback.run(); + } catch (ThingsboardException e) { + throw new RuntimeException(e); + } + } + ctx.registerDeleted(entityType); + } + persistToCache(ctx); } private VersionLoadResult onError(EntityId externalId, Throwable e) { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java index 0f0e3313b8..19999ea080 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java @@ -58,6 +58,7 @@ public class EntitiesImportCtx { private boolean finalImportAttempt = false; private EntityImportSettings settings; private EntityImportResult currentImportResult; + private boolean rollbackOnError; public EntitiesImportCtx(UUID requestId, User user, String versionId) { this(requestId, user, versionId, null); diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java index f868a9d4f3..6fa9111c3d 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java @@ -51,6 +51,8 @@ import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.WEB; + @Service @TbCoreComponent @RequiredArgsConstructor @@ -104,7 +106,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH private void fetchUnreadNotifications(NotificationsSubscription subscription) { log.trace("[{}, subId: {}] Fetching unread notifications from DB", subscription.getSessionId(), subscription.getSubscriptionId()); PageData notifications = notificationService.findLatestUnreadNotificationsByRecipientId(subscription.getTenantId(), - (UserId) subscription.getEntityId(), subscription.getLimit()); + WEB, (UserId) subscription.getEntityId(), subscription.getLimit()); subscription.getLatestUnreadNotifications().clear(); notifications.getData().forEach(notification -> { subscription.getLatestUnreadNotifications().put(notification.getUuidId(), notification); @@ -114,7 +116,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH private void fetchUnreadNotificationsCount(NotificationsCountSubscription subscription) { log.trace("[{}, subId: {}] Fetching unread notifications count from DB", subscription.getSessionId(), subscription.getSubscriptionId()); - int unreadCount = notificationService.countUnreadNotificationsByRecipientId(subscription.getTenantId(), (UserId) subscription.getEntityId()); + int unreadCount = notificationService.countUnreadNotificationsByRecipientId(subscription.getTenantId(), WEB, (UserId) subscription.getEntityId()); subscription.getTotalUnreadCounter().set(unreadCount); } @@ -235,7 +237,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH @Override public void handleMarkAllAsReadCmd(WebSocketSessionRef sessionRef, MarkAllNotificationsAsReadCmd cmd) { SecurityUser securityCtx = sessionRef.getSecurityCtx(); - notificationCenter.markAllNotificationsAsRead(securityCtx.getTenantId(), securityCtx.getId()); + notificationCenter.markAllNotificationsAsRead(securityCtx.getTenantId(), WEB, securityCtx.getId()); } @Override diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 535a396762..ef03034535 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -873,6 +873,8 @@ js: monitor_thread_pool_size: "${LOCAL_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}" # Maximum CPU time in milliseconds allowed for script execution max_cpu_time: "${LOCAL_JS_SANDBOX_MAX_CPU_TIME:8000}" + # Maximum memory in Bytes which JS executor thread can allocate (approximate calculation). A zero memory limit in combination with a non-zero CPU limit is not recommended due to the implementation of Nashorn 0.4.2. 100MiB is effectively unlimited for most cases + max_memory: "${LOCAL_JS_SANDBOX_MAX_MEMORY:104857600}" # Maximum allowed JavaScript execution errors before JavaScript will be blacklisted max_errors: "${LOCAL_JS_SANDBOX_MAX_ERRORS:3}" # JS Eval max request timeout. 0 - no timeout @@ -1311,6 +1313,8 @@ edges: private_key: "${EDGES_RPC_SSL_PRIVATE_KEY:privateKeyFile.pem}" # Maximum size (in bytes) of inbound messages the cloud can handle from the edge. By default, it can handle messages up to 4 Megabytes max_inbound_message_size: "${EDGES_RPC_MAX_INBOUND_MESSAGE_SIZE:4194304}" + # Maximum length of telemetry (time-series and attributes) message the cloud sends to the edge. By default, there is no limitation. + max_telemetry_message_size: "${EDGES_RPC_MAX_TELEMETRY_MESSAGE_SIZE:0}" storage: # Max records of edge event to read from DB and sent to the edge max_read_records_count: "${EDGES_STORAGE_MAX_READ_RECORDS_COUNT:50}" 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 49c26b5c38..e14b041e62 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -41,6 +41,8 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; @@ -112,6 +114,7 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.config.ThingsboardSecurityConfiguration; import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.device.ClaimDevicesService; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService; @@ -149,6 +152,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; +import static org.thingsboard.server.common.data.CacheConstants.CLAIM_DEVICES_CACHE; @Slf4j public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { @@ -230,6 +234,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { @Autowired protected DefaultActorService actorService; + @Autowired + protected ClaimDevicesService claimDevicesService; + @SpyBean protected MailService mailService; @@ -1055,6 +1062,16 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { }); } + protected void awaitForClaimingInfoToBeRegistered(DeviceId deviceId) { + CacheManager cacheManager = (CacheManager) ReflectionTestUtils.getField(claimDevicesService, "cacheManager"); + Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE); + Awaitility.await("Claiming request from the transport was registered").atMost(5, TimeUnit.SECONDS).until(() -> { + Cache.ValueWrapper value = cache.get(List.of(deviceId)); + log.warn("device {}, claimingRequest registered: {}", deviceId, value); + return value != null; + }); + } + protected static String getMapName(FeatureType featureType) { switch (featureType) { case ATTRIBUTES: diff --git a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java index b41c8c3e78..06d15ddb44 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java @@ -29,6 +29,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.ContextConfiguration; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.StringUtils; @@ -45,16 +46,17 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.asset.AssetDao; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.service.stats.DefaultRuleEngineStatisticsService; import java.util.ArrayList; import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @@ -327,6 +329,53 @@ public class AssetControllerTest extends AbstractControllerTest { .andExpect(statusReason(containsString(msgErrorNoFound("Alarm", alarm.getId().getId().toString())))); } + @Test + public void testDeleteAssetWithPropagatedAlarm() throws Exception { + Device device = new Device(); + device.setTenantId(savedTenant.getTenantId()); + device.setName("Test device"); + device.setLabel("Label"); + device.setType("default"); + device = doPost("/api/device", device, Device.class); + + Asset asset = new Asset(); + asset.setName("My asset"); + asset.setType("default"); + asset = doPost("/api/asset", asset, Asset.class); + + EntityRelation entityRelation = new EntityRelation(asset.getId(), device.getId(), "CONTAINS"); + doPost("/api/relation", entityRelation); + + //create alarm + Alarm alarm = Alarm.builder() + .tenantId(savedTenant.getTenantId()) + .originator(device.getId()) + .severity(AlarmSeverity.CRITICAL) + .type("test_type") + .propagate(true) + .build(); + + alarm = doPost("/api/alarm", alarm, Alarm.class); + Assert.assertNotNull(alarm); + + PageData deviceAlarms = doGetTyped("/api/alarm/DEVICE/" + device.getUuidId() + "?page=0&pageSize=10", new TypeReference<>() { + }); + assertThat(deviceAlarms.getData()).hasSize(1); + + PageData assetAlarms = doGetTyped("/api/alarm/ASSET/" + asset.getUuidId() + "?page=0&pageSize=10", new TypeReference<>() { + }); + assertThat(assetAlarms.getData()).hasSize(1); + + //delete asset + doDelete("/api/asset/" + asset.getId().getId().toString()) + .andExpect(status().isOk()); + + //check device alarms + PageData deviceAlarmsAfterAssetDeletion = doGetTyped("/api/alarm/DEVICE/" + device.getUuidId() + "?page=0&pageSize=10", new TypeReference>() { + }); + assertThat(deviceAlarmsAfterAssetDeletion.getData()).hasSize(1); + } + @Test public void testDeleteAssetAssignedToEntityView() throws Exception { Asset asset1 = new Asset(); @@ -567,8 +616,6 @@ public class AssetControllerTest extends AbstractControllerTest { savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, cntEntity, cntEntity, cntEntity); - loadedAssets.removeIf(asset -> asset.getType().equals(DefaultRuleEngineStatisticsService.TB_SERVICE_QUEUE)); - assets.sort(idComparator); loadedAssets.sort(idComparator); 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 772a3c5ecb..b0a6db944c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java @@ -25,7 +25,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.TsKvEntry; @@ -34,12 +33,13 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.queue.ProcessingStrategy; import org.thingsboard.server.common.data.queue.ProcessingStrategyType; import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.queue.QueueStats; import org.thingsboard.server.common.data.queue.SubmitStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategyType; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.stats.StatsFactory; -import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.timeseries.TimeseriesDao; import org.thingsboard.server.gen.transport.TransportProtos; @@ -50,6 +50,7 @@ import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingRes import org.thingsboard.server.service.stats.DefaultRuleEngineStatisticsService; import org.thingsboard.server.service.stats.RuleEngineStatisticsService; +import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -66,7 +67,6 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE; @DaoSqlTest @TestPropertySource(properties = { @@ -81,7 +81,7 @@ public class BaseQueueControllerTest extends AbstractControllerTest { @SpyBean private TimeseriesDao timeseriesDao; @Autowired - private AssetService assetService; + private QueueStatsService queueStatsService; @Test public void testQueueWithServiceTypeRE() throws Exception { @@ -176,16 +176,17 @@ public class BaseQueueControllerTest extends AbstractControllerTest { }); ruleEngineStatisticsService.reportQueueStats(System.currentTimeMillis(), testStats); - Asset serviceAsset = assetService.findAssetsByTenantIdAndType(tenantId, TB_SERVICE_QUEUE, new PageLink(100)).getData() - .stream().filter(asset -> asset.getName().startsWith(queue.getName())) - .findFirst().get(); + List queueStatsList = queueStatsService.findByTenantId(tenantId); + assertThat(queueStatsList).hasSize(1); + QueueStats queueStats = queueStatsList.get(0); + assertThat(queueStats.getQueueName()).isEqualTo(queue.getName()); ArgumentCaptor ttlCaptor = ArgumentCaptor.forClass(Long.class); - verify(timeseriesDao).save(eq(tenantId), eq(serviceAsset.getId()), argThat(tsKvEntry -> { + verify(timeseriesDao).save(eq(tenantId), eq(queueStats.getId()), argThat(tsKvEntry -> { return tsKvEntry.getKey().equals(TbRuleEngineConsumerStats.SUCCESSFUL_MSGS) && tsKvEntry.getLongValue().get().equals(5L); }), ttlCaptor.capture()); - verify(timeseriesDao).save(eq(tenantId), eq(serviceAsset.getId()), argThat(tsKvEntry -> { + verify(timeseriesDao).save(eq(tenantId), eq(queueStats.getId()), argThat(tsKvEntry -> { return tsKvEntry.getKey().equals(TbRuleEngineConsumerStats.FAILED_MSGS) && tsKvEntry.getLongValue().get().equals(5L); }), ttlCaptor.capture()); @@ -193,7 +194,7 @@ public class BaseQueueControllerTest extends AbstractControllerTest { assertThat(usedTtl).isEqualTo(TimeUnit.DAYS.toSeconds(queueStatsTtlDays)); }); - verify(timeseriesDao).save(eq(tenantId), eq(serviceAsset.getId()), argThat(tsKvEntry -> { + verify(timeseriesDao).save(eq(tenantId), eq(queueStats.getId()), argThat(tsKvEntry -> { return tsKvEntry.getKey().equals(DefaultRuleEngineStatisticsService.RULE_ENGINE_EXCEPTION) && tsKvEntry.getJsonValue().get().equals(ruleEngineException.toJsonString(0)); }), ttlCaptor.capture()); diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index 61d86994eb..73a3a2e25e 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -76,6 +76,7 @@ import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; @@ -631,7 +632,7 @@ public class EdgeControllerTest extends AbstractControllerTest { List loadedEdges = new ArrayList<>(); PageLink pageLink = new PageLink(23); - PageData pageData = null; + PageData pageData; do { pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?", new TypeReference<>() { @@ -885,6 +886,7 @@ public class EdgeControllerTest extends AbstractControllerTest { EdgeImitator edgeImitator = new EdgeImitator(EDGE_HOST, EDGE_PORT, edge.getRoutingKey(), edge.getSecret()); edgeImitator.ignoreType(UserCredentialsUpdateMsg.class); + edgeImitator.ignoreType(OAuth2UpdateMsg.class); edgeImitator.expectMessageAmount(24); edgeImitator.connect(); @@ -973,8 +975,7 @@ public class EdgeControllerTest extends AbstractControllerTest { private boolean popQueueMsg(List messages, UpdateMsgType msgType, String name) { for (AbstractMessage message : messages) { - if (message instanceof QueueUpdateMsg) { - QueueUpdateMsg queueUpdateMsg = (QueueUpdateMsg) message; + if (message instanceof QueueUpdateMsg queueUpdateMsg) { Queue queue = JacksonUtil.fromString(queueUpdateMsg.getEntity(), Queue.class, true); Assert.assertNotNull(queue); if (msgType.equals(queueUpdateMsg.getMsgType()) && name.equals(queue.getName())) { @@ -988,8 +989,7 @@ public class EdgeControllerTest extends AbstractControllerTest { private boolean popRuleChainMsg(List messages, UpdateMsgType msgType, String name) { for (AbstractMessage message : messages) { - if (message instanceof RuleChainUpdateMsg) { - RuleChainUpdateMsg ruleChainUpdateMsg = (RuleChainUpdateMsg) message; + if (message instanceof RuleChainUpdateMsg ruleChainUpdateMsg) { RuleChain ruleChain = JacksonUtil.fromString(ruleChainUpdateMsg.getEntity(), RuleChain.class, true); Assert.assertNotNull(ruleChain); if (msgType.equals(ruleChainUpdateMsg.getMsgType()) @@ -1005,8 +1005,7 @@ public class EdgeControllerTest extends AbstractControllerTest { private boolean popAdminSettingsMsg(List messages, String key) { for (AbstractMessage message : messages) { - if (message instanceof AdminSettingsUpdateMsg) { - AdminSettingsUpdateMsg adminSettingsUpdateMsg = (AdminSettingsUpdateMsg) message; + if (message instanceof AdminSettingsUpdateMsg adminSettingsUpdateMsg) { AdminSettings adminSettings = JacksonUtil.fromString(adminSettingsUpdateMsg.getEntity(), AdminSettings.class, true); Assert.assertNotNull(adminSettings); if (key.equals(adminSettings.getKey())) { @@ -1020,8 +1019,7 @@ public class EdgeControllerTest extends AbstractControllerTest { private boolean popDeviceProfileMsg(List messages, UpdateMsgType msgType, String name) { for (AbstractMessage message : messages) { - if (message instanceof DeviceProfileUpdateMsg) { - DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) message; + if (message instanceof DeviceProfileUpdateMsg deviceProfileUpdateMsg) { DeviceProfile deviceProfile = JacksonUtil.fromString(deviceProfileUpdateMsg.getEntity(), DeviceProfile.class, true); Assert.assertNotNull(deviceProfile); if (msgType.equals(deviceProfileUpdateMsg.getMsgType()) @@ -1036,8 +1034,7 @@ public class EdgeControllerTest extends AbstractControllerTest { private boolean popDeviceMsg(List messages, UpdateMsgType msgType, String name) { for (AbstractMessage message : messages) { - if (message instanceof DeviceUpdateMsg) { - DeviceUpdateMsg deviceUpdateMsg = (DeviceUpdateMsg) message; + if (message instanceof DeviceUpdateMsg deviceUpdateMsg) { Device device = JacksonUtil.fromString(deviceUpdateMsg.getEntity(), Device.class, true); Assert.assertNotNull(device); if (msgType.equals(deviceUpdateMsg.getMsgType()) @@ -1052,8 +1049,7 @@ public class EdgeControllerTest extends AbstractControllerTest { private boolean popAssetProfileMsg(List messages, UpdateMsgType msgType, String name) { for (AbstractMessage message : messages) { - if (message instanceof AssetProfileUpdateMsg) { - AssetProfileUpdateMsg assetProfileUpdateMsg = (AssetProfileUpdateMsg) message; + if (message instanceof AssetProfileUpdateMsg assetProfileUpdateMsg) { AssetProfile assetProfile = JacksonUtil.fromString(assetProfileUpdateMsg.getEntity(), AssetProfile.class, true); Assert.assertNotNull(assetProfile); if (msgType.equals(assetProfileUpdateMsg.getMsgType()) @@ -1068,8 +1064,7 @@ public class EdgeControllerTest extends AbstractControllerTest { private boolean popAssetMsg(List messages, UpdateMsgType msgType, String name) { for (AbstractMessage message : messages) { - if (message instanceof AssetUpdateMsg) { - AssetUpdateMsg assetUpdateMsg = (AssetUpdateMsg) message; + if (message instanceof AssetUpdateMsg assetUpdateMsg) { Asset asset = JacksonUtil.fromString(assetUpdateMsg.getEntity(), Asset.class, true); Assert.assertNotNull(asset); if (msgType.equals(assetUpdateMsg.getMsgType()) @@ -1084,8 +1079,7 @@ public class EdgeControllerTest extends AbstractControllerTest { private boolean popUserMsg(List messages, UpdateMsgType msgType, String email, Authority authority) { for (AbstractMessage message : messages) { - if (message instanceof UserUpdateMsg) { - UserUpdateMsg userUpdateMsg = (UserUpdateMsg) message; + if (message instanceof UserUpdateMsg userUpdateMsg) { User user = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); Assert.assertNotNull(user); if (msgType.equals(userUpdateMsg.getMsgType()) @@ -1101,8 +1095,7 @@ public class EdgeControllerTest extends AbstractControllerTest { private boolean popCustomerMsg(List messages, UpdateMsgType msgType, String title) { for (AbstractMessage message : messages) { - if (message instanceof CustomerUpdateMsg) { - CustomerUpdateMsg customerUpdateMsg = (CustomerUpdateMsg) message; + if (message instanceof CustomerUpdateMsg customerUpdateMsg) { Customer customer = JacksonUtil.fromString(customerUpdateMsg.getEntity(), Customer.class, true); Assert.assertNotNull(customer); if (msgType.equals(customerUpdateMsg.getMsgType()) @@ -1117,8 +1110,7 @@ public class EdgeControllerTest extends AbstractControllerTest { private boolean popTenantMsg(List messages, TenantId tenantId1) { for (AbstractMessage message : messages) { - if (message instanceof TenantUpdateMsg) { - TenantUpdateMsg tenantUpdateMsg = (TenantUpdateMsg) message; + if (message instanceof TenantUpdateMsg tenantUpdateMsg) { Tenant tenant = JacksonUtil.fromString(tenantUpdateMsg.getEntity(), Tenant.class, true); Assert.assertNotNull(tenant); if (UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE.equals(tenantUpdateMsg.getMsgType()) @@ -1133,8 +1125,7 @@ public class EdgeControllerTest extends AbstractControllerTest { private boolean popTenantProfileMsg(List messages, TenantProfileId tenantProfileId) { for (AbstractMessage message : messages) { - if (message instanceof TenantProfileUpdateMsg) { - TenantProfileUpdateMsg tenantProfileUpdateMsg = (TenantProfileUpdateMsg) message; + if (message instanceof TenantProfileUpdateMsg tenantProfileUpdateMsg) { TenantProfile tenantProfile = JacksonUtil.fromString(tenantProfileUpdateMsg.getEntity(), TenantProfile.class, true); Assert.assertNotNull(tenantProfile); if (UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE.equals(tenantProfileUpdateMsg.getMsgType()) @@ -1236,4 +1227,5 @@ public class EdgeControllerTest extends AbstractControllerTest { edgeUpgradeInstructionsService.setAppVersion("3.6.2.6"); Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 67ad2cbe55..8de8514e5b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -22,11 +22,13 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.web.servlet.ResultActions; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; @@ -51,10 +53,13 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.query.TsValue; +import org.thingsboard.server.common.data.queue.QueueStats; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; @@ -69,6 +74,9 @@ public class EntityQueryControllerTest extends AbstractControllerTest { private Tenant savedTenant; private User tenantAdmin; + @Autowired + private QueueStatsService queueStatsService; + @Before public void beforeTest() throws Exception { loginSysAdmin(); @@ -593,4 +601,47 @@ public class EntityQueryControllerTest extends AbstractControllerTest { assertThat(getErrorMessage(result)).contains("Invalid").contains("sort property"); } + @Test + public void testFindQueueStatsEntitiesByQuery() throws Exception { + List queueStatsList = new ArrayList<>(); + for (int i = 0; i < 97; i++) { + QueueStats queueStats = new QueueStats(); + queueStats.setQueueName(StringUtils.randomAlphabetic(5)); + queueStats.setServiceId(StringUtils.randomAlphabetic(5)); + queueStats.setTenantId(savedTenant.getTenantId()); + queueStatsList.add(queueStatsService.save(savedTenant.getId(), queueStats)); + Thread.sleep(1); + } + + EntityTypeFilter entityTypeFilter = new EntityTypeFilter(); + entityTypeFilter.setEntityType(EntityType.QUEUE_STATS); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "queueName"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "queueName"), + new EntityKey(EntityKeyType.ENTITY_FIELD, "serviceId")); + + EntityDataQuery query = new EntityDataQuery(entityTypeFilter, pageLink, entityFields, null, null); + + PageData data = + doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { + }); + + Assert.assertEquals(97, data.getTotalElements()); + Assert.assertEquals(10, data.getTotalPages()); + Assert.assertTrue(data.hasNext()); + Assert.assertEquals(10, data.getData().size()); + data.getData().forEach(entityData -> { + assertThat(entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("queueName")).asString().isNotBlank(); + assertThat(entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("serviceId")).asString().isNotBlank(); + }); + + EntityCountQuery countQuery = new EntityCountQuery(entityTypeFilter); + + Long count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); + Assert.assertEquals(97, count.longValue()); + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java b/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java index 8b0854983a..717c17d437 100644 --- a/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java @@ -494,7 +494,7 @@ public class HomePageApiTest extends AbstractControllerTest { } private OAuth2Info createDefaultOAuth2Info() { - return new OAuth2Info(true, Lists.newArrayList( + return new OAuth2Info(true, false, Lists.newArrayList( OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( OAuth2DomainInfo.builder().name("domain").scheme(SchemeType.MIXED).build() 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 cd47988658..63747d2c19 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java @@ -663,7 +663,7 @@ public class TenantControllerTest extends AbstractControllerTest { savedDifferentTenant.setTenantProfileId(tenantProfile.getId()); savedDifferentTenant = doPost("/api/tenant", savedDifferentTenant, Tenant.class); TenantId tenantId = differentTenantId; - await().atMost(10, TimeUnit.SECONDS) + await().atMost(30, TimeUnit.SECONDS) .until(() -> { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId); return !tpi.getTenantId().get().isSysTenantId(); @@ -677,7 +677,7 @@ public class TenantControllerTest extends AbstractControllerTest { tenantProfile.setIsolatedTbRuleEngine(false); tenantProfile.getProfileData().setQueueConfiguration(Collections.emptyList()); tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); - await().atMost(10, TimeUnit.SECONDS) + await().atMost(30, TimeUnit.SECONDS) .until(() -> partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId) .getTenantId().get().isSysTenantId()); @@ -689,11 +689,11 @@ public class TenantControllerTest extends AbstractControllerTest { submittedMsgs.add(tbMsg.getId()); Thread.sleep(timeLeft / msgs); } - await().atMost(15, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { verify(queueAdmin, times(1)).deleteTopic(eq(isolatedTopic)); }); - await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { for (UUID msgId : submittedMsgs) { verify(actorContext).tell(argThat(msg -> { return msg instanceof QueueToRuleEngineMsg && ((QueueToRuleEngineMsg) msg).getMsg().getId().equals(msgId); @@ -718,13 +718,13 @@ public class TenantControllerTest extends AbstractControllerTest { savedDifferentTenant.setTenantProfileId(tenantProfile.getId()); savedDifferentTenant = doPost("/api/tenant", savedDifferentTenant, Tenant.class); TenantId tenantId = differentTenantId; - await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(30, 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); - await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { verify(actorContext).tell(argThat(msg -> { return msg instanceof QueueToRuleEngineMsg && ((QueueToRuleEngineMsg) msg).getMsg().getId().equals(tbMsg.getId()); })); @@ -732,7 +732,7 @@ public class TenantControllerTest extends AbstractControllerTest { deleteDifferentTenant(); - await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { assertThat(partitionService.getMyPartitions(new QueueKey(ServiceType.TB_RULE_ENGINE, tenantId))).isNull(); assertThatThrownBy(() -> partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tenantId)) .isInstanceOf(TenantNotFoundException.class); @@ -752,7 +752,7 @@ public class TenantControllerTest extends AbstractControllerTest { } private void verifyUsedQueueAndMessage(String queue, TenantId tenantId, EntityId entityId, String msgType, Runnable action, Consumer tpiAssert) { - await().atMost(15, TimeUnit.SECONDS) + await().atMost(30, TimeUnit.SECONDS) .untilAsserted(() -> { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, queue, tenantId, entityId); tpiAssert.accept(tpi); diff --git a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java index d9c35826d4..72930e0327 100644 --- a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java @@ -1109,13 +1109,14 @@ public class UserControllerTest extends AbstractControllerTest { private List getUsersInfo(PageLink pageLink) throws Exception { List loadedCustomerUsers = new ArrayList<>(); - PageData pageData = null; + PageData pageData; do { pageData = doGetTypedWithPageLink("/api/users/info?", new TypeReference<>() { }, pageLink); loadedCustomerUsers.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); + Assert.assertEquals(pageLink.getPageSize(), pageData.getData().size()); } } while (pageData.hasNext()); return loadedCustomerUsers; diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index 0787d08fa7..e7f664477c 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -63,6 +63,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.OAuth2Info; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; @@ -85,6 +86,7 @@ import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; +import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; @@ -140,6 +142,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { installation(); edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); + edgeImitator.ignoreType(OAuth2UpdateMsg.class); edgeImitator.expectMessageAmount(21); edgeImitator.connect(); @@ -538,6 +541,18 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { Assert.assertTrue(customer.isPublic()); } + private void validateOAuth2() throws Exception { + Optional oAuth2UpdateMsgOpt = edgeImitator.findMessageByType(OAuth2UpdateMsg.class); + Assert.assertTrue(oAuth2UpdateMsgOpt.isPresent()); + OAuth2UpdateMsg oAuth2UpdateMsg = oAuth2UpdateMsgOpt.get(); + OAuth2Info oAuth2Info = JacksonUtil.fromString(oAuth2UpdateMsg.getEntity(), OAuth2Info.class, true); + Assert.assertNotNull(oAuth2Info); + OAuth2Info auth2Info = doGet("/api/oauth2/config", OAuth2Info.class); + Assert.assertNotNull(auth2Info); + Assert.assertEquals(oAuth2Info, auth2Info); + testAutoGeneratedCodeByProtobuf(oAuth2UpdateMsg); + } + private void validateSyncCompleted() { Optional syncCompletedMsgOpt = edgeImitator.findMessageByType(SyncCompletedMsg.class); Assert.assertTrue(syncCompletedMsgOpt.isPresent()); diff --git a/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java new file mode 100644 index 0000000000..890ec00913 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java @@ -0,0 +1,290 @@ +/** + * Copyright © 2016-2024 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.edge; + +import com.google.protobuf.AbstractMessage; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.notification.rule.EscalatedNotificationRuleRecipientsConfig; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; +import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.HasSubject; +import org.thingsboard.server.common.data.notification.template.MobileAppDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; +import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTargetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTemplateUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class NotificationEdgeTest extends AbstractEdgeTest { + + @Test + public void testNotificationTemplate() throws Exception { + // create notification template + edgeImitator.expectMessageAmount(1); + NotificationDeliveryMethod[] deliveryMethods = new NotificationDeliveryMethod[]{ + NotificationDeliveryMethod.WEB + }; + NotificationTemplate template = createNotificationTemplate(NotificationType.GENERAL, deliveryMethods); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationTemplateUpdateMsg); + NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = (NotificationTemplateUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, notificationTemplateUpdateMsg.getMsgType()); + NotificationTemplate notificationTemplate = JacksonUtil.fromString(notificationTemplateUpdateMsg.getEntity(), NotificationTemplate.class, true); + Assert.assertNotNull(notificationTemplate); + Assert.assertEquals(template.getId(), notificationTemplate.getId()); + Assert.assertEquals(template.getName(), notificationTemplate.getName()); + Assert.assertEquals(template.getNotificationType(), notificationTemplate.getNotificationType()); + + // update notification template + edgeImitator.expectMessageAmount(1); + template.setName(StringUtils.randomAlphanumeric(15)); + saveNotificationTemplate(template); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationTemplateUpdateMsg); + notificationTemplateUpdateMsg = (NotificationTemplateUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, notificationTemplateUpdateMsg.getMsgType()); + notificationTemplate = JacksonUtil.fromString(notificationTemplateUpdateMsg.getEntity(), NotificationTemplate.class, true); + Assert.assertNotNull(notificationTemplate); + Assert.assertEquals(template.getId(), notificationTemplate.getId()); + Assert.assertEquals(template.getName(), notificationTemplate.getName()); + Assert.assertEquals(template.getNotificationType(), notificationTemplate.getNotificationType()); + + // delete notification template + edgeImitator.expectMessageAmount(1); + doDelete("/api/notification/template/" + notificationTemplate.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationTemplateUpdateMsg); + notificationTemplateUpdateMsg = (NotificationTemplateUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, notificationTemplateUpdateMsg.getMsgType()); + Assert.assertEquals(notificationTemplate.getUuidId().getMostSignificantBits(), notificationTemplateUpdateMsg.getIdMSB()); + Assert.assertEquals(notificationTemplate.getUuidId().getLeastSignificantBits(), notificationTemplateUpdateMsg.getIdLSB()); + } + + @Test + public void testNotificationTarget() throws Exception { + // create notification target + edgeImitator.expectMessageAmount(1); + NotificationTarget target = createNotificationTarget(); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationTargetUpdateMsg); + NotificationTargetUpdateMsg notificationTargetUpdateMsg = (NotificationTargetUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, notificationTargetUpdateMsg.getMsgType()); + NotificationTarget notificationTarget = JacksonUtil.fromString(notificationTargetUpdateMsg.getEntity(), NotificationTarget.class, true); + Assert.assertNotNull(notificationTarget); + Assert.assertEquals(target, notificationTarget); + + // update notification target + edgeImitator.expectMessageAmount(1); + target.setName(StringUtils.randomAlphanumeric(15)); + target = saveNotificationTarget(target); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationTargetUpdateMsg); + notificationTargetUpdateMsg = (NotificationTargetUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, notificationTargetUpdateMsg.getMsgType()); + notificationTarget = JacksonUtil.fromString(notificationTargetUpdateMsg.getEntity(), NotificationTarget.class, true); + Assert.assertNotNull(notificationTarget); + Assert.assertEquals(target, notificationTarget); + + // delete notification target + edgeImitator.expectMessageAmount(1); + doDelete("/api/notification/target/" + notificationTarget.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationTargetUpdateMsg); + notificationTargetUpdateMsg = (NotificationTargetUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, notificationTargetUpdateMsg.getMsgType()); + Assert.assertEquals(notificationTarget.getUuidId().getMostSignificantBits(), notificationTargetUpdateMsg.getIdMSB()); + Assert.assertEquals(notificationTarget.getUuidId().getLeastSignificantBits(), notificationTargetUpdateMsg.getIdLSB()); + } + + @Test + public void testNotificationRule() throws Exception { + // create notification template for notification rule + edgeImitator.expectMessageAmount(1); + NotificationDeliveryMethod[] deliveryMethods = new NotificationDeliveryMethod[]{ + NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL + }; + NotificationTemplate template = createNotificationTemplate(NotificationType.EDGE_CONNECTION, deliveryMethods); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationTemplateUpdateMsg); + NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = (NotificationTemplateUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, notificationTemplateUpdateMsg.getMsgType()); + NotificationTemplate notificationTemplate = JacksonUtil.fromString(notificationTemplateUpdateMsg.getEntity(), NotificationTemplate.class, true); + Assert.assertNotNull(notificationTemplate); + Assert.assertEquals(template.getId(), notificationTemplate.getId()); + Assert.assertEquals(template.getName(), notificationTemplate.getName()); + Assert.assertEquals(template.getNotificationType(), notificationTemplate.getNotificationType()); + + // create notification rule + edgeImitator.expectMessageAmount(1); + NotificationRule rule = createNotificationRule(template); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationRuleUpdateMsg); + NotificationRuleUpdateMsg notificationRuleUpdateMsg = (NotificationRuleUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, notificationRuleUpdateMsg.getMsgType()); + NotificationRule notificationRule = JacksonUtil.fromString(notificationRuleUpdateMsg.getEntity(), NotificationRule.class, true); + Assert.assertNotNull(notificationRule); + Assert.assertEquals(rule, notificationRule); + + // update notification rule + edgeImitator.expectMessageAmount(1); + rule.setEnabled(false); + rule = saveNotificationRule(rule); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationRuleUpdateMsg); + notificationRuleUpdateMsg = (NotificationRuleUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, notificationRuleUpdateMsg.getMsgType()); + notificationRule = JacksonUtil.fromString(notificationRuleUpdateMsg.getEntity(), NotificationRule.class, true); + Assert.assertNotNull(notificationRule); + Assert.assertEquals(rule, notificationRule); + + // delete notification rule + edgeImitator.expectMessageAmount(1); + doDelete("/api/notification/rule/" + notificationRule.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationRuleUpdateMsg); + notificationRuleUpdateMsg = (NotificationRuleUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, notificationRuleUpdateMsg.getMsgType()); + Assert.assertEquals(notificationRule.getUuidId().getMostSignificantBits(), notificationRuleUpdateMsg.getIdMSB()); + Assert.assertEquals(notificationRule.getUuidId().getLeastSignificantBits(), notificationRuleUpdateMsg.getIdLSB()); + + // delete notification template + edgeImitator.expectMessageAmount(1); + doDelete("/api/notification/template/" + notificationTemplate.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationTemplateUpdateMsg); + notificationTemplateUpdateMsg = (NotificationTemplateUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, notificationTemplateUpdateMsg.getMsgType()); + Assert.assertEquals(notificationTemplate.getUuidId().getMostSignificantBits(), notificationTemplateUpdateMsg.getIdMSB()); + Assert.assertEquals(notificationTemplate.getUuidId().getLeastSignificantBits(), notificationTemplateUpdateMsg.getIdLSB()); + } + + private NotificationTemplate createNotificationTemplate(NotificationType notificationType, NotificationDeliveryMethod... deliveryMethods) { + NotificationTemplate notificationTemplate = new NotificationTemplate(); + notificationTemplate.setTenantId(tenantId); + notificationTemplate.setName(StringUtils.randomAlphanumeric(15)); + notificationTemplate.setNotificationType(notificationType); + NotificationTemplateConfig config = new NotificationTemplateConfig(); + config.setDeliveryMethodsTemplates(new HashMap<>()); + for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) { + DeliveryMethodNotificationTemplate deliveryMethodNotificationTemplate; + switch (deliveryMethod) { + case WEB -> deliveryMethodNotificationTemplate = new WebDeliveryMethodNotificationTemplate(); + case EMAIL -> deliveryMethodNotificationTemplate = new EmailDeliveryMethodNotificationTemplate(); + case SMS -> deliveryMethodNotificationTemplate = new SmsDeliveryMethodNotificationTemplate(); + case MOBILE_APP -> deliveryMethodNotificationTemplate = new MobileAppDeliveryMethodNotificationTemplate(); + default -> throw new IllegalArgumentException("Unsupported delivery method " + deliveryMethod); + } + deliveryMethodNotificationTemplate.setEnabled(true); + deliveryMethodNotificationTemplate.setBody("Test text"); + if (deliveryMethodNotificationTemplate instanceof HasSubject) { + ((HasSubject) deliveryMethodNotificationTemplate).setSubject("Test subject"); + } + config.getDeliveryMethodsTemplates().put(deliveryMethod, deliveryMethodNotificationTemplate); + } + notificationTemplate.setConfiguration(config); + return saveNotificationTemplate(notificationTemplate); + } + + private NotificationTarget createNotificationTarget() { + NotificationTarget notificationTarget = new NotificationTarget(); + notificationTarget.setTenantId(tenantId); + notificationTarget.setName("Test target"); + + PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig(); + TenantAdministratorsFilter tenantAdministratorsFilter = new TenantAdministratorsFilter(); + tenantAdministratorsFilter.setTenantsIds(Set.of()); + tenantAdministratorsFilter.setTenantProfilesIds(Set.of()); + targetConfig.setUsersFilter(tenantAdministratorsFilter); + notificationTarget.setConfiguration(targetConfig); + return saveNotificationTarget(notificationTarget); + } + + private NotificationRule createNotificationRule(NotificationTemplate notificationTemplate) { + NotificationRule notificationRule = new NotificationRule(); + notificationRule.setName("Web notification on any alarm"); + notificationRule.setEnabled(true); + notificationRule.setTemplateId(notificationTemplate.getId()); + notificationRule.setTriggerType(NotificationRuleTriggerType.ALARM); + + AlarmNotificationRuleTriggerConfig triggerConfig = new AlarmNotificationRuleTriggerConfig(); + triggerConfig.setAlarmTypes(null); + triggerConfig.setAlarmSeverities(null); + triggerConfig.setNotifyOn(Set.of(AlarmNotificationRuleTriggerConfig.AlarmAction.CREATED, AlarmNotificationRuleTriggerConfig.AlarmAction.SEVERITY_CHANGED, AlarmNotificationRuleTriggerConfig.AlarmAction.ACKNOWLEDGED, AlarmNotificationRuleTriggerConfig.AlarmAction.CLEARED)); + notificationRule.setTriggerConfig(triggerConfig); + + EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig(); + recipientsConfig.setTriggerType(NotificationRuleTriggerType.ALARM); + Map> escalationTable = new HashMap<>(); + escalationTable.put(Integer.valueOf("1"), new ArrayList<>()); + recipientsConfig.setEscalationTable(escalationTable); + notificationRule.setRecipientsConfig(recipientsConfig); + return saveNotificationRule(notificationRule); + } + + private NotificationTemplate saveNotificationTemplate(NotificationTemplate notificationTemplate) { + return doPost("/api/notification/template", notificationTemplate, NotificationTemplate.class); + } + + private NotificationTarget saveNotificationTarget(NotificationTarget notificationTarget) { + return doPost("/api/notification/target", notificationTarget, NotificationTarget.class); + } + + private NotificationRule saveNotificationRule(NotificationRule notificationRule) { + return doPost("/api/notification/rule", notificationRule, NotificationRule.class); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java new file mode 100644 index 0000000000..c25410b562 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java @@ -0,0 +1,113 @@ +/** + * Copyright © 2016-2024 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.edge; + +import com.google.common.collect.Lists; +import com.google.protobuf.AbstractMessage; +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.oauth2.MapperType; +import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2DomainInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Info; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2ParamsInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2RegistrationInfo; +import org.thingsboard.server.common.data.oauth2.SchemeType; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; + +import java.util.Arrays; +import java.util.Collections; +import java.util.UUID; + +@DaoSqlTest +public class OAuth2EdgeTest extends AbstractEdgeTest { + + @Test + public void testOAuth2Support() throws Exception { + loginSysAdmin(); + + // enable oauth + edgeImitator.allowIgnoredTypes(); + edgeImitator.expectMessageAmount(1); + OAuth2Info oAuth2Info = createDefaultOAuth2Info(); + oAuth2Info = doPost("/api/oauth2/config", oAuth2Info, OAuth2Info.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof OAuth2UpdateMsg); + OAuth2UpdateMsg oAuth2UpdateMsg = (OAuth2UpdateMsg) latestMessage; + OAuth2Info result = JacksonUtil.fromString(oAuth2UpdateMsg.getEntity(), OAuth2Info.class, true); + Assert.assertEquals(oAuth2Info, result); + + // disable oauth support + edgeImitator.expectMessageAmount(1); + oAuth2Info.setEnabled(false); + oAuth2Info.setEdgeEnabled(false); + doPost("/api/oauth2/config", oAuth2Info, OAuth2Info.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof OAuth2UpdateMsg); + oAuth2UpdateMsg = (OAuth2UpdateMsg) latestMessage; + result = JacksonUtil.fromString(oAuth2UpdateMsg.getEntity(), OAuth2Info.class, true); + Assert.assertEquals(oAuth2Info, result); + + edgeImitator.ignoreType(OAuth2UpdateMsg.class); + loginTenantAdmin(); + } + + private OAuth2Info createDefaultOAuth2Info() { + return new OAuth2Info(true, true, Lists.newArrayList( + OAuth2ParamsInfo.builder() + .domainInfos(Lists.newArrayList( + OAuth2DomainInfo.builder().name("domain").scheme(SchemeType.MIXED).build() + )) + .mobileInfos(Collections.emptyList()) + .clientRegistrations(Lists.newArrayList( + validRegistrationInfo() + )) + .build() + )); + } + + private OAuth2RegistrationInfo validRegistrationInfo() { + return OAuth2RegistrationInfo.builder() + .clientId(UUID.randomUUID().toString()) + .clientSecret(UUID.randomUUID().toString()) + .authorizationUri(UUID.randomUUID().toString()) + .accessTokenUri(UUID.randomUUID().toString()) + .scope(Arrays.asList(UUID.randomUUID().toString(), UUID.randomUUID().toString())) + .platforms(Collections.emptyList()) + .userInfoUri(UUID.randomUUID().toString()) + .userNameAttributeName(UUID.randomUUID().toString()) + .jwkSetUri(UUID.randomUUID().toString()) + .clientAuthenticationMethod(UUID.randomUUID().toString()) + .loginButtonLabel(UUID.randomUUID().toString()) + .mapperConfig( + OAuth2MapperConfig.builder() + .type(MapperType.CUSTOM) + .custom( + OAuth2CustomMapperConfig.builder() + .url(UUID.randomUUID().toString()) + .build() + ) + .build() + ) + .build(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java index 916e5790e9..d470ee686f 100644 --- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java +++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java @@ -44,6 +44,10 @@ import org.thingsboard.server.gen.edge.v1.DownlinkResponseMsg; import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; import org.thingsboard.server.gen.edge.v1.EntityDataProto; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTargetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTemplateUpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; import org.thingsboard.server.gen.edge.v1.OtaPackageUpdateMsg; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; @@ -73,16 +77,16 @@ import java.util.stream.Collectors; @Slf4j public class EdgeImitator { - private String routingKey; - private String routingSecret; + private final String routingKey; + private final String routingSecret; - private EdgeRpcClient edgeRpcClient; + private final EdgeRpcClient edgeRpcClient; private final Lock lock = new ReentrantLock(); private CountDownLatch messagesLatch; private CountDownLatch responsesLatch; - private List> ignoredTypes; + private final List> ignoredTypes; @Setter private boolean randomFailuresOnTimeseriesDownlink = false; @@ -92,7 +96,7 @@ public class EdgeImitator { @Getter private EdgeConfiguration configuration; @Getter - private List downlinkMsgs; + private final List downlinkMsgs; @Getter private UplinkResponseMsg latestResponseMsg; @@ -314,6 +318,26 @@ public class EdgeImitator { result.add(saveDownlinkMsg(resourceUpdateMsg)); } } + if (downlinkMsg.getOAuth2UpdateMsgCount() > 0) { + for (OAuth2UpdateMsg oAuth2UpdateMsg : downlinkMsg.getOAuth2UpdateMsgList()) { + result.add(saveDownlinkMsg(oAuth2UpdateMsg)); + } + } + if (downlinkMsg.getNotificationTemplateUpdateMsgCount() > 0) { + for (NotificationTemplateUpdateMsg notificationTemplateUpdateMsg : downlinkMsg.getNotificationTemplateUpdateMsgList()) { + result.add(saveDownlinkMsg(notificationTemplateUpdateMsg)); + } + } + if (downlinkMsg.getNotificationRuleUpdateMsgCount() > 0) { + for (NotificationRuleUpdateMsg notificationRuleUpdateMsg : downlinkMsg.getNotificationRuleUpdateMsgList()) { + result.add(saveDownlinkMsg(notificationRuleUpdateMsg)); + } + } + if (downlinkMsg.getNotificationTargetUpdateMsgCount() > 0) { + for (NotificationTargetUpdateMsg notificationTargetUpdateMsg : downlinkMsg.getNotificationTargetUpdateMsgList()) { + result.add(saveDownlinkMsg(notificationTargetUpdateMsg)); + } + } if (downlinkMsg.hasEdgeConfiguration()) { result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration())); } diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java index ba8eda3879..9e4fd65ab4 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java @@ -46,6 +46,10 @@ import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.notification.NotificationRuleService; +import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.dao.notification.NotificationTemplateService; +import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; @@ -81,6 +85,8 @@ import org.thingsboard.server.service.edge.rpc.constructor.edge.EdgeMsgConstruct import org.thingsboard.server.service.edge.rpc.constructor.entityview.EntityViewMsgConstructorFactory; import org.thingsboard.server.service.edge.rpc.constructor.entityview.EntityViewMsgConstructorV1; import org.thingsboard.server.service.edge.rpc.constructor.entityview.EntityViewMsgConstructorV2; +import org.thingsboard.server.service.edge.rpc.constructor.notification.NotificationMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.oauth2.OAuth2MsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.ota.OtaPackageMsgConstructorFactory; import org.thingsboard.server.service.edge.rpc.constructor.ota.OtaPackageMsgConstructorV1; import org.thingsboard.server.service.edge.rpc.constructor.ota.OtaPackageMsgConstructorV2; @@ -128,6 +134,8 @@ import org.thingsboard.server.service.edge.rpc.processor.device.profile.DevicePr import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewProcessorFactory; import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewProcessorV1; import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewProcessorV2; +import org.thingsboard.server.service.edge.rpc.processor.notification.NotificationEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.oauth2.OAuth2EdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgeProcessorFactory; import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgeProcessorV1; import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgeProcessorV2; @@ -194,6 +202,15 @@ public abstract class BaseEdgeProcessorTest { @MockBean protected UserService userService; + @MockBean + protected NotificationRuleService notificationRuleService; + + @MockBean + protected NotificationTargetService notificationTargetService; + + @MockBean + protected NotificationTemplateService notificationTemplateService; + @MockBean protected DeviceProfileService deviceProfileService; @@ -239,6 +256,9 @@ public abstract class BaseEdgeProcessorTest { @MockBean protected ResourceService resourceService; + @MockBean + protected OAuth2Service oAuth2Service; + @MockBean @Lazy protected TbQueueProducerProvider producerProvider; @@ -360,6 +380,12 @@ public abstract class BaseEdgeProcessorTest { @MockBean protected WidgetMsgConstructorV2 widgetMsgConstructorV2; + @MockBean + protected NotificationMsgConstructor notificationMsgConstructor; + + @MockBean + protected OAuth2MsgConstructor oAuth2MsgConstructor; + @MockBean protected AlarmEdgeProcessorV1 alarmProcessorV1; @@ -417,6 +443,12 @@ public abstract class BaseEdgeProcessorTest { @MockBean protected RelationEdgeProcessorV2 relationEdgeProcessorV2; + @MockBean + protected OAuth2EdgeProcessor oAuth2EdgeProcessor; + + @MockBean + protected NotificationEdgeProcessor notificationEdgeProcessor; + @SpyBean protected RuleChainMsgConstructorFactory ruleChainMsgConstructorFactory; @@ -514,17 +546,18 @@ public abstract class BaseEdgeProcessorTest { } protected static Stream provideParameters() { - UUID dashoboardUUID = UUID.randomUUID(); - UUID ruleChaindUUID = UUID.randomUUID(); + UUID dashboardUUID = UUID.randomUUID(); + UUID ruleChainUUID = UUID.randomUUID(); return Stream.of( Arguments.of(EdgeVersion.V_3_3_0, 0, 0, 0, 0), Arguments.of(EdgeVersion.V_3_3_3, 0, 0, 0, 0), Arguments.of(EdgeVersion.V_3_4_0, 0, 0, 0, 0), Arguments.of(EdgeVersion.V_3_6_0, - dashoboardUUID.getMostSignificantBits(), - dashoboardUUID.getLeastSignificantBits(), - ruleChaindUUID.getMostSignificantBits(), - ruleChaindUUID.getLeastSignificantBits()) + dashboardUUID.getMostSignificantBits(), + dashboardUUID.getLeastSignificantBits(), + ruleChainUUID.getMostSignificantBits(), + ruleChainUUID.getLeastSignificantBits()) ); } + } diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessorTest.java index a6e28e7968..94714af463 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessorTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessorTest.java @@ -16,27 +16,51 @@ package org.thingsboard.server.service.edge.rpc.processor.telemetry; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessorTest; -@Slf4j -@RunWith(MockitoJUnitRunner.class) -public class TelemetryEdgeProcessorTest { +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; + +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = {TelemetryEdgeProcessor.class}) +@TestPropertySource(properties = { + "edges.rpc.max_telemetry_message_size=1000" +}) +public class TelemetryEdgeProcessorTest extends BaseEdgeProcessorTest { + + @SpyBean + private TelemetryEdgeProcessor telemetryEdgeProcessor; + + @MockBean + private NotificationRuleProcessor notificationRuleProcessor; @Test - public void testConvert_maxSizeLimit() throws Exception { + public void testConvert_maxSizeLimit() { + Edge edge = new Edge(); EdgeEvent edgeEvent = new EdgeEvent(); ObjectNode body = JacksonUtil.newObjectNode(); - body.put("value", StringUtils.randomAlphanumeric(10000)); + body.put("value", StringUtils.randomAlphanumeric(1000)); edgeEvent.setBody(body); - DownlinkMsg downlinkMsg = new TelemetryEdgeProcessor().convertTelemetryEventToDownlink(edgeEvent); + + DownlinkMsg downlinkMsg = telemetryEdgeProcessor.convertTelemetryEventToDownlink(edge, edgeEvent); Assert.assertNull(downlinkMsg); + + verify(notificationRuleProcessor, Mockito.times(1)).process(any()); } + } diff --git a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java index b945c162df..ae9dbe92c4 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java @@ -145,7 +145,7 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest protected NotificationRequest submitNotificationRequest(List targets, String text, int delayInSec, NotificationDeliveryMethod... deliveryMethods) { if (deliveryMethods.length == 0) { - deliveryMethods = new NotificationDeliveryMethod[]{NotificationDeliveryMethod.WEB}; + deliveryMethods = new NotificationDeliveryMethod[]{NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.MOBILE_APP}; } NotificationTemplate notificationTemplate = createNotificationTemplate(DEFAULT_NOTIFICATION_TYPE, DEFAULT_NOTIFICATION_SUBJECT, text, deliveryMethods); return submitNotificationRequest(targets, notificationTemplate.getId(), delayInSec); @@ -247,8 +247,12 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest } protected List getMyNotifications(boolean unreadOnly, int limit) throws Exception { - return doGetTypedWithPageLink("/api/notifications?unreadOnly={unreadOnly}&", new TypeReference>() {}, - new PageLink(limit, 0), unreadOnly).getData(); + return getMyNotifications(NotificationDeliveryMethod.WEB, unreadOnly, limit); + } + + protected List getMyNotifications(NotificationDeliveryMethod deliveryMethod, boolean unreadOnly, int limit) throws Exception { + return doGetTypedWithPageLink("/api/notifications?unreadOnly={unreadOnly}&deliveryMethod={deliveryMethod}&", new TypeReference>() {}, + new PageLink(limit, 0), unreadOnly, deliveryMethod).getData(); } protected NotificationRule createNotificationRule(NotificationRuleTriggerConfig triggerConfig, String subject, String text, NotificationTargetId... targets) { diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java index d3b79029ba..060af9eac1 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationRuleId; @@ -52,6 +53,7 @@ import org.thingsboard.server.common.data.notification.NotificationRequestPrevie import org.thingsboard.server.common.data.notification.NotificationRequestStats; import org.thingsboard.server.common.data.notification.NotificationRequestStatus; import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.notification.info.AlarmCommentNotificationInfo; import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo; import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.settings.MobileAppNotificationDeliveryMethodConfig; @@ -81,7 +83,6 @@ import org.thingsboard.server.common.data.notification.template.WebDeliveryMetho import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.notification.DefaultNotifications; -import org.thingsboard.server.dao.notification.NotificationDao; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.service.notification.channels.MicrosoftTeamsNotificationChannel; import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate; @@ -116,17 +117,25 @@ public class NotificationApiTest extends AbstractNotificationApiTest { @Autowired private NotificationCenter notificationCenter; @Autowired - private NotificationDao notificationDao; - @Autowired private MicrosoftTeamsNotificationChannel microsoftTeamsNotificationChannel; @MockBean private FirebaseService firebaseService; + private static final String TEST_MOBILE_TOKEN = "tenantFcmToken"; + @Before public void beforeEach() throws Exception { loginCustomerUser(); wsClient = getWsClient(); + + loginSysAdmin(); + MobileAppNotificationDeliveryMethodConfig config = new MobileAppNotificationDeliveryMethodConfig(); + config.setFirebaseServiceAccountCredentials("testCredentials"); + saveNotificationSettings(config); + loginTenantAdmin(); + mobileToken = TEST_MOBILE_TOKEN; + doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk()); } @Test @@ -242,7 +251,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest { int notificationsCount = 20; wsClient.registerWaitForUpdate(notificationsCount); for (int i = 1; i <= notificationsCount; i++) { - submitNotificationRequest(target.getId(), "Test " + i, NotificationDeliveryMethod.WEB); + submitNotificationRequest(target.getId(), "Test " + i, NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.MOBILE_APP); } wsClient.waitForUpdate(true); assertThat(wsClient.getLastDataUpdate().getTotalUnreadCount()).isEqualTo(notificationsCount); @@ -306,7 +315,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest { NotificationTarget target = createNotificationTarget(savedDifferentTenantUser.getId()); int notificationsCount = 20; for (int i = 0; i < notificationsCount; i++) { - NotificationRequest request = submitNotificationRequest(target.getId(), "Test " + i, NotificationDeliveryMethod.WEB); + NotificationRequest request = submitNotificationRequest(target.getId(), "Test " + i, NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.MOBILE_APP); awaitNotificationRequest(request.getId()); } List requests = notificationRequestService.findNotificationRequestsByTenantIdAndOriginatorType(tenantId, EntityType.USER, new PageLink(100)).getData(); @@ -500,7 +509,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest { @Test public void testNotificationRequestInfo() throws Exception { NotificationDeliveryMethod[] deliveryMethods = new NotificationDeliveryMethod[]{ - NotificationDeliveryMethod.WEB + NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.MOBILE_APP }; NotificationTemplate template = createNotificationTemplate(NotificationType.GENERAL, "Test subject", "Test text", deliveryMethods); NotificationTarget target = createNotificationTarget(tenantAdminUserId); @@ -525,7 +534,6 @@ public class NotificationApiTest extends AbstractNotificationApiTest { await().atMost(2, TimeUnit.SECONDS) .until(() -> findNotificationRequest(notificationRequest.getId()).isSent()); NotificationRequestStats stats = getStats(notificationRequest.getId()); - assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB)).hasValue(1); } @@ -727,17 +735,12 @@ public class NotificationApiTest extends AbstractNotificationApiTest { @Test public void testMobileAppNotifications() throws Exception { - loginSysAdmin(); - MobileAppNotificationDeliveryMethodConfig config = new MobileAppNotificationDeliveryMethodConfig(); - config.setFirebaseServiceAccountCredentials("testCredentials"); - saveNotificationSettings(config); - loginCustomerUser(); mobileToken = "customerFcmToken"; doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk()); loginTenantAdmin(); - mobileToken = "tenantFcmToken1"; + mobileToken = TEST_MOBILE_TOKEN; doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk()); mobileToken = "tenantFcmToken2"; doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk()); @@ -746,7 +749,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest { loginTenantAdmin(); NotificationTarget target = createNotificationTarget(new AllUsersFilter()); - NotificationTemplate template = createNotificationTemplate(NotificationType.GENERAL, "Title", "Message", NotificationDeliveryMethod.MOBILE_APP); + NotificationTemplate template = createNotificationTemplate(NotificationType.GENERAL, "Title", "Message", NotificationDeliveryMethod.MOBILE_APP, NotificationDeliveryMethod.WEB); ((MobileAppDeliveryMethodNotificationTemplate) template.getConfiguration().getDeliveryMethodsTemplates().get(NotificationDeliveryMethod.MOBILE_APP)) .setAdditionalConfig(JacksonUtil.newObjectNode().set("test", JacksonUtil.newObjectNode().put("test", "test"))); saveNotificationTemplate(template); @@ -758,11 +761,19 @@ public class NotificationApiTest extends AbstractNotificationApiTest { .contains("doesn't use the mobile app"); verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), - eq("tenantFcmToken1"), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test")))); + eq(TEST_MOBILE_TOKEN), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test"))), eq(1)); verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), - eq("tenantFcmToken2"), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test")))); + eq("tenantFcmToken2"), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test"))), eq(1)); verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), - eq("customerFcmToken"), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test")))); + eq("customerFcmToken"), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test"))), eq(1)); + assertThat(getMyNotifications(NotificationDeliveryMethod.MOBILE_APP, true, 10)).singleElement().satisfies(notification -> { + assertThat(notification.getDeliveryMethod()).isEqualTo(NotificationDeliveryMethod.MOBILE_APP); + assertThat(notification.getText()).isEqualTo("Message"); + assertThat(notification.getSubject()).isEqualTo("Title"); + }); + assertThat(getMyNotifications(true, 10)).singleElement().satisfies(notification -> { + assertThat(notification.getDeliveryMethod()).isEqualTo(NotificationDeliveryMethod.WEB); + }); verifyNoMoreInteractions(firebaseService); clearInvocations(firebaseService); @@ -770,26 +781,24 @@ public class NotificationApiTest extends AbstractNotificationApiTest { request = submitNotificationRequest(List.of(target.getId()), template.getId(), 0); awaitNotificationRequest(request.getId()); verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), - eq("tenantFcmToken1"), eq("Title"), eq("Message"), anyMap()); + eq(TEST_MOBILE_TOKEN), eq("Title"), eq("Message"), anyMap(), eq(2)); verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), - eq("customerFcmToken"), eq("Title"), eq("Message"), anyMap()); + eq("customerFcmToken"), eq("Title"), eq("Message"), anyMap(), eq(2)); verifyNoMoreInteractions(firebaseService); + + Integer unreadCount = doGet("/api/notifications/unread/count", Integer.class); + assertThat(unreadCount).isEqualTo(2); } @Test public void testMobileAppNotifications_ruleBased() throws Exception { - loginSysAdmin(); - MobileAppNotificationDeliveryMethodConfig config = new MobileAppNotificationDeliveryMethodConfig(); - config.setFirebaseServiceAccountCredentials("testCredentials"); - saveNotificationSettings(config); - loginTenantAdmin(); - mobileToken = "tenantFcmToken"; + mobileToken = TEST_MOBILE_TOKEN; doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk()); createNotificationRule(AlarmCommentNotificationRuleTriggerConfig.builder().onlyUserComments(true).build(), DefaultNotifications.alarmComment.getSubject(), DefaultNotifications.alarmComment.getText(), - List.of(createNotificationTarget(tenantAdminUserId).getId()), NotificationDeliveryMethod.MOBILE_APP); + List.of(createNotificationTarget(tenantAdminUserId).getId()), NotificationDeliveryMethod.MOBILE_APP, NotificationDeliveryMethod.WEB); Device device = createDevice("test", "test"); UUID alarmDashboardId = UUID.randomUUID(); @@ -802,18 +811,21 @@ public class NotificationApiTest extends AbstractNotificationApiTest { .put("dashboardId", alarmDashboardId.toString())) .build(); alarm = doPost("/api/alarm", alarm, Alarm.class); + AlarmId alarmId = alarm.getId(); AlarmComment comment = new AlarmComment(); comment.setComment(JacksonUtil.newObjectNode() .put("text", "text")); - doPost("/api/alarm/" + alarm.getId() + "/comment", comment, AlarmComment.class); + doPost("/api/alarm/" + alarmId + "/comment", comment, AlarmComment.class); + String expectedSubject = "Comment on 'test' alarm"; + String expectedBody = TENANT_ADMIN_EMAIL + " added comment: text"; ArgumentCaptor> msgCaptor = ArgumentCaptor.forClass(Map.class); await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), - eq("tenantFcmToken"), eq("Comment on 'test' alarm"), - eq(TENANT_ADMIN_EMAIL + " added comment: text"), - msgCaptor.capture()); + eq(TEST_MOBILE_TOKEN), eq(expectedSubject), + eq(expectedBody), + msgCaptor.capture(), eq(1)); }); Map firebaseMessageData = msgCaptor.getValue(); assertThat(firebaseMessageData.keySet()).doesNotContainNull().doesNotContain(""); @@ -823,6 +835,14 @@ public class NotificationApiTest extends AbstractNotificationApiTest { assertThat(firebaseMessageData.get("onClick.enabled")).isEqualTo("true"); assertThat(firebaseMessageData.get("onClick.linkType")).isEqualTo("DASHBOARD"); assertThat(firebaseMessageData.get("onClick.dashboardId")).isEqualTo(alarmDashboardId.toString()); + + assertThat(getMyNotifications(NotificationDeliveryMethod.MOBILE_APP, true, 10)).singleElement().satisfies(notification -> { + assertThat(notification.getDeliveryMethod()).isEqualTo(NotificationDeliveryMethod.MOBILE_APP); + assertThat(notification.getSubject()).isEqualTo(expectedSubject); + assertThat(notification.getText()).isEqualTo(expectedBody); + assertThat(notification.getInfo()).asInstanceOf(type(AlarmCommentNotificationInfo.class)) + .matches(info -> info.getAlarmId().equals(alarmId.getId()) && info.getDashboardId().getId().equals(alarmDashboardId)); + }); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java index 55c521c309..28834a0ab7 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java @@ -15,23 +15,29 @@ */ package org.thingsboard.server.service.script; -import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.test.context.TestPropertySource; -import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.TbStopWatch; import org.thingsboard.script.api.ScriptType; import org.thingsboard.script.api.js.NashornJsInvokeService; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.service.DaoSqlTest; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; @@ -41,8 +47,9 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_RE "js.max_script_body_size=50", "js.max_total_args_size=50", "js.max_result_size=50", - "js.local.max_errors=2" + "js.local.max_errors=2", }) +@Slf4j class NashornJsInvokeServiceTest extends AbstractControllerTest { @Autowired @@ -56,23 +63,64 @@ class NashornJsInvokeServiceTest extends AbstractControllerTest { int iterations = 1000; UUID scriptId = evalScript("return msg.temperature > 20"); // warmup - ObjectNode msg = JacksonUtil.newObjectNode(); - for (int i = 0; i < 100; i++) { - msg.put("temperature", i); + log.info("Warming up 1000 times..."); + var warmupWatch = TbStopWatch.create(); + for (int i = 0; i < 1000; i++) { boolean expected = i > 20; - boolean result = Boolean.valueOf(invokeScript(scriptId, JacksonUtil.toString(msg))); + boolean result = Boolean.parseBoolean(invokeScript(scriptId, "{\"temperature\":" + i + "}")); Assert.assertEquals(expected, result); } - long startTs = System.currentTimeMillis(); + log.info("Warming up finished in {} ms", warmupWatch.stopAndGetTotalTimeMillis()); + log.info("Starting performance test..."); + var watch = TbStopWatch.create(); for (int i = 0; i < iterations; i++) { - msg.put("temperature", i); boolean expected = i > 20; - boolean result = Boolean.valueOf(invokeScript(scriptId, JacksonUtil.toString(msg))); + boolean result = Boolean.parseBoolean(invokeScript(scriptId, "{\"temperature\":" + i + "}")); + log.debug("asserting result"); Assert.assertEquals(expected, result); } - long duration = System.currentTimeMillis() - startTs; - System.out.println(iterations + " invocations took: " + duration + "ms"); - Assert.assertTrue(duration < TimeUnit.MINUTES.toMillis(4)); + long duration = watch.stopAndGetTotalTimeMillis(); + log.info("Performance test with {} invocations took: {} ms", iterations, duration); + assertThat(duration).as("duration ms") + .isLessThan(TimeUnit.MINUTES.toMillis(1)); // effective exec time is about 500ms + } + + @Test + void givenSimpleScriptMultiThreadTestPerformance() throws ExecutionException, InterruptedException, TimeoutException { + int iterations = 1000*4; + List> futures = new ArrayList<>(iterations); + UUID scriptId = evalScript("return msg.temperature > 20 ;"); + // warmup + log.info("Warming up 1000 times..."); + + var warmupWatch = TbStopWatch.create(); + for (int i = 0; i < 1000; i++) { + futures.add(invokeScriptAsync(scriptId, "{\"temperature\":" + i + "}")); + } + List results = Futures.allAsList(futures).get(1, TimeUnit.MINUTES); + for (int i = 0; i < 1000; i++) { + boolean expected = i > 20; + boolean result = Boolean.parseBoolean(results.get(i).toString()); + Assert.assertEquals(expected, result); + } + log.info("Warming up finished in {} ms", warmupWatch.stopAndGetTotalTimeMillis()); + futures.clear(); + + log.info("Starting performance test..."); + var watch = TbStopWatch.create(); + for (int i = 0; i < iterations; i++) { + futures.add(invokeScriptAsync(scriptId, "{\"temperature\":" + i + "}")); + } + results = Futures.allAsList(futures).get(1, TimeUnit.MINUTES); + for (int i = 0; i < iterations; i++) { + boolean expected = i > 20; + boolean result = Boolean.parseBoolean(results.get(i).toString()); + Assert.assertEquals(expected, result); + } + long duration = watch.stopAndGetTotalTimeMillis(); + log.info("Performance test with {} invocations took: {} ms", iterations, duration); + assertThat(duration).as("duration ms") + .isLessThan(TimeUnit.MINUTES.toMillis(1)); // effective exec time is about 500ms } @Test @@ -122,7 +170,11 @@ class NashornJsInvokeServiceTest extends AbstractControllerTest { } private String invokeScript(UUID scriptId, String msg) throws ExecutionException, InterruptedException { - return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", POST_TELEMETRY_REQUEST.name()).get().toString(); + return invokeScriptAsync(scriptId, msg).get().toString(); + } + + private ListenableFuture invokeScriptAsync(UUID scriptId, String msg) { + return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", POST_TELEMETRY_REQUEST.name()); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java index a7de7eb571..9ac2d5fc11 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java @@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.server.resources.Resource; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.coapserver.DefaultCoapServerService; @@ -60,15 +59,11 @@ public class CoapAttributesUpdatesIntegrationTest extends AbstractCoapAttributes processAfterTest(); } - - - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { processJsonTestSubscribeToAttributesUpdates(false); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testSubscribeToAttributesUpdatesFromTheServerWithEmptyCurrentStateNotification() throws Exception { processJsonTestSubscribeToAttributesUpdates(true); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesJsonIntegrationTest.java index 4755ba134d..3fa625796c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesJsonIntegrationTest.java @@ -18,7 +18,6 @@ package org.thingsboard.server.transport.coap.attributes.updates; import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.CoapDeviceType; import org.thingsboard.server.common.data.TransportPayloadType; @@ -45,12 +44,11 @@ public class CoapAttributesUpdatesJsonIntegrationTest extends AbstractCoapAttrib processAfterTest(); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { processJsonTestSubscribeToAttributesUpdates(false); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 + @Test public void testSubscribeToAttributesUpdatesFromTheServerWithEmptyCurrentStateNotification() throws Exception { processJsonTestSubscribeToAttributesUpdates(true); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesProtoIntegrationTest.java index 1c9589bba9..4b07c8dcc2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesProtoIntegrationTest.java @@ -18,7 +18,6 @@ package org.thingsboard.server.transport.coap.attributes.updates; import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.CoapDeviceType; import org.thingsboard.server.common.data.TransportPayloadType; @@ -44,12 +43,10 @@ public class CoapAttributesUpdatesProtoIntegrationTest extends AbstractCoapAttri public void afterTest() throws Exception { processAfterTest(); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { processProtoTestSubscribeToAttributesUpdates(false); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testSubscribeToAttributesUpdatesFromTheServerWithEmptyCurrentStateNotification() throws Exception { processProtoTestSubscribeToAttributesUpdates(true); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimDeviceTest.java index 924792fca1..26bef40c48 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimDeviceTest.java @@ -23,10 +23,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.ClaimRequest; -import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.dao.device.claim.ClaimResponse; import org.thingsboard.server.dao.device.claim.ClaimResult; @@ -45,37 +42,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @DaoSqlTest public class CoapClaimDeviceTest extends AbstractCoapIntegrationTest { - protected static final String CUSTOMER_USER_PASSWORD = "customerUser123!"; - - protected User customerAdmin; - protected Customer savedCustomer; - @Before public void beforeTest() throws Exception { CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder() .deviceName("Test Claim device") .build(); processBeforeTest(configProperties); - createCustomerAndUser(); - } - - protected void createCustomerAndUser() throws Exception { - Customer customer = new Customer(); - customer.setTenantId(tenantId); - customer.setTitle("Test Claiming Customer"); - savedCustomer = doPost("/api/customer", customer, Customer.class); - assertNotNull(savedCustomer); - assertEquals(tenantId, savedCustomer.getTenantId()); - - User user = new User(); - user.setAuthority(Authority.CUSTOMER_USER); - user.setTenantId(tenantId); - user.setCustomerId(savedCustomer.getId()); - user.setEmail("customer@thingsboard.org"); - - customerAdmin = createUser(user, CUSTOMER_USER_PASSWORD); - assertNotNull(customerAdmin); - assertEquals(customerAdmin.getCustomerId(), savedCustomer.getId()); } @After @@ -110,8 +82,9 @@ public class CoapClaimDeviceTest extends AbstractCoapIntegrationTest { protected void validateClaimResponse(boolean emptyPayload, CoapTestClient client, byte[] payloadBytes, byte[] failurePayloadBytes) throws Exception { postClaimRequest(client, failurePayloadBytes); + awaitForClaimingInfoToBeRegistered(savedDevice.getId()); - loginUser(customerAdmin.getName(), CUSTOMER_USER_PASSWORD); + loginCustomerUser(); ClaimRequest claimRequest; if (!emptyPayload) { claimRequest = new ClaimRequest("value"); @@ -128,6 +101,7 @@ public class CoapClaimDeviceTest extends AbstractCoapIntegrationTest { assertEquals(claimResponse, ClaimResponse.FAILURE); postClaimRequest(client, payloadBytes); + awaitForClaimingInfoToBeRegistered(savedDevice.getId()); ClaimResult claimResult = doExecuteWithRetriesAndInterval( () -> doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResult.class, status().isOk()), @@ -138,7 +112,7 @@ public class CoapClaimDeviceTest extends AbstractCoapIntegrationTest { Device claimedDevice = claimResult.getDevice(); assertNotNull(claimedDevice); assertNotNull(claimedDevice.getCustomerId()); - assertEquals(customerAdmin.getCustomerId(), claimedDevice.getCustomerId()); + assertEquals(customerId, claimedDevice.getCustomerId()); claimResponse = doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest()); assertEquals(claimResponse, ClaimResponse.CLAIMED); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimJsonDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimJsonDeviceTest.java index 89ef5e57fe..8e1d663565 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimJsonDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimJsonDeviceTest.java @@ -36,7 +36,6 @@ public class CoapClaimJsonDeviceTest extends CoapClaimDeviceTest { .transportPayloadType(TransportPayloadType.JSON) .build(); processBeforeTest(configProperties); - createCustomerAndUser(); } @After diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimProtoDeviceTest.java index 7dbebac847..eb66e63190 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimProtoDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimProtoDeviceTest.java @@ -39,7 +39,6 @@ public class CoapClaimProtoDeviceTest extends CoapClaimDeviceTest { .transportPayloadType(TransportPayloadType.PROTOBUF) .build(); processBeforeTest(configProperties); - createCustomerAndUser(); } @After diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java index e7a7485ef5..112d3e6aa5 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java @@ -26,7 +26,6 @@ import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.MediaTypeRegistry; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.DeviceId; @@ -69,7 +68,6 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest { private static final List EXPECTED_KEYS = Arrays.asList("key1", "key2", "key3", "key4", "key5"); private static final String DEVICE_RESPONSE = "{\"value1\":\"A\",\"value2\":\"B\"}"; - @Before public void beforeTest() throws Exception { CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder() @@ -83,7 +81,6 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest { processAfterTest(); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testConfirmableRequests() throws Exception { boolean confirmable = true; @@ -92,7 +89,6 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest { processTestRequestAttributesValuesFromTheServer(confirmable); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testNonConfirmableRequests() throws Exception { boolean confirmable = false; diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcDefaultIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcDefaultIntegrationTest.java index 8f9d3379ef..df1159b128 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcDefaultIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcDefaultIntegrationTest.java @@ -20,7 +20,6 @@ import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Assert; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.service.security.AccessValidator; @@ -83,13 +82,11 @@ public class CoapServerSideRpcDefaultIntegrationTest extends AbstractCoapServerS Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testServerCoapOneWayRpc() throws Exception { processOneWayRpcTest(false); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testServerCoapTwoWayRpc() throws Exception { processTwoWayRpcTest("{\"value1\":\"A\",\"value2\":\"B\"}", false); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcJsonIntegrationTest.java index 34d678b5b5..5c94e91b72 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcJsonIntegrationTest.java @@ -18,7 +18,6 @@ package org.thingsboard.server.transport.coap.rpc; import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.CoapDeviceType; import org.thingsboard.server.common.data.TransportPayloadType; @@ -44,13 +43,11 @@ public class CoapServerSideRpcJsonIntegrationTest extends AbstractCoapServerSide processAfterTest(); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testServerCoapOneWayRpc() throws Exception { processOneWayRpcTest(false); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testServerCoapTwoWayRpc() throws Exception { processTwoWayRpcTest("{\"value1\":\"A\",\"value2\":\"B\"}", false); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcProtoIntegrationTest.java index 138f87e964..2f7d46a390 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcProtoIntegrationTest.java @@ -18,7 +18,6 @@ package org.thingsboard.server.transport.coap.rpc; import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.CoapDeviceType; import org.thingsboard.server.common.data.TransportPayloadType; @@ -45,13 +44,11 @@ public class CoapServerSideRpcProtoIntegrationTest extends AbstractCoapServerSid processAfterTest(); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testServerCoapOneWayRpc() throws Exception { processOneWayRpcTest(true); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testServerCoapTwoWayRpc() throws Exception { processTwoWayRpcTest("{\"payload\":\"{\\\"value1\\\":\\\"A\\\",\\\"value2\\\":\\\"B\\\"}\"}", true); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/diffPort/AbstractLwM2MIntegrationDiffPortTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/diffPort/AbstractLwM2MIntegrationDiffPortTest.java index e61b1afb6c..9e9a38925c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/diffPort/AbstractLwM2MIntegrationDiffPortTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/diffPort/AbstractLwM2MIntegrationDiffPortTest.java @@ -22,8 +22,6 @@ import org.eclipse.leshan.core.peer.SocketIdentity; import org.eclipse.leshan.server.registration.RegistrationStore; import org.eclipse.leshan.server.registration.RegistrationUpdate; import org.junit.Assert; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -50,6 +48,17 @@ public abstract class AbstractLwM2MIntegrationDiffPortTest extends AbstractSecur protected void basicTestConnectionDifferentPort(Lwm2mDeviceProfileTransportConfiguration transportConfiguration, String awaitAlias) throws Exception { + doAnswer((invocation) -> { + Object[] arguments = invocation.getArguments(); + log.trace("doAnswer for registrationStoreTest.updateRegistration with args {}", arguments); + int portOld = ((RegistrationUpdate) arguments[0]).getPort(); + int portValueChange = 5; + arguments[0] = registrationUpdateNewPort((RegistrationUpdate) arguments[0], portValueChange); + int portNew = ((RegistrationUpdate) arguments[0]).getPort(); + Assert.assertEquals((portNew - portOld), portValueChange); + return invocation.callRealMethod(); + }).when(registrationStoreTest).updateRegistration(any(RegistrationUpdate.class)); + createDeviceProfile(transportConfiguration); createDevice(deviceCredentials, clientEndpoint); createNewClient(security, null, true, clientEndpoint); @@ -61,23 +70,8 @@ public abstract class AbstractLwM2MIntegrationDiffPortTest extends AbstractSecur await(awaitAlias) .atMost(40, TimeUnit.SECONDS) - .until(() -> { - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - Object[] arguments = invocation.getArguments(); - if (arguments.length > 0 && arguments[0] instanceof RegistrationUpdate) { - int portOld = ((RegistrationUpdate) arguments[0]).getPort(); - int portValueChange = 5; - arguments[0] = registrationUpdateNewPort((RegistrationUpdate) arguments[0], portValueChange); - int portNew = ((RegistrationUpdate) arguments[0]).getPort(); - Assert.assertEquals((portNew - portOld), portValueChange); - } - return invocation.callRealMethod(); - } - }).when(registrationStoreTest).updateRegistration(any(RegistrationUpdate.class)); - return lwM2MTestClient.getClientStates().contains(ON_UPDATE_SUCCESS); - }); + .until(() -> lwM2MTestClient.getClientStates().contains(ON_UPDATE_SUCCESS)); + Assert.assertTrue(lwM2MTestClient.getClientStates().containsAll(expectedStatusesRegistrationLwm2mSuccessUpdate)); } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java index 510854d759..c0aeaf37ef 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java @@ -189,9 +189,9 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg subscribeAndWait(client, attrSubTopic, deviceId, featureType, MqttQoS.AT_MOST_ONCE); } - protected void subscribeAndWait(MqttTestClient client, String attrSubTopic, DeviceId deviceId, FeatureType featureType, MqttQoS mqttQoS) throws MqttException { + protected void subscribeAndWait(MqttTestClient client, String subTopic, DeviceId deviceId, FeatureType featureType, MqttQoS mqttQoS) throws MqttException { int subscriptionCount = getDeviceActorSubscriptionCount(deviceId, featureType); - client.subscribeAndWait(attrSubTopic, mqttQoS); + client.subscribeAndWait(subTopic, mqttQoS); // TODO: This test awaits for the device actor to receive the subscription. Ideally it should not happen. See details below: // The transport layer acknowledge subscription request once the message about subscription is in the queue. // Test sends data immediately after acknowledgement. @@ -200,8 +200,8 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg awaitForDeviceActorToReceiveSubscription(deviceId, featureType, subscriptionCount + 1); } - protected void subscribeAndCheckSubscription(MqttTestClient client, String attrSubTopic, DeviceId deviceId, FeatureType featureType) throws MqttException { - client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE); + protected void subscribeAndCheckSubscription(MqttTestClient client, String subTopic, DeviceId deviceId, FeatureType featureType) throws MqttException { + client.subscribeAndWait(subTopic, MqttQoS.AT_MOST_ONCE); // TODO: This test awaits for the device actor to receive the subscription. Ideally it should not happen. See details below: // The transport layer acknowledge subscription request once the message about subscription is in the queue. // Test sends data immediately after acknowledgement. diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimBackwardCompatibilityDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimBackwardCompatibilityDeviceTest.java index feab658a9b..96e45d51c0 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimBackwardCompatibilityDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimBackwardCompatibilityDeviceTest.java @@ -36,7 +36,6 @@ public class MqttClaimBackwardCompatibilityDeviceTest extends MqttClaimDeviceTes .useJsonPayloadFormatForDefaultDownlinkTopics(true) .build(); processBeforeTest(configProperties); - createCustomerAndUser(); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimDeviceTest.java index b253136b6e..fbc12e48ba 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimDeviceTest.java @@ -19,10 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.ClaimRequest; -import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.device.claim.ClaimResponse; import org.thingsboard.server.dao.device.claim.ClaimResult; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -41,11 +38,6 @@ import static org.thingsboard.server.common.data.device.profile.MqttTopics.GATEW @DaoSqlTest public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { - protected static final String CUSTOMER_USER_PASSWORD = "customerUser123!"; - - protected User customerAdmin; - protected Customer savedCustomer; - @Before public void beforeTest() throws Exception { MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder() @@ -53,26 +45,6 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { .gatewayName("Test Claim gateway") .build(); processBeforeTest(configProperties); - createCustomerAndUser(); - } - - protected void createCustomerAndUser() throws Exception { - Customer customer = new Customer(); - customer.setTenantId(tenantId); - customer.setTitle("Test Claiming Customer"); - savedCustomer = doPost("/api/customer", customer, Customer.class); - assertNotNull(savedCustomer); - assertEquals(tenantId, savedCustomer.getTenantId()); - - User user = new User(); - user.setAuthority(Authority.CUSTOMER_USER); - user.setTenantId(tenantId); - user.setCustomerId(savedCustomer.getId()); - user.setEmail("customer@thingsboard.org"); - - customerAdmin = createUser(user, CUSTOMER_USER_PASSWORD); - assertNotNull(customerAdmin); - assertEquals(customerAdmin.getCustomerId(), savedCustomer.getId()); } @Test @@ -113,8 +85,9 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { protected void validateClaimResponse(boolean emptyPayload, MqttTestClient client, byte[] payloadBytes, byte[] failurePayloadBytes) throws Exception { client.publishAndWait(DEVICE_CLAIM_TOPIC, failurePayloadBytes); + awaitForClaimingInfoToBeRegistered(savedDevice.getId()); - loginUser(customerAdmin.getName(), CUSTOMER_USER_PASSWORD); + loginCustomerUser(); ClaimRequest claimRequest; if (!emptyPayload) { claimRequest = new ClaimRequest("value"); @@ -132,6 +105,7 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { client.publishAndWait(DEVICE_CLAIM_TOPIC, payloadBytes); client.disconnect(); + awaitForClaimingInfoToBeRegistered(savedDevice.getId()); ClaimResult claimResult = doExecuteWithRetriesAndInterval( () -> doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResult.class, status().isOk()), @@ -142,7 +116,7 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { Device claimedDevice = claimResult.getDevice(); assertNotNull(claimedDevice); assertNotNull(claimedDevice.getCustomerId()); - assertEquals(customerAdmin.getCustomerId(), claimedDevice.getCustomerId()); + assertEquals(customerId, claimedDevice.getCustomerId()); claimResponse = doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest()); assertEquals(claimResponse, ClaimResponse.CLAIMED); @@ -158,8 +132,9 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { ); assertNotNull(savedDevice); + awaitForClaimingInfoToBeRegistered(savedDevice.getId()); - loginUser(customerAdmin.getName(), CUSTOMER_USER_PASSWORD); + loginCustomerUser(); ClaimRequest claimRequest; if (!emptyPayload) { claimRequest = new ClaimRequest("value"); @@ -172,6 +147,7 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { client.publishAndWait(GATEWAY_CLAIM_TOPIC, payloadBytes); client.disconnect(); + awaitForClaimingInfoToBeRegistered(savedDevice.getId()); ClaimResult claimResult = doExecuteWithRetriesAndInterval( () -> doPostClaimAsync("/api/customer/device/" + deviceName + "/claim", claimRequest, ClaimResult.class, status().isOk()), @@ -183,7 +159,7 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { Device claimedDevice = claimResult.getDevice(); assertNotNull(claimedDevice); assertNotNull(claimedDevice.getCustomerId()); - assertEquals(customerAdmin.getCustomerId(), claimedDevice.getCustomerId()); + assertEquals(customerId, claimedDevice.getCustomerId()); claimResponse = doPostClaimAsync("/api/customer/device/" + deviceName + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest()); assertEquals(claimResponse, ClaimResponse.CLAIMED); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimJsonDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimJsonDeviceTest.java index a591933cf7..c567ef25d1 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimJsonDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimJsonDeviceTest.java @@ -34,7 +34,6 @@ public class MqttClaimJsonDeviceTest extends MqttClaimDeviceTest { .transportPayloadType(TransportPayloadType.JSON) .build(); processBeforeTest(configProperties); - createCustomerAndUser(); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimProtoDeviceTest.java index d4276eb211..a8bc81cee5 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimProtoDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimProtoDeviceTest.java @@ -36,7 +36,6 @@ public class MqttClaimProtoDeviceTest extends MqttClaimDeviceTest { .transportPayloadType(TransportPayloadType.PROTOBUF) .build(); processBeforeTest(configProperties); - createCustomerAndUser(); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/claim/AbstractMqttV5ClaimTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/claim/AbstractMqttV5ClaimTest.java index 7213ecafb5..00a190f6d0 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/claim/AbstractMqttV5ClaimTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/claim/AbstractMqttV5ClaimTest.java @@ -17,10 +17,7 @@ package org.thingsboard.server.transport.mqtt.mqttv5.claim; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.ClaimRequest; -import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.device.claim.ClaimResponse; import org.thingsboard.server.dao.device.claim.ClaimResult; import org.thingsboard.server.transport.mqtt.mqttv5.AbstractMqttV5Test; @@ -33,10 +30,6 @@ import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVIC @Slf4j public abstract class AbstractMqttV5ClaimTest extends AbstractMqttV5Test { - protected static final String CUSTOMER_USER_PASSWORD = "customerUser123!"; - - protected User customerAdmin; - protected Customer savedCustomer; protected void processTestClaimingDevice() throws Exception { MqttV5TestClient client = new MqttV5TestClient(); @@ -50,8 +43,9 @@ public abstract class AbstractMqttV5ClaimTest extends AbstractMqttV5Test { protected void validateClaimResponse(MqttV5TestClient client, byte[] payloadBytes, byte[] failurePayloadBytes) throws Exception { client.publishAndWait(DEVICE_CLAIM_TOPIC, failurePayloadBytes); + awaitForClaimingInfoToBeRegistered(savedDevice.getId()); - loginUser(customerAdmin.getName(), CUSTOMER_USER_PASSWORD); + loginCustomerUser(); ClaimRequest claimRequest = new ClaimRequest("value"); ClaimResponse claimResponse = doExecuteWithRetriesAndInterval( @@ -64,6 +58,7 @@ public abstract class AbstractMqttV5ClaimTest extends AbstractMqttV5Test { client.publishAndWait(DEVICE_CLAIM_TOPIC, payloadBytes); client.disconnect(); + awaitForClaimingInfoToBeRegistered(savedDevice.getId()); ClaimResult claimResult = doExecuteWithRetriesAndInterval( () -> doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResult.class, status().isOk()), @@ -74,28 +69,10 @@ public abstract class AbstractMqttV5ClaimTest extends AbstractMqttV5Test { Device claimedDevice = claimResult.getDevice(); assertNotNull(claimedDevice); assertNotNull(claimedDevice.getCustomerId()); - assertEquals(customerAdmin.getCustomerId(), claimedDevice.getCustomerId()); + assertEquals(customerId, claimedDevice.getCustomerId()); claimResponse = doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest()); assertEquals(claimResponse, ClaimResponse.CLAIMED); } - protected void createCustomerAndUser() throws Exception { - Customer customer = new Customer(); - customer.setTenantId(tenantId); - customer.setTitle("Test Claiming Customer"); - savedCustomer = doPost("/api/customer", customer, Customer.class); - assertNotNull(savedCustomer); - assertEquals(tenantId, savedCustomer.getTenantId()); - - User user = new User(); - user.setAuthority(Authority.CUSTOMER_USER); - user.setTenantId(tenantId); - user.setCustomerId(savedCustomer.getId()); - user.setEmail("customer@thingsboard.org"); - - customerAdmin = createUser(user, CUSTOMER_USER_PASSWORD); - assertNotNull(customerAdmin); - assertEquals(customerAdmin.getCustomerId(), savedCustomer.getId()); - } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/claim/MqttV5ClaimTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/claim/MqttV5ClaimTest.java index dcabe07de1..75e884f9fd 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/claim/MqttV5ClaimTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/claim/MqttV5ClaimTest.java @@ -29,7 +29,6 @@ public class MqttV5ClaimTest extends AbstractMqttV5ClaimTest { .deviceName("Test Claim device") .build(); processBeforeTest(configProperties); - createCustomerAndUser(); } @Test diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index 981bcab132..8b20a2817a 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -16,7 +16,7 @@ - + @@ -37,6 +37,9 @@ + + + diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index 203fe8e744..a6e6e2390b 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -106,6 +106,8 @@ public interface AlarmService extends EntityDaoService { PageData findAlarmIdsByAssigneeId(TenantId tenantId, UserId userId, PageLink pageLink); + PageData findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, PageLink pageLink); + void deleteEntityAlarmRelations(TenantId tenantId, EntityId entityId); void deleteEntityAlarmRecordsByTenantId(TenantId tenantId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java index 9b0d5a280d..ef6ebdcdd7 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java @@ -35,6 +35,8 @@ public interface EventService { List findLatestEvents(TenantId tenantId, EntityId entityId, EventType eventType, int limit); + EventInfo findLatestDebugRuleNodeInEvent(TenantId tenantId, EntityId entityId); + PageData findEventsByFilter(TenantId tenantId, EntityId entityId, EventFilter eventFilter, TimePageLink pageLink); void removeEvents(TenantId tenantId, EntityId entityId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationService.java index 18c9d4a7fb..91f6f7a379 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationService.java @@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -30,13 +31,13 @@ public interface NotificationService { boolean markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId); - int markAllNotificationsAsRead(TenantId tenantId, UserId recipientId); + int markAllNotificationsAsRead(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId); - PageData findNotificationsByRecipientIdAndReadStatus(TenantId tenantId, UserId recipientId, boolean unreadOnly, PageLink pageLink); + PageData findNotificationsByRecipientIdAndReadStatus(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, boolean unreadOnly, PageLink pageLink); - PageData findLatestUnreadNotificationsByRecipientId(TenantId tenantId, UserId recipientId, int limit); + PageData findLatestUnreadNotificationsByRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, int limit); - int countUnreadNotificationsByRecipientId(TenantId tenantId, UserId recipientId); + int countUnreadNotificationsByRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId); boolean deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java index 941432b879..11129002e7 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.UUID; public interface OAuth2Service { + List getOAuth2Clients(String domainScheme, String domainName, String pkgName, PlatformType platformType); void saveOAuth2Info(OAuth2Info oauth2Info); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueStatsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueStatsService.java new file mode 100644 index 0000000000..9ab67c73a7 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueStatsService.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2024 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.dao.queue; + +import org.thingsboard.server.common.data.id.QueueStatsId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.QueueStats; +import org.thingsboard.server.dao.entity.EntityDaoService; + +import java.util.List; + +public interface QueueStatsService extends EntityDaoService { + + QueueStats save(TenantId tenantId, QueueStats queueStats); + + QueueStats findQueueStatsById(TenantId tenantId, QueueStatsId queueStatsId); + + QueueStats findByTenantIdAndNameAndServiceId(TenantId tenantId, String queueName, String serviceId); + + List findByTenantId(TenantId tenantId); + + void deleteByTenantId(TenantId tenantId); + +} \ No newline at end of file diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java index 11a71eaff5..bef82f921f 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.List; +import java.util.function.Consumer; public interface TenantService extends EntityDaoService { @@ -36,7 +37,7 @@ public interface TenantService extends EntityDaoService { Tenant saveTenant(Tenant tenant); - Tenant saveTenant(Tenant tenant, boolean publishSaveEvent); + Tenant saveTenant(Tenant tenant, Consumer defaultEntitiesCreator); boolean tenantExists(TenantId tenantId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java index 096a39bff6..ffea217da7 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java @@ -38,12 +38,10 @@ public interface TimeseriesService { ListenableFuture> findAll(TenantId tenantId, EntityId entityId, List queries); - ListenableFuture> findLatest(TenantId tenantId, EntityId entityId, String keys); + ListenableFuture> findLatest(TenantId tenantId, EntityId entityId, String key); ListenableFuture> findLatest(TenantId tenantId, EntityId entityId, Collection keys); - List findLatestSync(TenantId tenantId, EntityId entityId, Collection keys); - ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId); ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java index 9f0100dbb1..398cd17aa1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java @@ -36,7 +36,7 @@ public class Customer extends ContactBased implements HasTenantId, E @NoXss @Length(fieldName = "title") - @Schema(required = true, description = "Title of the customer", example = "Company A") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Title of the customer", example = "Company A") private String title; @Schema(description = "JSON object with Tenant Id") private TenantId tenantId; @@ -132,7 +132,7 @@ public class Customer extends ContactBased implements HasTenantId, E return super.getPhone(); } - @Schema(required = true, description = "Email", example = "example@company.com") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Email", example = "example@company.com") @Override public String getEmail() { return super.getEmail(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java index be0f7ce109..238ef8744c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java @@ -85,7 +85,7 @@ public class DashboardInfo extends BaseData implements HasName, Has this.tenantId = tenantId; } - @Schema(required = true, description = "Title of the dashboard.") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Title of the dashboard.") public String getTitle() { return title; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java index 13df99ca50..e7c37d3ed1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java @@ -136,7 +136,7 @@ public class Device extends BaseDataWithAdditionalInfo implements HasL this.customerId = customerId; } - @Schema(required = true, description = "Unique Device Name in scope of Tenant", example = "A4B72CCDFF33") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Unique Device Name in scope of Tenant", example = "A4B72CCDFF33") @Override public String getName() { return name; @@ -164,7 +164,7 @@ public class Device extends BaseDataWithAdditionalInfo implements HasL this.label = label; } - @Schema(required = true, description = "JSON object with Device Profile Id.") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with Device Profile Id.") public DeviceProfileId getDeviceProfileId() { return deviceProfileId; } 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 3580a86bcc..bb43bf4d11 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 @@ -26,7 +26,6 @@ import java.util.stream.Collectors; * @author Andrew Shvayka */ public enum EntityType { - TENANT(1), CUSTOMER(2), USER(3), @@ -59,12 +58,13 @@ public enum EntityType { NOTIFICATION_TEMPLATE (30), NOTIFICATION_REQUEST (31), NOTIFICATION (32), - NOTIFICATION_RULE (33); + NOTIFICATION_RULE (33), + QUEUE_STATS(34); @Getter private final int protoNumber; // Corresponds to EntityTypeProto - EntityType(int protoNumber) { + private EntityType(int protoNumber) { this.protoNumber = protoNumber; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java index 4a4e785dac..344045134e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java @@ -40,17 +40,17 @@ public class EntityView extends BaseDataWithAdditionalInfo private static final long serialVersionUID = 5582010124562018986L; - @Schema(required = true, description = "JSON object with the referenced Entity Id (Device or Asset).") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with the referenced Entity Id (Device or Asset).") private EntityId entityId; private TenantId tenantId; private CustomerId customerId; @NoXss @Length(fieldName = "name") - @Schema(required = true, description = "Entity View name", example = "A4B72CCDFF33") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Entity View name", example = "A4B72CCDFF33") private String name; @NoXss @Length(fieldName = "type") - @Schema(required = true, description = "Device Profile Name", example = "Temperature Sensor") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Device Profile Name", example = "Temperature Sensor") private String type; @Schema(description = "Set of telemetry and attribute keys to expose via Entity View.") private TelemetryEntityView keys; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SaveDeviceWithCredentialsRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/SaveDeviceWithCredentialsRequest.java index bc40710aed..24991bb71d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SaveDeviceWithCredentialsRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SaveDeviceWithCredentialsRequest.java @@ -25,10 +25,10 @@ import jakarta.validation.constraints.NotNull;; @Data public class SaveDeviceWithCredentialsRequest { - @Schema(description = "The JSON with device entity.", required = true) + @Schema(description = "The JSON with device entity.", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull private final Device device; - @Schema(description = "The JSON with credentials entity.", required = true) + @Schema(description = "The JSON with credentials entity.", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull private final DeviceCredentials credentials; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java index 1f649f69e6..7cb7e15395 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java @@ -33,7 +33,7 @@ public class Tenant extends ContactBased implements HasTenantId, HasTi @Length(fieldName = "title") @NoXss - @Schema(required = true, description = "Title of the tenant", example = "Company A") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Title of the tenant", example = "Company A") private String title; @NoXss @Length(fieldName = "region") @@ -152,7 +152,7 @@ public class Tenant extends ContactBased implements HasTenantId, HasTi return super.getPhone(); } - @Schema(required = true, description = "Email", example = "example@company.com") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Email", example = "example@company.com") @Override public String getEmail() { return super.getEmail(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/User.java b/common/data/src/main/java/org/thingsboard/server/common/data/User.java index 2027de62be..678b7cdac6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/User.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/User.java @@ -103,7 +103,7 @@ public class User extends BaseDataWithAdditionalInfo implements HasName, this.customerId = customerId; } - @Schema(required = true, description = "Email of the user", example = "user@example.com") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Email of the user", example = "user@example.com") public String getEmail() { return email; } @@ -119,7 +119,7 @@ public class User extends BaseDataWithAdditionalInfo implements HasName, return email; } - @Schema(required = true, description = "Authority", example = "SYS_ADMIN, TENANT_ADMIN or CUSTOMER_USER") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Authority", example = "SYS_ADMIN, TENANT_ADMIN or CUSTOMER_USER") public Authority getAuthority() { return authority; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java index 04d2b9231c..19e0bc29b1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java @@ -59,16 +59,16 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha private CustomerId customerId; @NoXss - @Schema(required = true, description = "representing type of the Alarm", example = "High Temperature Alarm") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "representing type of the Alarm", example = "High Temperature Alarm") @Length(fieldName = "type") private String type; - @Schema(required = true, description = "JSON object with alarm originator id") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with alarm originator id") private EntityId originator; - @Schema(required = true, description = "Alarm severity", example = "CRITICAL") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Alarm severity", example = "CRITICAL") private AlarmSeverity severity; - @Schema(required = true, description = "Acknowledged", example = "true") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Acknowledged", example = "true") private boolean acknowledged; - @Schema(required = true, description = "Cleared", example = "false") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Cleared", example = "false") private boolean cleared; @Schema(description = "Alarm assignee user id") private UserId assigneeId; @@ -128,7 +128,7 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha @Override @JsonProperty(access = JsonProperty.Access.READ_ONLY) - @Schema(required = true, description = "representing type of the Alarm", example = "High Temperature Alarm") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "representing type of the Alarm", example = "High Temperature Alarm") public String getName() { return type; } @@ -150,7 +150,7 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha } @JsonProperty(access = JsonProperty.Access.READ_ONLY) - @Schema(required = true, description = "status of the Alarm", example = "ACTIVE_UNACK", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "status of the Alarm", example = "ACTIVE_UNACK", accessMode = Schema.AccessMode.READ_ONLY) public AlarmStatus getStatus() { return toStatus(cleared, acknowledged); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java index 54dd8ba71b..c39c603a9e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java @@ -40,14 +40,14 @@ public class AlarmCreateOrUpdateActiveRequest implements AlarmModificationReques @Schema(description = "JSON object with Customer Id", accessMode = Schema.AccessMode.READ_ONLY) private CustomerId customerId; @NotNull - @Schema(required = true, description = "representing type of the Alarm", example = "High Temperature Alarm") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "representing type of the Alarm", example = "High Temperature Alarm") @Length(fieldName = "type") private String type; @NotNull - @Schema(required = true, description = "JSON object with alarm originator id") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with alarm originator id") private EntityId originator; @NotNull - @Schema(required = true, description = "Alarm severity", example = "CRITICAL") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Alarm severity", example = "CRITICAL") private AlarmSeverity severity; @Schema(description = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565") private long startTs; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java index 544d200a27..8eb418d6dc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java @@ -41,7 +41,7 @@ public class AlarmUpdateRequest implements AlarmModificationRequest { "Omit this field to create new alarm.") private AlarmId alarmId; @NotNull - @Schema(required = true, description = "Alarm severity", example = "CRITICAL") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Alarm severity", example = "CRITICAL") private AlarmSeverity severity; @Schema(description = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565") private long startTs; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java index 753a83fbaf..16518bc618 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java @@ -120,7 +120,7 @@ public class Asset extends BaseDataWithAdditionalInfo implements HasLab this.customerId = customerId; } - @Schema(required = true, description = "Unique Asset Name in scope of Tenant", example = "Empire State Building") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Unique Asset Name in scope of Tenant", example = "Empire State Building") @Override public String getName() { return name; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java index d281589734..67d32d6e89 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java @@ -120,13 +120,13 @@ public class Edge extends BaseDataWithAdditionalInfo implements HasLabel return this.rootRuleChainId; } - @Schema(required = true, description = "Unique Edge Name in scope of Tenant", example = "Silo_A_Edge") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Unique Edge Name in scope of Tenant", example = "Silo_A_Edge") @Override public String getName() { return this.name; } - @Schema(required = true, description = "Edge type", example = "Silos") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Edge type", example = "Silos") public String getType() { return this.type; } @@ -136,12 +136,12 @@ public class Edge extends BaseDataWithAdditionalInfo implements HasLabel return this.label; } - @Schema(required = true, description = "Edge routing key ('username') to authorize on cloud") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Edge routing key ('username') to authorize on cloud") public String getRoutingKey() { return this.routingKey; } - @Schema(required = true, description = "Edge secret ('password') to authorize on cloud") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Edge secret ('password') to authorize on cloud") public String getSecret() { return this.secret; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java index 6fc255501f..44a3ed8efe 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java @@ -41,7 +41,11 @@ public enum EdgeEventType { ADMIN_SETTINGS(true, null), OTA_PACKAGE(true, EntityType.OTA_PACKAGE), QUEUE(true, EntityType.QUEUE), - TB_RESOURCE(true, EntityType.TB_RESOURCE); + NOTIFICATION_RULE (true, EntityType.NOTIFICATION_RULE), + NOTIFICATION_TARGET (true, EntityType.NOTIFICATION_TARGET), + NOTIFICATION_TEMPLATE (true, EntityType.NOTIFICATION_TEMPLATE), + TB_RESOURCE(true, EntityType.TB_RESOURCE), + OAUTH2(true, null); private final boolean allEdgesRelated; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java index dc7f632037..6d2a110cf7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java @@ -33,7 +33,7 @@ import io.swagger.v3.oas.annotations.media.Schema; }) public interface EventFilter { - @Schema(required = true, description = "String value representing the event type", example = "STATS") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "String value representing the event type", example = "STATS") EventType getEventType(); boolean isNotEmpty(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java index aae2e3726a..02b4d602ce 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java @@ -36,7 +36,7 @@ public class AlarmId extends UUIDBased implements EntityId { return new AlarmId(UUID.fromString(alarmId)); } - @Schema(required = true, description = "string", example = "ALARM", allowableValues = "ALARM") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ALARM", allowableValues = "ALARM") @Override public EntityType getEntityType() { return EntityType.ALARM; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java index 228b4160a0..ec942731f3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java @@ -34,7 +34,7 @@ public class ApiUsageStateId extends UUIDBased implements EntityId { return new ApiUsageStateId(UUID.fromString(userId)); } - @Schema(required = true, description = "string", example = "API_USAGE_STATE", allowableValues = "API_USAGE_STATE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "API_USAGE_STATE", allowableValues = "API_USAGE_STATE") @Override public EntityType getEntityType() { return EntityType.API_USAGE_STATE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java index 2c70920aeb..a45668e9bd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java @@ -36,7 +36,7 @@ public class AssetId extends UUIDBased implements EntityId { return new AssetId(UUID.fromString(assetId)); } - @Schema(required = true, description = "string", example = "ASSET", allowableValues = "ASSET") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ASSET", allowableValues = "ASSET") @Override public EntityType getEntityType() { return EntityType.ASSET; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java index 0efd6dcf71..d2377c19e6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java @@ -35,7 +35,7 @@ public class AssetProfileId extends UUIDBased implements EntityId { return new AssetProfileId(UUID.fromString(assetProfileId)); } - @Schema(required = true, description = "string", example = "ASSET_PROFILE", allowableValues = "ASSET_PROFILE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ASSET_PROFILE", allowableValues = "ASSET_PROFILE") @Override public EntityType getEntityType() { return EntityType.ASSET_PROFILE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java index 72f380a4bd..d8ef7fd09e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java @@ -32,7 +32,7 @@ public final class CustomerId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "CUSTOMER", allowableValues = "CUSTOMER") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "CUSTOMER", allowableValues = "CUSTOMER") @Override public EntityType getEntityType() { return EntityType.CUSTOMER; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java index 7220919b14..a7c21821ff 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java @@ -34,7 +34,7 @@ public class DashboardId extends UUIDBased implements EntityId { return new DashboardId(UUID.fromString(dashboardId)); } - @Schema(required = true, description = "string", example = "DASHBOARD", allowableValues = "DASHBOARD") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "DASHBOARD", allowableValues = "DASHBOARD") @Override public EntityType getEntityType() { return EntityType.DASHBOARD; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java index d59de6cb07..8fe48b9836 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java @@ -37,7 +37,7 @@ public class DeviceId extends UUIDBased implements EntityId { } @Override - @Schema(required = true, description = "string", example = "DEVICE", allowableValues = "DEVICE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "DEVICE", allowableValues = "DEVICE") public EntityType getEntityType() { return EntityType.DEVICE; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java index 11679436f5..7050fa7ff3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java @@ -35,7 +35,7 @@ public class DeviceProfileId extends UUIDBased implements EntityId { return new DeviceProfileId(UUID.fromString(deviceProfileId)); } - @Schema(required = true, description = "string", example = "DEVICE_PROFILE", allowableValues = "DEVICE_PROFILE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "DEVICE_PROFILE", allowableValues = "DEVICE_PROFILE") @Override public EntityType getEntityType() { return EntityType.DEVICE_PROFILE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java index 1d6c1dad49..5c29c210df 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java @@ -41,7 +41,7 @@ public class EdgeId extends UUIDBased implements EntityId { return new EdgeId(UUID.fromString(edgeId)); } - @Schema(required = true, description = "string", example = "EDGE", allowableValues = "EDGE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "EDGE", allowableValues = "EDGE") @Override public EntityType getEntityType() { return EntityType.EDGE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index eb8b337c7d..ad2df69349 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -103,6 +103,8 @@ public class EntityIdFactory { return new NotificationTemplateId(uuid); case NOTIFICATION: return new NotificationId(uuid); + case QUEUE_STATS: + return new QueueStatsId(uuid); } throw new IllegalArgumentException("EntityType " + type + " is not supported!"); } @@ -145,6 +147,12 @@ public class EntityIdFactory { return new QueueId(uuid); case TB_RESOURCE: return new TbResourceId(uuid); + case NOTIFICATION_RULE: + return new NotificationRuleId(uuid); + case NOTIFICATION_TARGET: + return new NotificationTargetId(uuid); + case NOTIFICATION_TEMPLATE: + return new NotificationTemplateId(uuid); } throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java index a44f35e5d3..7c5f7be1e8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java @@ -38,7 +38,7 @@ public class EntityViewId extends UUIDBased implements EntityId { return new EntityViewId(UUID.fromString(entityViewID)); } - @Schema(required = true, description = "string", example = "ENTITY_VIEW", allowableValues = "ENTITY_VIEW") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ENTITY_VIEW", allowableValues = "ENTITY_VIEW") @Override public EntityType getEntityType() { return EntityType.ENTITY_VIEW; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java index be8aa8e9cd..8fc12993b2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java @@ -29,7 +29,7 @@ public class NotificationId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "NOTIFICATION", allowableValues = "NOTIFICATION") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION", allowableValues = "NOTIFICATION") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java index afcb27f0c9..30c7336b79 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java @@ -29,7 +29,7 @@ public class NotificationRequestId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "NOTIFICATION_REQUEST", allowableValues = "NOTIFICATION_REQUEST") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_REQUEST", allowableValues = "NOTIFICATION_REQUEST") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_REQUEST; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java index dc5e6c8671..c17e42a759 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java @@ -29,7 +29,7 @@ public class NotificationRuleId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "NOTIFICATION_RULE", allowableValues = "NOTIFICATION_RULE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_RULE", allowableValues = "NOTIFICATION_RULE") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_RULE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java index 3966dbbbd7..2984bc7644 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java @@ -29,7 +29,7 @@ public class NotificationTargetId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "NOTIFICATION_TARGET", allowableValues = "NOTIFICATION_TARGET") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_TARGET", allowableValues = "NOTIFICATION_TARGET") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_TARGET; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java index 65f312b7e0..9fb99a578e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java @@ -29,7 +29,7 @@ public class NotificationTemplateId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "NOTIFICATION_TEMPLATE", allowableValues = "NOTIFICATION_TEMPLATE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_TEMPLATE", allowableValues = "NOTIFICATION_TEMPLATE") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_TEMPLATE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java index 3225a72f6f..bc042883ce 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java @@ -35,7 +35,7 @@ public class OtaPackageId extends UUIDBased implements EntityId { return new OtaPackageId(UUID.fromString(firmwareId)); } - @Schema(required = true, description = "string", example = "OTA_PACKAGE", allowableValues = "OTA_PACKAGE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "OTA_PACKAGE", allowableValues = "OTA_PACKAGE") @Override public EntityType getEntityType() { return EntityType.OTA_PACKAGE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java index 7f415fc451..48987a0539 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java @@ -35,9 +35,9 @@ public class QueueId extends UUIDBased implements EntityId { return new QueueId(UUID.fromString(queueId)); } - @Schema(required = true, description = "string", example = "QUEUE", allowableValues = "QUEUE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "QUEUE", allowableValues = "QUEUE") @Override public EntityType getEntityType() { return EntityType.QUEUE; } -} \ No newline at end of file +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java new file mode 100644 index 0000000000..dd36f09313 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import org.thingsboard.server.common.data.EntityType; + +import java.util.UUID; + +public class QueueStatsId extends UUIDBased implements EntityId { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public QueueStatsId(@JsonProperty("id") UUID id) { + super(id); + } + + public static QueueStatsId fromString(String queueId) { + return new QueueStatsId(UUID.fromString(queueId)); + } + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "QUEUE_STATS", allowableValues = "QUEUE_STATS") + @Override + public EntityType getEntityType() { + return EntityType.QUEUE_STATS; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java index b544971d98..c6bf1aff87 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java @@ -31,7 +31,7 @@ public final class RpcId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "RPC", allowableValues = "RPC") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "RPC", allowableValues = "RPC") @Override public EntityType getEntityType() { return EntityType.RPC; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java index 379d1f608e..1557b053c2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java @@ -29,7 +29,7 @@ public class RuleChainId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "RULE_CHAIN", allowableValues = "RULE_CHAIN") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "RULE_CHAIN", allowableValues = "RULE_CHAIN") @Override public EntityType getEntityType() { return EntityType.RULE_CHAIN; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java index b0c1d1c8e5..945c99a89a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java @@ -29,7 +29,7 @@ public class RuleNodeId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "RULE_NODE", allowableValues = "RULE_NODE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "RULE_NODE", allowableValues = "RULE_NODE") @Override public EntityType getEntityType() { return EntityType.RULE_NODE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java index 28d22d04f5..a2ef70b75b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java @@ -31,7 +31,7 @@ public class TbResourceId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "TB_RESOURCE", allowableValues = "TB_RESOURCE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TB_RESOURCE", allowableValues = "TB_RESOURCE") @Override public EntityType getEntityType() { return EntityType.TB_RESOURCE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java index aead099818..e160eb59d0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java @@ -50,7 +50,7 @@ public final class TenantId extends UUIDBased implements EntityId { return this.equals(SYS_TENANT_ID); } - @Schema(required = true, description = "string", example = "TENANT", allowableValues = "TENANT") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TENANT", allowableValues = "TENANT") @Override public EntityType getEntityType() { return EntityType.TENANT; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java index ea2aaa1ac1..7b40eb01ed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java @@ -35,7 +35,7 @@ public class TenantProfileId extends UUIDBased implements EntityId { return new TenantProfileId(UUID.fromString(tenantProfileId)); } - @Schema(required = true, description = "string", example = "TENANT_PROFILE", allowableValues = "TENANT_PROFILE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TENANT_PROFILE", allowableValues = "TENANT_PROFILE") @Override public EntityType getEntityType() { return EntityType.TENANT_PROFILE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java index ac56ccef12..e0a1abf9ff 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java @@ -36,7 +36,7 @@ public abstract class UUIDBased implements HasUUID, Serializable { this.id = id; } - @Schema(required = true, description = "string", example = "784f394c-42b6-435a-983c-b7beff2784f9") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "784f394c-42b6-435a-983c-b7beff2784f9") public UUID getId() { return id; } @@ -68,7 +68,7 @@ public abstract class UUIDBased implements HasUUID, Serializable { @Override public String toString() { - return id.toString(); + return String.valueOf(id); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java index dfa1934499..76a631d4fb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java @@ -31,7 +31,7 @@ public final class WidgetTypeId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "WIDGET_TYPE", allowableValues = "WIDGET_TYPE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "WIDGET_TYPE", allowableValues = "WIDGET_TYPE") @Override public EntityType getEntityType() { return EntityType.WIDGET_TYPE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java index eaa679bc6a..fd9d3e9db7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java @@ -31,7 +31,7 @@ public final class WidgetsBundleId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "WIDGETS_BUNDLE", allowableValues = "WIDGETS_BUNDLE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "WIDGETS_BUNDLE", allowableValues = "WIDGETS_BUNDLE") @Override public EntityType getEntityType() { return EntityType.WIDGETS_BUNDLE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/Notification.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/Notification.java index 0f7095c755..6335cb671d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/Notification.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/Notification.java @@ -36,13 +36,12 @@ public class Notification extends BaseData { private NotificationRequestId requestId; private UserId recipientId; - private NotificationType type; + private NotificationDeliveryMethod deliveryMethod; private String subject; private String text; private JsonNode additionalConfig; private NotificationInfo info; - private NotificationStatus status; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2BasicMapperConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2BasicMapperConfig.java index 3d419caea8..926e063f8d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2BasicMapperConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2BasicMapperConfig.java @@ -38,7 +38,7 @@ public class OAuth2BasicMapperConfig { @Length(fieldName = "lastNameAttributeKey", max = 31) @Schema(description = "Last name attribute key") private final String lastNameAttributeKey; - @Schema(description = "Tenant naming strategy. For DOMAIN type, domain for tenant name will be taken from the email (substring before '@')", required = true) + @Schema(description = "Tenant naming strategy. For DOMAIN type, domain for tenant name will be taken from the email (substring before '@')", requiredMode = Schema.RequiredMode.REQUIRED) private final TenantNameStrategyType tenantNameStrategy; @Length(fieldName = "tenantNamePattern") @Schema(description = "Tenant name pattern for CUSTOM naming strategy. " + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java index 0f9f580264..aa15466d0e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java @@ -36,7 +36,7 @@ import java.util.List; public class OAuth2ClientRegistrationTemplate extends BaseDataWithAdditionalInfo implements HasName { @Length(fieldName = "providerId") - @Schema(description = "OAuth2 provider identifier (e.g. its name)", required = true) + @Schema(description = "OAuth2 provider identifier (e.g. its name)", requiredMode = Schema.RequiredMode.REQUIRED) private String providerId; @Valid @Schema(description = "Default config for mapping OAuth2 log in response to platform entities") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2DomainInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2DomainInfo.java index 9ddc3c8492..dc70213e95 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2DomainInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2DomainInfo.java @@ -31,8 +31,8 @@ import lombok.ToString; @Builder @Schema public class OAuth2DomainInfo { - @Schema(description = "Domain scheme. Mixed scheme means than both HTTP and HTTPS are going to be used", required = true) + @Schema(description = "Domain scheme. Mixed scheme means than both HTTP and HTTPS are going to be used", requiredMode = Schema.RequiredMode.REQUIRED) private SchemeType scheme; - @Schema(description = "Domain name. Cannot be empty", required = true) + @Schema(description = "Domain name. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private String name; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Info.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Info.java index c163647d49..0a2ace050c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Info.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Info.java @@ -35,6 +35,8 @@ import java.util.List; public class OAuth2Info { @Schema(description = "Whether OAuth2 settings are enabled or not") private boolean enabled; - @Schema(description = "List of configured OAuth2 clients. Cannot contain null values", required = true) + @Schema(description = "Whether OAuth2 settings are enabled on Edge or not") + private boolean edgeEnabled; + @Schema(description = "List of configured OAuth2 clients. Cannot contain null values", requiredMode = Schema.RequiredMode.REQUIRED) private List oauth2ParamsInfos; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java index ad39cdad04..be13a032e2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java @@ -32,7 +32,7 @@ public class OAuth2MapperConfig { private boolean allowUserCreation; @Schema(description = "Whether user credentials should be activated when user is created after successful authentication") private boolean activateUser; - @Schema(description = "Type of OAuth2 mapper. Depending on this param, different mapper config fields must be specified", required = true) + @Schema(description = "Type of OAuth2 mapper. Depending on this param, different mapper config fields must be specified", requiredMode = Schema.RequiredMode.REQUIRED) private MapperType type; @Valid @Schema(description = "Mapper config for BASIC and GITHUB mapper types") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java index 8aaffb80aa..053762bdf0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java @@ -31,8 +31,8 @@ import lombok.ToString; @Builder @Schema public class OAuth2MobileInfo { - @Schema(description = "Application package name. Cannot be empty", required = true) + @Schema(description = "Application package name. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private String pkgName; - @Schema(description = "Application secret. The length must be at least 16 characters", required = true) + @Schema(description = "Application secret. The length must be at least 16 characters", requiredMode = Schema.RequiredMode.REQUIRED) private String appSecret; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Params.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Params.java index 3f847c2f9e..70020fc7cd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Params.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Params.java @@ -30,11 +30,13 @@ import org.thingsboard.server.common.data.id.TenantId; public class OAuth2Params extends BaseData { private boolean enabled; + private boolean edgeEnabled; private TenantId tenantId; public OAuth2Params(OAuth2Params oauth2Params) { super(oauth2Params); this.enabled = oauth2Params.enabled; + this.edgeEnabled = oauth2Params.edgeEnabled; this.tenantId = oauth2Params.tenantId; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ParamsInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ParamsInfo.java index 76dca282cf..c1b6124b8f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ParamsInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ParamsInfo.java @@ -36,11 +36,11 @@ public class OAuth2ParamsInfo { @Schema(description = "List of configured domains where OAuth2 platform will redirect a user after successful " + "authentication. Cannot be empty. There have to be only one domain with specific name with scheme type 'MIXED'. " + - "Configured domains with the same name must have different scheme types", required = true) + "Configured domains with the same name must have different scheme types", requiredMode = Schema.RequiredMode.REQUIRED) private List domainInfos; - @Schema(description = "Mobile applications settings. Application package name must be unique within the list", required = true) + @Schema(description = "Mobile applications settings. Application package name must be unique within the list", requiredMode = Schema.RequiredMode.REQUIRED) private List mobileInfos; - @Schema(description = "List of OAuth2 provider settings. Cannot be empty", required = true) + @Schema(description = "List of OAuth2 provider settings. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private List clientRegistrations; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2RegistrationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2RegistrationInfo.java index 1773583ffc..36bae12b23 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2RegistrationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2RegistrationInfo.java @@ -34,17 +34,17 @@ import java.util.List; @Builder @Schema public class OAuth2RegistrationInfo { - @Schema(description = "Config for mapping OAuth2 log in response to platform entities", required = true) + @Schema(description = "Config for mapping OAuth2 log in response to platform entities", requiredMode = Schema.RequiredMode.REQUIRED) private OAuth2MapperConfig mapperConfig; - @Schema(description = "OAuth2 client ID. Cannot be empty", required = true) + @Schema(description = "OAuth2 client ID. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private String clientId; - @Schema(description = "OAuth2 client secret. Cannot be empty", required = true) + @Schema(description = "OAuth2 client secret. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private String clientSecret; - @Schema(description = "Authorization URI of the OAuth2 provider. Cannot be empty", required = true) + @Schema(description = "Authorization URI of the OAuth2 provider. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private String authorizationUri; - @Schema(description = "Access token URI of the OAuth2 provider. Cannot be empty", required = true) + @Schema(description = "Access token URI of the OAuth2 provider. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private String accessTokenUri; - @Schema(description = "OAuth scopes that will be requested from OAuth2 platform. Cannot be empty", required = true) + @Schema(description = "OAuth scopes that will be requested from OAuth2 platform. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private List scope; @Schema(description = "User info URI of the OAuth2 provider") private String userInfoUri; @@ -52,14 +52,14 @@ public class OAuth2RegistrationInfo { private String userNameAttributeName; @Schema(description = "JSON Web Key URI of the OAuth2 provider") private String jwkSetUri; - @Schema(description = "Client authentication method to use: 'BASIC' or 'POST'. Cannot be empty", required = true) + @Schema(description = "Client authentication method to use: 'BASIC' or 'POST'. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private String clientAuthenticationMethod; - @Schema(description = "OAuth2 provider label. Cannot be empty", required = true) + @Schema(description = "OAuth2 provider label. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private String loginButtonLabel; @Schema(description = "Log in button icon for OAuth2 provider") private String loginButtonIcon; @Schema(description = "List of platforms for which usage of the OAuth2 client is allowed (empty for all allowed)") private List platforms; - @Schema(description = "Additional info of OAuth2 client (e.g. providerName)", required = true) + @Schema(description = "Additional info of OAuth2 client (e.g. providerName)", requiredMode = Schema.RequiredMode.REQUIRED) private JsonNode additionalInfo; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java index a6f504577b..309385d5e4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java @@ -31,11 +31,11 @@ import java.util.List; @NoArgsConstructor public class AttributesEntityView implements Serializable { - @Schema(required = true, description = "List of client-side attribute keys to expose", example = "currentConfiguration") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of client-side attribute keys to expose", example = "currentConfiguration") private List cs = new ArrayList<>(); - @Schema(required = true, description = "List of server-side attribute keys to expose", example = "model") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of server-side attribute keys to expose", example = "model") private List ss = new ArrayList<>(); - @Schema(required = true, description = "List of shared attribute keys to expose", example = "targetConfiguration") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of shared attribute keys to expose", example = "targetConfiguration") private List sh = new ArrayList<>(); public AttributesEntityView(List cs, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java index 609d185f9c..a2484b0707 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java @@ -31,9 +31,9 @@ import java.util.List; @NoArgsConstructor public class TelemetryEntityView implements Serializable { - @Schema(required = true, description = "List of time-series data keys to expose", example = "temperature, humidity") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of time-series data keys to expose", example = "temperature, humidity") private List timeseries; - @Schema(required = true, description = "JSON object with attributes to expose") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with attributes to expose") private AttributesEntityView attributes; public TelemetryEntityView(List timeseries, AttributesEntityView attributes) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageLink.java index 14f98ca97d..042a9b1896 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageLink.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageLink.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import org.springframework.data.domain.Sort; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -65,19 +66,19 @@ public class PageLink { return new PageLink(this.pageSize, this.page+1, this.textSearch, this.sortOrder); } - public Sort toSort(SortOrder sortOrder, Map columnMap) { + public Sort toSort(SortOrder sortOrder, Map columnMap, boolean addDefaultSorting) { if (sortOrder == null) { return DEFAULT_SORT; } else { - String property = sortOrder.getProperty(); - if (columnMap.containsKey(property)) { - property = columnMap.get(property); - } - return Sort.by(Sort.Direction.fromString(sortOrder.getDirection().name()), property); + return toSort(List.of(sortOrder), columnMap, addDefaultSorting); } } - public Sort toSort(List sortOrders, Map columnMap) { + public Sort toSort(List sortOrders, Map columnMap, boolean addDefaultSorting) { + if (addDefaultSorting && !isDefaultSortOrderAvailable(sortOrders)) { + sortOrders = new ArrayList<>(sortOrders); + sortOrders.add(new SortOrder(DEFAULT_SORT_PROPERTY, SortOrder.Direction.ASC)); + } return Sort.by(sortOrders.stream().map(s -> toSortOrder(s, columnMap)).collect(Collectors.toList())); } @@ -86,7 +87,16 @@ public class PageLink { if (columnMap.containsKey(property)) { property = columnMap.get(property); } - return new Sort.Order(Sort.Direction.fromString(sortOrder.getDirection().name()), property, Sort.NullHandling.NULLS_LAST); + return new Sort.Order(Sort.Direction.fromString(sortOrder.getDirection().name()), property); + } + + public boolean isDefaultSortOrderAvailable(List sortOrders) { + for (SortOrder sortOrder : sortOrders) { + if (DEFAULT_SORT_PROPERTY.equals(sortOrder.getProperty())) { + return true; + } + } + return false; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageLink.java index c9dacbff40..365d4f7e35 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageLink.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageLink.java @@ -19,11 +19,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; -import org.springframework.data.domain.Sort; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; @Data @ToString(callSuper = true) @@ -67,29 +62,4 @@ public class TimePageLink extends PageLink { this.startTime, this.endTime); } - @Override - public Sort toSort(SortOrder sortOrder, Map columnMap) { - if (sortOrder == null) { - return super.toSort(sortOrder, columnMap); - } else { - return toSort(new ArrayList<>(List.of(sortOrder)), columnMap); - } - } - - @Override - public Sort toSort(List sortOrders, Map columnMap) { - if (!isDefaultSortOrderAvailable(sortOrders)) { - sortOrders.add(new SortOrder(DEFAULT_SORT_PROPERTY, SortOrder.Direction.ASC)); - } - return super.toSort(sortOrders, columnMap); - } - - private boolean isDefaultSortOrderAvailable(List sortOrders) { - for (SortOrder sortOrder : sortOrders) { - if (DEFAULT_SORT_PROPERTY.equals(sortOrder.getProperty())) { - return true; - } - } - return false; - } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueStats.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueStats.java new file mode 100644 index 0000000000..04d50dfe6a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueStats.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.queue; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.QueueStatsId; + +@EqualsAndHashCode(callSuper = true) +@Data +public class QueueStats extends BaseData implements HasTenantId { + private TenantId tenantId; + private String queueName; + private String serviceId; + + public QueueStats() { + } + + public QueueStats(QueueStatsId id) { + super(id); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java index d984fd244d..cda3072101 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java @@ -28,7 +28,7 @@ public class DefaultRuleChainCreateRequest implements Serializable { private static final long serialVersionUID = 5600333716030561537L; - @Schema(required = true, description = "Name of the new rule chain", example = "Root Rule Chain") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Name of the new rule chain", example = "Root Rule Chain") private String name; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java index 82be10c121..fe80293d16 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java @@ -24,10 +24,10 @@ import lombok.Data; @Schema @Data public class NodeConnectionInfo { - @Schema(required = true, description = "Index of rule node in the 'nodes' array of the RuleChainMetaData. Indicates the 'from' part of the connection.") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Index of rule node in the 'nodes' array of the RuleChainMetaData. Indicates the 'from' part of the connection.") private int fromIndex; - @Schema(required = true, description = "Index of rule node in the 'nodes' array of the RuleChainMetaData. Indicates the 'to' part of the connection.") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Index of rule node in the 'nodes' array of the RuleChainMetaData. Indicates the 'to' part of the connection.") private int toIndex; - @Schema(required = true, description = "Type of the relation. Typically indicated the result of processing by the 'from' rule node. For example, 'Success' or 'Failure'") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Type of the relation. Typically indicated the result of processing by the 'from' rule node. For example, 'Success' or 'Failure'") private String type; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java index be901a9176..c293e6f570 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java @@ -39,11 +39,11 @@ public class RuleChain extends BaseDataWithAdditionalInfo implement private static final long serialVersionUID = -5656679015121935465L; - @Schema(required = true, description = "JSON object with Tenant Id.", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with Tenant Id.", accessMode = Schema.AccessMode.READ_ONLY) private TenantId tenantId; @NoXss @Length(fieldName = "name") - @Schema(required = true, description = "Rule Chain name", example = "Humidity data processing") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Rule Chain name", example = "Humidity data processing") private String name; @Schema(description = "Rule Chain type. 'EDGE' rule chains are processing messages on the edge devices only.", example = "A4B72CCDFF33") private RuleChainType type; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java index 654f296c63..029cfb9976 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java @@ -26,12 +26,12 @@ import org.thingsboard.server.common.data.id.RuleChainId; @Schema @Data public class RuleChainConnectionInfo { - @Schema(required = true, description = "Index of rule node in the 'nodes' array of the RuleChainMetaData. Indicates the 'from' part of the connection.") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Index of rule node in the 'nodes' array of the RuleChainMetaData. Indicates the 'from' part of the connection.") private int fromIndex; - @Schema(required = true, description = "JSON object with the Rule Chain Id.") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with the Rule Chain Id.") private RuleChainId targetRuleChainId; - @Schema(required = true, description = "JSON object with the additional information about the connection.") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with the additional information about the connection.") private JsonNode additionalInfo; - @Schema(required = true, description = "Type of the relation. Typically indicated the result of processing by the 'from' rule node. For example, 'Success' or 'Failure'") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Type of the relation. Typically indicated the result of processing by the 'from' rule node. For example, 'Success' or 'Failure'") private String type; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java index e9899d6019..e75d830e84 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java @@ -24,8 +24,8 @@ import java.util.List; @Data public class RuleChainData { - @Schema(required = true, description = "List of the Rule Chain objects.", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of the Rule Chain objects.", accessMode = Schema.AccessMode.READ_ONLY) List ruleChains; - @Schema(required = true, description = "List of the Rule Chain metadata objects.", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of the Rule Chain metadata objects.", accessMode = Schema.AccessMode.READ_ONLY) List metadata; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java index 9a7bc70a7f..eb18e28c8c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java @@ -29,19 +29,19 @@ import java.util.List; @Data public class RuleChainMetaData { - @Schema(required = true, description = "JSON object with Rule Chain Id.", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with Rule Chain Id.", accessMode = Schema.AccessMode.READ_ONLY) private RuleChainId ruleChainId; - @Schema(required = true, description = "Index of the first rule node in the 'nodes' list") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Index of the first rule node in the 'nodes' list") private Integer firstNodeIndex; - @Schema(required = true, description = "List of rule node JSON objects") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of rule node JSON objects") private List nodes; - @Schema(required = true, description = "List of JSON objects that represent connections between rule nodes") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of JSON objects that represent connections between rule nodes") private List connections; - @Schema(required = true, description = "List of JSON objects that represent connections between rule nodes and other rule chains.") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of JSON objects that represent connections between rule nodes and other rule chains.") private List ruleChainConnections; public void addConnectionInfo(int fromIndex, int toIndex, String type) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainOutputLabelsUsage.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainOutputLabelsUsage.java index d0c7186501..b9fb1fc6d8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainOutputLabelsUsage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainOutputLabelsUsage.java @@ -28,16 +28,16 @@ import java.util.Set; @Slf4j public class RuleChainOutputLabelsUsage { - @Schema(required = true, description = "Rule Chain Id", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Rule Chain Id", accessMode = Schema.AccessMode.READ_ONLY) private RuleChainId ruleChainId; - @Schema(required = true, description = "Rule Node Id", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Rule Node Id", accessMode = Schema.AccessMode.READ_ONLY) private RuleNodeId ruleNodeId; - @Schema(required = true, description = "Rule Chain Name", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Rule Chain Name", accessMode = Schema.AccessMode.READ_ONLY) private String ruleChainName; - @Schema(required = true, description = "Rule Node Name", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Rule Node Name", accessMode = Schema.AccessMode.READ_ONLY) private String ruleNodeName; - @Schema(required = true, description = "Output labels", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Output labels", accessMode = Schema.AccessMode.READ_ONLY) private Set labels; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java index f59b86af63..24d8cc1175 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java @@ -47,7 +47,7 @@ public class DeviceCredentials extends BaseData implements this.credentialsValue = deviceCredentials.getCredentialsValue(); } - @Schema(required = true, accessMode = Schema.AccessMode.READ_ONLY, description = "The Id is automatically generated during device creation. " + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "The Id is automatically generated during device creation. " + "Use 'getDeviceCredentialsByDeviceId' to obtain the id based on device id. " + "Use 'updateDeviceCredentials' to update device credentials. ", example = "784f394c-42b6-435a-983c-b7beff2784f9") @Override @@ -61,7 +61,7 @@ public class DeviceCredentials extends BaseData implements return super.getCreatedTime(); } - @Schema(required = true, description = "JSON object with the device Id.") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with the device Id.") public DeviceId getDeviceId() { return deviceId; } @@ -80,7 +80,7 @@ public class DeviceCredentials extends BaseData implements this.credentialsType = credentialsType; } - @Schema(required = true, description = "Unique Credentials Id per platform instance. " + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Unique Credentials Id per platform instance. " + "Used to lookup credentials from the database. " + "By default, new access token for your device. " + "Depends on the type of the credentials." diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java index 8ed16a1c1c..5fc0c14d61 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java @@ -20,27 +20,27 @@ import lombok.Data; @Data public class SmppSmsProviderConfiguration implements SmsProviderConfiguration { - @Schema(description = "SMPP version", allowableValues = "3.3, 3.4", required = true) + @Schema(description = "SMPP version", allowableValues = "3.3, 3.4", requiredMode = Schema.RequiredMode.REQUIRED) private String protocolVersion; - @Schema(description = "SMPP host", required = true) + @Schema(description = "SMPP host", requiredMode = Schema.RequiredMode.REQUIRED) private String host; - @Schema(description = "SMPP port", required = true) + @Schema(description = "SMPP port", requiredMode = Schema.RequiredMode.REQUIRED) private Integer port; - @Schema(description = "System ID", required = true) + @Schema(description = "System ID", requiredMode = Schema.RequiredMode.REQUIRED) private String systemId; - @Schema(description = "Password", required = true) + @Schema(description = "Password", requiredMode = Schema.RequiredMode.REQUIRED) private String password; - @Schema(description = "System type", required = false) + @Schema(description = "System type", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String systemType; - @Schema(description = "TX - Transmitter, RX - Receiver, TRX - Transciever. By default TX is used", required = false) + @Schema(description = "TX - Transmitter, RX - Receiver, TRX - Transciever. By default TX is used", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private SmppBindType bindType; - @Schema(description = "Service type", required = false) + @Schema(description = "Service type", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String serviceType; - @Schema(description = "Source address", required = false) + @Schema(description = "Source address", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String sourceAddress; @Schema(description = "Source TON (Type of Number). Needed is source address is set. 5 by default.\n" + "0 - Unknown\n" + @@ -49,7 +49,7 @@ public class SmppSmsProviderConfiguration implements SmsProviderConfiguration { "3 - Network Specific\n" + "4 - Subscriber Number\n" + "5 - Alphanumeric\n" + - "6 - Abbreviated", required = false) + "6 - Abbreviated", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private Byte sourceTon; @Schema(description = "Source NPI (Numbering Plan Identification). Needed is source address is set. 0 by default.\n" + "0 - Unknown\n" + @@ -61,7 +61,7 @@ public class SmppSmsProviderConfiguration implements SmsProviderConfiguration { "9 - Private numbering plan\n" + "10 - ERMES numbering plan (ETSI DE/PS 3 01-3)\n" + "13 - Internet (IP)\n" + - "18 - WAP Client Id (to be defined by WAP Forum)", required = false) + "18 - WAP Client Id (to be defined by WAP Forum)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private Byte sourceNpi; @Schema(description = "Destination TON (Type of Number). 5 by default.\n" + @@ -71,7 +71,7 @@ public class SmppSmsProviderConfiguration implements SmsProviderConfiguration { "3 - Network Specific\n" + "4 - Subscriber Number\n" + "5 - Alphanumeric\n" + - "6 - Abbreviated", required = false) + "6 - Abbreviated", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private Byte destinationTon; @Schema(description = "Destination NPI (Numbering Plan Identification). 0 by default.\n" + "0 - Unknown\n" + @@ -83,10 +83,10 @@ public class SmppSmsProviderConfiguration implements SmsProviderConfiguration { "9 - Private numbering plan\n" + "10 - ERMES numbering plan (ETSI DE/PS 3 01-3)\n" + "13 - Internet (IP)\n" + - "18 - WAP Client Id (to be defined by WAP Forum)", required = false) + "18 - WAP Client Id (to be defined by WAP Forum)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private Byte destinationNpi; - @Schema(description = "Address range", required = false) + @Schema(description = "Address range", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String addressRange; @Schema(allowableValues = "0-10,13-14", @@ -102,7 +102,7 @@ public class SmppSmsProviderConfiguration implements SmsProviderConfiguration { "9 - Pictogram Encoding\n" + "10 - Music Codes (ISO-2022-JP)\n" + "13 - Extended Kanji JIS (X 0212-1990)\n" + - "14 - Korean Graphic Character Set (KS C 5601/KS X 1001)", required = false) + "14 - Korean Graphic Character Set (KS C 5601/KS X 1001)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private Byte codingScheme; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadRequest.java index bcc6fc9c5b..7eca2c04de 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadRequest.java @@ -26,6 +26,7 @@ import java.util.Map; public class EntityTypeVersionLoadRequest extends VersionLoadRequest { private Map entityTypes; + private boolean rollbackOnError; @Override public VersionLoadRequestType getType() { diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index 3c4a1e6be9..23bfe55f1b 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -111,7 +111,7 @@ public class EdgeGrpcClient implements EdgeRpcClient { .setConnectRequestMsg(ConnectRequestMsg.newBuilder() .setEdgeRoutingKey(edgeKey) .setEdgeSecret(edgeSecret) - .setEdgeVersion(EdgeVersion.V_3_6_2) + .setEdgeVersion(EdgeVersion.V_3_7_0) .setMaxInboundMessageSize(maxInboundMessageSize) .build()) .build()); @@ -248,4 +248,5 @@ public class EdgeGrpcClient implements EdgeRpcClient { uplinkMsgLock.unlock(); } } + } diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 34a8c6f093..7c60985683 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -37,6 +37,8 @@ enum EdgeVersion { V_3_6_0 = 3; V_3_6_1 = 4; V_3_6_2 = 5; + V_3_6_4 = 6; + V_3_7_0 = 7; } /** @@ -467,6 +469,31 @@ message ResourceUpdateMsg { string entity = 11; } +message OAuth2UpdateMsg { + string entity = 1; +} + +message NotificationRuleUpdateMsg { + UpdateMsgType msgType = 1; + optional int64 idMSB = 2; + optional int64 idLSB = 3; + optional string entity = 4; +} + +message NotificationTargetUpdateMsg { + UpdateMsgType msgType = 1; + optional int64 idMSB = 2; + optional int64 idLSB = 3; + optional string entity = 4; +} + +message NotificationTemplateUpdateMsg { + UpdateMsgType msgType = 1; + optional int64 idMSB = 2; + optional int64 idLSB = 3; + optional string entity = 4; +} + message RuleChainMetadataRequestMsg { int64 ruleChainIdMSB = 1; int64 ruleChainIdLSB = 2; @@ -668,5 +695,9 @@ message DownlinkMsg { repeated TenantProfileUpdateMsg tenantProfileUpdateMsg = 27; repeated ResourceUpdateMsg resourceUpdateMsg = 28; repeated AlarmCommentUpdateMsg alarmCommentUpdateMsg = 29; + repeated OAuth2UpdateMsg oAuth2UpdateMsg = 30; + repeated NotificationRuleUpdateMsg notificationRuleUpdateMsg = 31; + repeated NotificationTargetUpdateMsg notificationTargetUpdateMsg = 32; + repeated NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = 33; } diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/KvProtoUtil.java b/common/proto/src/main/java/org/thingsboard/server/common/util/KvProtoUtil.java index 504ffe368d..74674e1e45 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/KvProtoUtil.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/KvProtoUtil.java @@ -29,11 +29,26 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.gen.transport.TransportProtos; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; public class KvProtoUtil { + private static final DataType[] dataTypeByProtoNumber; + + static { + int arraySize = Arrays.stream(DataType.values()).mapToInt(DataType::getProtoNumber).max().orElse(0); + dataTypeByProtoNumber = new DataType[arraySize + 1]; + Arrays.stream(DataType.values()).forEach(dataType -> dataTypeByProtoNumber[dataType.getProtoNumber()] = dataType); + } + + public static List toAttributeKvList(List dataList) { + List result = new ArrayList<>(dataList.size()); + dataList.forEach(proto -> result.add(new BaseAttributeKvEntry(fromTsKvProto(proto.getKv()), proto.getTs()))); + return result; + } + public static List attrToTsKvProtos(List result) { List clientAttributes; if (result == null || result.isEmpty()) { @@ -47,8 +62,7 @@ public class KvProtoUtil { return clientAttributes; } - - public static List tsToTsKvProtos(List result) { + public static List toTsKvProtoList(List result) { List ts; if (result == null || result.isEmpty()) { ts = Collections.emptyList(); @@ -61,95 +75,85 @@ public class KvProtoUtil { return ts; } + public static List fromTsKvProtoList(List dataList) { + List result = new ArrayList<>(dataList.size()); + dataList.forEach(proto -> result.add(new BasicTsKvEntry(proto.getTs(), fromTsKvProto(proto.getKv())))); + return result; + } + public static TransportProtos.TsKvProto toTsKvProto(long ts, KvEntry kvEntry) { return TransportProtos.TsKvProto.newBuilder().setTs(ts) - .setKv(KvProtoUtil.toKeyValueProto(kvEntry)).build(); + .setKv(KvProtoUtil.toKeyValueTypeProto(kvEntry)).build(); + } + + public static TsKvEntry fromTsKvProto(TransportProtos.TsKvProto proto) { + return new BasicTsKvEntry(proto.getTs(), fromTsKvProto(proto.getKv())); } - public static TransportProtos.KeyValueProto toKeyValueProto(KvEntry kvEntry) { + public static TransportProtos.KeyValueProto toKeyValueTypeProto(KvEntry kvEntry) { TransportProtos.KeyValueProto.Builder builder = TransportProtos.KeyValueProto.newBuilder(); builder.setKey(kvEntry.getKey()); + builder.setType(toKeyValueTypeProto(kvEntry.getDataType())); switch (kvEntry.getDataType()) { - case BOOLEAN: - builder.setType(TransportProtos.KeyValueType.BOOLEAN_V); - builder.setBoolV(kvEntry.getBooleanValue().get()); - break; - case DOUBLE: - builder.setType(TransportProtos.KeyValueType.DOUBLE_V); - builder.setDoubleV(kvEntry.getDoubleValue().get()); - break; - case LONG: - builder.setType(TransportProtos.KeyValueType.LONG_V); - builder.setLongV(kvEntry.getLongValue().get()); - break; - case STRING: - builder.setType(TransportProtos.KeyValueType.STRING_V); - builder.setStringV(kvEntry.getStrValue().get()); - break; - case JSON: - builder.setType(TransportProtos.KeyValueType.JSON_V); - builder.setJsonV(kvEntry.getJsonValue().get()); - break; + case BOOLEAN -> kvEntry.getBooleanValue().ifPresent(builder::setBoolV); + case LONG -> kvEntry.getLongValue().ifPresent(builder::setLongV); + case DOUBLE -> kvEntry.getDoubleValue().ifPresent(builder::setDoubleV); + case JSON -> kvEntry.getJsonValue().ifPresent(builder::setJsonV); + case STRING -> kvEntry.getStrValue().ifPresent(builder::setStringV); } return builder.build(); } - public static TransportProtos.TsKvProto.Builder toKeyValueProto(long ts, KvEntry attr) { - TransportProtos.KeyValueProto.Builder dataBuilder = TransportProtos.KeyValueProto.newBuilder(); - dataBuilder.setKey(attr.getKey()); - dataBuilder.setType(TransportProtos.KeyValueType.forNumber(attr.getDataType().ordinal())); - switch (attr.getDataType()) { - case BOOLEAN: - attr.getBooleanValue().ifPresent(dataBuilder::setBoolV); - break; - case LONG: - attr.getLongValue().ifPresent(dataBuilder::setLongV); - break; - case DOUBLE: - attr.getDoubleValue().ifPresent(dataBuilder::setDoubleV); - break; - case JSON: - attr.getJsonValue().ifPresent(dataBuilder::setJsonV); - break; - case STRING: - attr.getStrValue().ifPresent(dataBuilder::setStringV); - break; - } - return TransportProtos.TsKvProto.newBuilder().setTs(ts).setKv(dataBuilder); + public static KvEntry fromTsKvProto(TransportProtos.KeyValueProto proto) { + return switch (fromKeyValueTypeProto(proto.getType())) { + case BOOLEAN -> new BooleanDataEntry(proto.getKey(), proto.getBoolV()); + case LONG -> new LongDataEntry(proto.getKey(), proto.getLongV()); + case DOUBLE -> new DoubleDataEntry(proto.getKey(), proto.getDoubleV()); + case STRING -> new StringDataEntry(proto.getKey(), proto.getStringV()); + case JSON -> new JsonDataEntry(proto.getKey(), proto.getJsonV()); + }; } - public static List toTsKvEntityList(List dataList) { - List result = new ArrayList<>(dataList.size()); - dataList.forEach(proto -> result.add(new BasicTsKvEntry(proto.getTs(), getKvEntry(proto.getKv())))); - return result; + public static TransportProtos.TsKvProto.Builder toTsKvProtoBuilder(long ts, KvEntry kvEntry) { + return TransportProtos.TsKvProto.newBuilder().setTs(ts).setKv(KvProtoUtil.toKeyValueTypeProto(kvEntry)); } - public static List toAttributeKvList(List dataList) { - List result = new ArrayList<>(dataList.size()); - dataList.forEach(proto -> result.add(new BaseAttributeKvEntry(getKvEntry(proto.getKv()), proto.getTs()))); + public static List fromTsValueProtoList(String key, List dataList) { + List result = new ArrayList<>(dataList.size()); + dataList.forEach(proto -> result.add(new BasicTsKvEntry(proto.getTs(), fromTsValueProto(key, proto)))); return result; } - private static KvEntry getKvEntry(TransportProtos.KeyValueProto proto) { - KvEntry entry = null; - DataType type = DataType.values()[proto.getType().getNumber()]; - switch (type) { - case BOOLEAN: - entry = new BooleanDataEntry(proto.getKey(), proto.getBoolV()); - break; - case LONG: - entry = new LongDataEntry(proto.getKey(), proto.getLongV()); - break; - case DOUBLE: - entry = new DoubleDataEntry(proto.getKey(), proto.getDoubleV()); - break; - case STRING: - entry = new StringDataEntry(proto.getKey(), proto.getStringV()); - break; - case JSON: - entry = new JsonDataEntry(proto.getKey(), proto.getJsonV()); - break; + public static TransportProtos.TsValueProto toTsValueProto(long ts, KvEntry attr) { + TransportProtos.TsValueProto.Builder dataBuilder = TransportProtos.TsValueProto.newBuilder(); + dataBuilder.setTs(ts); + dataBuilder.setType(toKeyValueTypeProto(attr.getDataType())); + switch (attr.getDataType()) { + case BOOLEAN -> attr.getBooleanValue().ifPresent(dataBuilder::setBoolV); + case LONG -> attr.getLongValue().ifPresent(dataBuilder::setLongV); + case DOUBLE -> attr.getDoubleValue().ifPresent(dataBuilder::setDoubleV); + case JSON -> attr.getJsonValue().ifPresent(dataBuilder::setJsonV); + case STRING -> attr.getStrValue().ifPresent(dataBuilder::setStringV); } - return entry; + return dataBuilder.build(); } + + public static KvEntry fromTsValueProto(String key, TransportProtos.TsValueProto proto) { + return switch (fromKeyValueTypeProto(proto.getType())) { + case BOOLEAN -> new BooleanDataEntry(key, proto.getBoolV()); + case LONG -> new LongDataEntry(key, proto.getLongV()); + case DOUBLE -> new DoubleDataEntry(key, proto.getDoubleV()); + case STRING -> new StringDataEntry(key, proto.getStringV()); + case JSON -> new JsonDataEntry(key, proto.getJsonV()); + }; + } + + public static TransportProtos.KeyValueType toKeyValueTypeProto(DataType dataType) { + return TransportProtos.KeyValueType.forNumber(dataType.getProtoNumber()); + } + + public static DataType fromKeyValueTypeProto(TransportProtos.KeyValueType keyValueType) { + return dataTypeByProtoNumber[keyValueType.getNumber()]; + } + } 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 ca6e826cac..e89ce5634c 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 @@ -258,31 +258,31 @@ public class ProtoUtils { .setLastUpdateTs(attributeKvEntry.getLastUpdateTs()) .setKey(attributeKvEntry.getKey()); switch (attributeKvEntry.getDataType()) { - case BOOLEAN: + case BOOLEAN -> { attributeKvEntry.getBooleanValue().ifPresent(attributeValueBuilder::setBoolV); attributeValueBuilder.setHasV(attributeKvEntry.getBooleanValue().isPresent()); attributeValueBuilder.setType(TransportProtos.KeyValueType.BOOLEAN_V); - break; - case STRING: + } + case STRING -> { attributeKvEntry.getStrValue().ifPresent(attributeValueBuilder::setStringV); attributeValueBuilder.setHasV(attributeKvEntry.getStrValue().isPresent()); attributeValueBuilder.setType(TransportProtos.KeyValueType.STRING_V); - break; - case DOUBLE: + } + case DOUBLE -> { attributeKvEntry.getDoubleValue().ifPresent(attributeValueBuilder::setDoubleV); attributeValueBuilder.setHasV(attributeKvEntry.getDoubleValue().isPresent()); attributeValueBuilder.setType(TransportProtos.KeyValueType.DOUBLE_V); - break; - case LONG: + } + case LONG -> { attributeKvEntry.getLongValue().ifPresent(attributeValueBuilder::setLongV); attributeValueBuilder.setHasV(attributeKvEntry.getLongValue().isPresent()); attributeValueBuilder.setType(TransportProtos.KeyValueType.LONG_V); - break; - case JSON: + } + case JSON -> { attributeKvEntry.getJsonValue().ifPresent(attributeValueBuilder::setJsonV); attributeValueBuilder.setHasV(attributeKvEntry.getJsonValue().isPresent()); attributeValueBuilder.setType(TransportProtos.KeyValueType.JSON_V); - break; + } } builder.addValues(attributeValueBuilder.build()); } @@ -434,36 +434,28 @@ public class ProtoUtils { } public static TransportProtos.ToDeviceActorNotificationMsgProto toProto(ToDeviceActorNotificationMsg msg) { - if (msg instanceof DeviceEdgeUpdateMsg) { - DeviceEdgeUpdateMsg updateMsg = (DeviceEdgeUpdateMsg) msg; + if (msg instanceof DeviceEdgeUpdateMsg updateMsg) { TransportProtos.DeviceEdgeUpdateMsgProto proto = toProto(updateMsg); return TransportProtos.ToDeviceActorNotificationMsgProto.newBuilder().setDeviceEdgeUpdateMsg(proto).build(); - } else if (msg instanceof DeviceNameOrTypeUpdateMsg) { - DeviceNameOrTypeUpdateMsg updateMsg = (DeviceNameOrTypeUpdateMsg) msg; + } else if (msg instanceof DeviceNameOrTypeUpdateMsg updateMsg) { TransportProtos.DeviceNameOrTypeUpdateMsgProto proto = toProto(updateMsg); return TransportProtos.ToDeviceActorNotificationMsgProto.newBuilder().setDeviceNameOrTypeMsg(proto).build(); - } else if (msg instanceof DeviceAttributesEventNotificationMsg) { - DeviceAttributesEventNotificationMsg updateMsg = (DeviceAttributesEventNotificationMsg) msg; + } else if (msg instanceof DeviceAttributesEventNotificationMsg updateMsg) { TransportProtos.DeviceAttributesEventMsgProto proto = toProto(updateMsg); return TransportProtos.ToDeviceActorNotificationMsgProto.newBuilder().setDeviceAttributesEventMsg(proto).build(); - } else if (msg instanceof DeviceCredentialsUpdateNotificationMsg) { - DeviceCredentialsUpdateNotificationMsg updateMsg = (DeviceCredentialsUpdateNotificationMsg) msg; + } else if (msg instanceof DeviceCredentialsUpdateNotificationMsg updateMsg) { TransportProtos.DeviceCredentialsUpdateMsgProto proto = toProto(updateMsg); return TransportProtos.ToDeviceActorNotificationMsgProto.newBuilder().setDeviceCredentialsUpdateMsg(proto).build(); - } else if (msg instanceof ToDeviceRpcRequestActorMsg) { - ToDeviceRpcRequestActorMsg updateMsg = (ToDeviceRpcRequestActorMsg) msg; + } else if (msg instanceof ToDeviceRpcRequestActorMsg updateMsg) { TransportProtos.ToDeviceRpcRequestActorMsgProto proto = toProto(updateMsg); return TransportProtos.ToDeviceActorNotificationMsgProto.newBuilder().setToDeviceRpcRequestMsg(proto).build(); - } else if (msg instanceof FromDeviceRpcResponseActorMsg) { - FromDeviceRpcResponseActorMsg updateMsg = (FromDeviceRpcResponseActorMsg) msg; + } else if (msg instanceof FromDeviceRpcResponseActorMsg updateMsg) { TransportProtos.FromDeviceRpcResponseActorMsgProto proto = toProto(updateMsg); return TransportProtos.ToDeviceActorNotificationMsgProto.newBuilder().setFromDeviceRpcResponseMsg(proto).build(); - } else if (msg instanceof RemoveRpcActorMsg) { - RemoveRpcActorMsg updateMsg = (RemoveRpcActorMsg) msg; + } else if (msg instanceof RemoveRpcActorMsg updateMsg) { TransportProtos.RemoveRpcActorMsgProto proto = toProto(updateMsg); return TransportProtos.ToDeviceActorNotificationMsgProto.newBuilder().setRemoveRpcActorMsg(proto).build(); - } else if (msg instanceof DeviceDeleteMsg) { - DeviceDeleteMsg updateMsg = (DeviceDeleteMsg) msg; + } else if (msg instanceof DeviceDeleteMsg updateMsg) { TransportProtos.DeviceDeleteMsgProto proto = toProto(updateMsg); return TransportProtos.ToDeviceActorNotificationMsgProto.newBuilder().setDeviceDeleteMsg(proto).build(); } @@ -507,24 +499,14 @@ public class ProtoUtils { List result = new ArrayList<>(); for (TransportProtos.AttributeValueProto kvEntry : valuesList) { boolean hasValue = kvEntry.getHasV(); - KvEntry entry = null; - switch (kvEntry.getType()) { - case BOOLEAN_V: - entry = new BooleanDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getBoolV() : null); - break; - case LONG_V: - entry = new LongDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getLongV() : null); - break; - case DOUBLE_V: - entry = new DoubleDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getDoubleV() : null); - break; - case STRING_V: - entry = new StringDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getStringV() : null); - break; - case JSON_V: - entry = new JsonDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getJsonV() : null); - break; - } + KvEntry entry = switch (kvEntry.getType()) { + case BOOLEAN_V -> new BooleanDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getBoolV() : null); + case LONG_V -> new LongDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getLongV() : null); + case DOUBLE_V -> new DoubleDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getDoubleV() : null); + case STRING_V -> new StringDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getStringV() : null); + case JSON_V -> new JsonDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getJsonV() : null); + default -> null; + }; result.add(new BaseAttributeKvEntry(kvEntry.getLastUpdateTs(), entry)); } return result; @@ -1029,15 +1011,11 @@ public class ProtoUtils { .setDeviceProfileIdLSB(device.getDeviceProfileId().getId().getLeastSignificantBits()) .setAdditionalInfo(JacksonUtil.toString(device.getAdditionalInfo())); - PowerSavingConfiguration psmConfiguration = null; - switch (device.getDeviceData().getTransportConfiguration().getType()) { - case LWM2M: - psmConfiguration = (Lwm2mDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration(); - break; - case COAP: - psmConfiguration = (CoapDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration(); - break; - } + PowerSavingConfiguration psmConfiguration = switch (device.getDeviceData().getTransportConfiguration().getType()) { + case LWM2M -> (Lwm2mDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration(); + case COAP -> (CoapDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration(); + default -> null; + }; if (psmConfiguration != null) { PowerMode powerMode = psmConfiguration.getPowerMode(); @@ -1079,4 +1057,5 @@ public class ProtoUtils { private static Long checkLong(Long l) { return isNotNull(l) ? l : 0; } + } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 888e31209c..44f0b0caf0 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -54,6 +54,7 @@ enum EntityTypeProto { NOTIFICATION_REQUEST = 31; NOTIFICATION = 32; NOTIFICATION_RULE = 33; + QUEUE_STATS = 34; } /** diff --git a/common/proto/src/test/java/org/thingsboard/server/common/util/KvProtoUtilTest.java b/common/proto/src/test/java/org/thingsboard/server/common/util/KvProtoUtilTest.java new file mode 100644 index 0000000000..72d71c7c93 --- /dev/null +++ b/common/proto/src/test/java/org/thingsboard/server/common/util/KvProtoUtilTest.java @@ -0,0 +1,118 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.util; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.thingsboard.server.common.data.kv.AggTsKvEntry; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DataType; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class KvProtoUtilTest { + + private static final long TS = System.currentTimeMillis(); + + private static Stream kvEntryData() { + String key = "key"; + return Stream.of( + new BooleanDataEntry(key, true), + new LongDataEntry(key, 23L), + new DoubleDataEntry(key, 23.0), + new StringDataEntry(key, "stringValue"), + new JsonDataEntry(key, "jsonValue") + ); + } + + private static Stream basicTsKvEntryData() { + return kvEntryData().map(kvEntry -> new BasicTsKvEntry(TS, kvEntry)); + } + + private static Stream> attributeKvEntryData() { + return Stream.of(kvEntryData().map(kvEntry -> new BaseAttributeKvEntry(TS, kvEntry)).toList()); + } + + private static List createTsKvEntryList(boolean withAggregation) { + return kvEntryData().map(kvEntry -> { + if (withAggregation) { + return new AggTsKvEntry(TS, kvEntry, 0); + } else { + return new BasicTsKvEntry(TS, kvEntry); + } + }).collect(Collectors.toList()); + } + + @ParameterizedTest + @EnumSource(DataType.class) + void protoDataTypeSerialization(DataType dataType) { + assertThat(KvProtoUtil.fromKeyValueTypeProto(KvProtoUtil.toKeyValueTypeProto(dataType))) + .as(dataType.name()).isEqualTo(dataType); + } + + @ParameterizedTest + @MethodSource("kvEntryData") + void protoKeyValueProtoSerialization(KvEntry kvEntry) { + assertThat(KvProtoUtil.fromTsKvProto(KvProtoUtil.toKeyValueTypeProto(kvEntry))) + .as("deserialized").isEqualTo(kvEntry); + } + + @ParameterizedTest + @MethodSource("basicTsKvEntryData") + void protoTsKvEntrySerialization(KvEntry kvEntry) { + assertThat(KvProtoUtil.fromTsKvProto(KvProtoUtil.toTsKvProto(TS, kvEntry))) + .as("deserialized").isEqualTo(kvEntry); + } + + @ParameterizedTest + @MethodSource("kvEntryData") + void protoTsValueSerialization(KvEntry kvEntry) { + assertThat(KvProtoUtil.fromTsValueProto(kvEntry.getKey(), KvProtoUtil.toTsValueProto(TS, kvEntry))) + .as("deserialized").isEqualTo(kvEntry); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void protoListTsKvEntrySerialization(boolean withAggregation) { + List tsKvEntries = createTsKvEntryList(withAggregation); + assertThat(KvProtoUtil.fromTsKvProtoList(KvProtoUtil.toTsKvProtoList(tsKvEntries))) + .as("deserialized").isEqualTo(tsKvEntries); + } + + @ParameterizedTest + @MethodSource("attributeKvEntryData") + void protoListAttributeKvSerialization(List attributeKvEntries) { + assertThat(KvProtoUtil.toAttributeKvList(KvProtoUtil.attrToTsKvProtos(attributeKvEntries))) + .as("deserialized") + .isEqualTo(attributeKvEntries); + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java index c7c59974cd..61c2614fc7 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java @@ -109,40 +109,40 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng @Override public TbQueueProducer> createTransportNotificationsMsgProducer() { - return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, transportNotificationSettings.getNotificationsTopic()); + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, topicService.buildTopicName(transportNotificationSettings.getNotificationsTopic())); } @Override public TbQueueProducer> createRuleEngineMsgProducer() { - return new TbPubSubProducerTemplate<>(ruleEngineAdmin, pubSubSettings, ruleEngineSettings.getTopic()); + return new TbPubSubProducerTemplate<>(ruleEngineAdmin, pubSubSettings, topicService.buildTopicName(ruleEngineSettings.getTopic())); } @Override public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { - return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, ruleEngineSettings.getTopic()); + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, topicService.buildTopicName(ruleEngineSettings.getTopic())); } @Override public TbQueueProducer> createTbCoreMsgProducer() { - return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getTopic()); + return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getTopic())); } @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, coreSettings.getTopic()); + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getTopic())); } @Override public TbQueueConsumer> createToVersionControlMsgConsumer() { - return new TbPubSubConsumerTemplate<>(vcAdmin, pubSubSettings, vcSettings.getTopic(), + return new TbPubSubConsumerTemplate<>(vcAdmin, pubSubSettings, topicService.buildTopicName(vcSettings.getTopic()), msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders()) ); } @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(Queue configuration) { - return new TbPubSubConsumerTemplate<>(ruleEngineAdmin, pubSubSettings, configuration.getTopic(), + return new TbPubSubConsumerTemplate<>(ruleEngineAdmin, pubSubSettings, topicService.buildTopicName(configuration.getTopic()), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); } @@ -155,7 +155,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng @Override public TbQueueConsumer> createToCoreMsgConsumer() { - return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getTopic(), + return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getTopic()), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreMsg.parseFrom(msg.getData()), msg.getHeaders())); } @@ -168,13 +168,13 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng @Override public TbQueueConsumer> createTransportApiRequestConsumer() { - return new TbPubSubConsumerTemplate<>(transportApiAdmin, pubSubSettings, transportApiSettings.getRequestsTopic(), + return new TbPubSubConsumerTemplate<>(transportApiAdmin, pubSubSettings, topicService.buildTopicName(transportApiSettings.getRequestsTopic()), msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportApiRequestMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override public TbQueueProducer> createTransportApiResponseProducer() { - return new TbPubSubProducerTemplate<>(transportApiAdmin, pubSubSettings, transportApiSettings.getResponsesTopic()); + return new TbPubSubProducerTemplate<>(transportApiAdmin, pubSubSettings, topicService.buildTopicName(transportApiSettings.getResponsesTopic())); } @Override @@ -202,29 +202,29 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng @Override public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { - return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getUsageStatsTopic(), + return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic()), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override public TbQueueConsumer> createToOtaPackageStateServiceMsgConsumer() { - return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getOtaPackageTopic(), + return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getOtaPackageTopic()), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToOtaPackageStateServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { - return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getOtaPackageTopic()); + return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getOtaPackageTopic())); } @Override public TbQueueProducer> createToUsageStatsServiceMsgProducer() { - return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getUsageStatsTopic()); + return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } @Override public TbQueueProducer> createVersionControlMsgProducer() { - return new TbPubSubProducerTemplate<>(vcAdmin, pubSubSettings, vcSettings.getTopic()); + return new TbPubSubProducerTemplate<>(vcAdmin, pubSubSettings, topicService.buildTopicName(vcSettings.getTopic())); } @PreDestroy diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java index a6b9e44022..63975e3116 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java @@ -100,32 +100,32 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTransportNotificationsMsgProducer() { - return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, transportNotificationSettings.getNotificationsTopic()); + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, topicService.buildTopicName(transportNotificationSettings.getNotificationsTopic())); } @Override public TbQueueProducer> createRuleEngineMsgProducer() { - return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getTopic()); + return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getTopic())); } @Override public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { - return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, ruleEngineSettings.getTopic()); + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, topicService.buildTopicName(ruleEngineSettings.getTopic())); } @Override public TbQueueProducer> createTbCoreMsgProducer() { - return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getTopic()); + return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getTopic())); } @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, coreSettings.getTopic()); + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getTopic())); } @Override public TbQueueConsumer> createToCoreMsgConsumer() { - return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getTopic(), + return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getTopic()), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreMsg.parseFrom(msg.getData()), msg.getHeaders())); } @@ -138,13 +138,13 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueConsumer> createTransportApiRequestConsumer() { - return new TbPubSubConsumerTemplate<>(transportApiAdmin, pubSubSettings, transportApiSettings.getRequestsTopic(), + return new TbPubSubConsumerTemplate<>(transportApiAdmin, pubSubSettings, topicService.buildTopicName(transportApiSettings.getRequestsTopic()), msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportApiRequestMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override public TbQueueProducer> createTransportApiResponseProducer() { - return new TbPubSubProducerTemplate<>(transportApiAdmin, pubSubSettings, transportApiSettings.getResponsesTopic()); + return new TbPubSubProducerTemplate<>(transportApiAdmin, pubSubSettings, topicService.buildTopicName(transportApiSettings.getResponsesTopic())); } @Override @@ -172,24 +172,24 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueConsumer> createToUsageStatsServiceMsgConsumer() { - return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getUsageStatsTopic(), + return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic()), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override public TbQueueConsumer> createToOtaPackageStateServiceMsgConsumer() { - return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getOtaPackageTopic(), + return new TbPubSubConsumerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getOtaPackageTopic()), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToOtaPackageStateServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { - return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getOtaPackageTopic()); + return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getOtaPackageTopic())); } @Override public TbQueueProducer> createToUsageStatsServiceMsgProducer() { - return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getUsageStatsTopic()); + return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } @Override diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java index f88625f7fd..32e209b2ea 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java @@ -145,14 +145,14 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService log.trace("[{}] InvokeScript uuid {} with timeout {}ms", tenantId, scriptId, getMaxInvokeRequestsTimeout()); var task = doInvokeFunction(scriptId, args); - var resultFuture = Futures.transformAsync(task.getResultFuture(), output -> { + var resultFuture = Futures.transform(task.getResultFuture(), output -> { String result = JacksonUtil.toString(output); if (resultSizeExceeded(result)) { throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, null, new RuntimeException( format("Script invocation result exceeds maximum allowed size of %s symbols", getMaxResultSize()) )); } - return Futures.immediateFuture(output); + return output; }, MoreExecutors.directExecutor()); return withTimeoutAndStatsCallback(scriptId, task, resultFuture, invokeCallback, getMaxInvokeRequestsTimeout()); diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java index 0e37bd89d6..dc83b08af4 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java @@ -41,7 +41,6 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.locks.ReentrantLock; @Slf4j @@ -62,9 +61,12 @@ public class NashornJsInvokeService extends AbstractJsInvokeService { @Value("${js.local.monitor_thread_pool_size}") private int monitorThreadPoolSize; - @Value("${js.local.max_cpu_time}") + @Value("${js.local.max_cpu_time:8000}") // 8 seconds private long maxCpuTime; + @Value("${js.local.max_memory:104857600}") // 100 MiB + private long maxMemory; + @Getter @Value("${js.local.max_errors}") private int maxErrors; @@ -107,12 +109,13 @@ public class NashornJsInvokeService extends AbstractJsInvokeService { @Override public void init() { super.init(); - jsExecutor = MoreExecutors.listeningDecorator(Executors.newWorkStealingPool(jsExecutorThreadPoolSize)); + jsExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(jsExecutorThreadPoolSize, "nashorn-js-executor")); if (useJsSandbox) { sandbox = NashornSandboxes.create(); monitorExecutorService = ThingsBoardExecutors.newWorkStealingPool(monitorThreadPoolSize, "nashorn-js-monitor"); sandbox.setExecutor(monitorExecutorService); sandbox.setMaxCPUTime(maxCpuTime); + sandbox.setMaxMemory(maxMemory); sandbox.allowNoBraces(false); sandbox.allowLoadFunctions(true); sandbox.setMaxPreparedStatements(30); diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java index 7d2198f45a..e9674d767e 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java @@ -16,12 +16,11 @@ package org.thingsboard.server.transport.http; import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -31,6 +30,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -56,6 +56,7 @@ import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetOtaPackageResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseNotificationProto; import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; @@ -429,22 +430,17 @@ public class DeviceApiController implements TbTransportService { .setDeviceIdMSB(sessionInfo.getDeviceIdMSB()) .setDeviceIdLSB(sessionInfo.getDeviceIdLSB()) .setType(firmwareType.name()).build(); - transportContext.getTransportService().process(sessionInfo, requestMsg, new GetOtaPackageCallback(responseWriter, title, version, size, chunk)); + transportContext.getTransportService().process(sessionInfo, requestMsg, new GetOtaPackageCallback(transportContext, responseWriter, title, version, size, chunk)); })); return responseWriter; } - private static class DeviceAuthCallback implements TransportServiceCallback { + @RequiredArgsConstructor + static class DeviceAuthCallback implements TransportServiceCallback { private final TransportContext transportContext; private final DeferredResult responseWriter; private final Consumer onSuccess; - DeviceAuthCallback(TransportContext transportContext, DeferredResult responseWriter, Consumer onSuccess) { - this.transportContext = transportContext; - this.responseWriter = responseWriter; - this.onSuccess = onSuccess; - } - @Override public void onSuccess(ValidateDeviceCredentialsResponse msg) { if (msg.hasDeviceInfo()) { @@ -456,18 +452,21 @@ public class DeviceApiController implements TbTransportService { @Override public void onError(Throwable e) { - log.warn("Failed to process request", e); - responseWriter.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + String body = null; + if (e instanceof HttpMessageNotReadableException || e instanceof JsonParseException) { + body = e.getMessage(); + log.debug("Failed to process request in DeviceAuthCallback: {}", body); + } else { + log.warn("Failed to process request in DeviceAuthCallback", e); + } + responseWriter.setResult(new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR)); } } - private static class DeviceProvisionCallback implements TransportServiceCallback { + @RequiredArgsConstructor + static class DeviceProvisionCallback implements TransportServiceCallback { private final DeferredResult responseWriter; - DeviceProvisionCallback(DeferredResult responseWriter) { - this.responseWriter = responseWriter; - } - @Override public void onSuccess(ProvisionDeviceResponseMsg msg) { responseWriter.setResult(new ResponseEntity<>(JsonConverter.toJson(msg).toString(), HttpStatus.OK)); @@ -475,25 +474,25 @@ public class DeviceApiController implements TbTransportService { @Override public void onError(Throwable e) { - log.warn("Failed to process request", e); - responseWriter.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + String body = null; + if (e instanceof HttpMessageNotReadableException || e instanceof JsonParseException) { + body = e.getMessage(); + log.debug("Failed to process request in DeviceProvisionCallback: {}", body); + } else { + log.warn("Failed to process request in DeviceProvisionCallback", e); + } + responseWriter.setResult(new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR)); } } - private class GetOtaPackageCallback implements TransportServiceCallback { + @RequiredArgsConstructor + static class GetOtaPackageCallback implements TransportServiceCallback { + private final TransportContext transportContext; private final DeferredResult responseWriter; private final String title; private final String version; - private final int chuckSize; - private final int chuck; - - GetOtaPackageCallback(DeferredResult responseWriter, String title, String version, int chuckSize, int chuck) { - this.responseWriter = responseWriter; - this.title = title; - this.version = version; - this.chuckSize = chuckSize; - this.chuck = chuck; - } + private final int chunkSize; + private final int chunk; @Override public void onSuccess(TransportProtos.GetOtaPackageResponseMsg otaPackageResponseMsg) { @@ -501,7 +500,7 @@ public class DeviceApiController implements TbTransportService { responseWriter.setResult(new ResponseEntity<>(HttpStatus.NOT_FOUND)); } else if (title.equals(otaPackageResponseMsg.getTitle()) && version.equals(otaPackageResponseMsg.getVersion())) { String otaPackageId = new UUID(otaPackageResponseMsg.getOtaPackageIdMSB(), otaPackageResponseMsg.getOtaPackageIdLSB()).toString(); - ByteArrayResource resource = new ByteArrayResource(transportContext.getOtaPackageDataCache().get(otaPackageId, chuckSize, chuck)); + ByteArrayResource resource = new ByteArrayResource(transportContext.getOtaPackageDataCache().get(otaPackageId, chunkSize, chunk)); ResponseEntity response = ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + otaPackageResponseMsg.getFileName()) .header("x-filename", otaPackageResponseMsg.getFileName()) @@ -516,8 +515,14 @@ public class DeviceApiController implements TbTransportService { @Override public void onError(Throwable e) { - log.warn("Failed to process request", e); - responseWriter.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + String body = null; + if (e instanceof HttpMessageNotReadableException || e instanceof JsonParseException) { + body = e.getMessage(); + log.debug("Failed to process request in GetOtaPackageCallback: {}", body); + } else { + log.warn("Failed to process request in GetOtaPackageCallback", e); + } + responseWriter.setResult(new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR)); } } diff --git a/common/transport/http/src/test/java/org/thingsboard/server/transport/http/DeviceApiControllerTest.java b/common/transport/http/src/test/java/org/thingsboard/server/transport/http/DeviceApiControllerTest.java new file mode 100644 index 0000000000..ce1e2b2fde --- /dev/null +++ b/common/transport/http/src/test/java/org/thingsboard/server/transport/http/DeviceApiControllerTest.java @@ -0,0 +1,82 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.http; + +import com.google.gson.JsonParseException; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.context.request.async.DeferredResult; +import org.thingsboard.server.common.transport.TransportContext; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.io.IOException; +import java.util.function.Consumer; + +class DeviceApiControllerTest { + + @Test + void deviceAuthCallbackTest() { + TransportContext transportContext = Mockito.mock(TransportContext.class); + DeferredResult responseWriter = Mockito.mock(DeferredResult.class); + Consumer onSuccess = x -> { + }; + var callback = new DeviceApiController.DeviceAuthCallback(transportContext, responseWriter, onSuccess); + + callback.onError(new HttpMessageNotReadableException("JSON incorrect syntax")); + + callback.onError(new JsonParseException("Json ; expected")); + + callback.onError(new IOException("not found")); + + callback.onError(new RuntimeException("oops it is run time error")); + } + + @Test + void deviceProvisionCallbackTest() { + DeferredResult responseWriter = Mockito.mock(DeferredResult.class); + var callback = new DeviceApiController.DeviceProvisionCallback(responseWriter); + + callback.onError(new HttpMessageNotReadableException("JSON incorrect syntax")); + + callback.onError(new JsonParseException("Json ; expected")); + + callback.onError(new IOException("not found")); + + callback.onError(new RuntimeException("oops it is run time error")); + } + + @Test + void getOtaPackageCallback() { + TransportContext transportContext = Mockito.mock(TransportContext.class); + DeferredResult responseWriter = Mockito.mock(DeferredResult.class); + String title = "Title"; + String version = "version"; + int chunkSize = 11; + int chunk = 3; + + var callback = new DeviceApiController.GetOtaPackageCallback(transportContext, responseWriter, title, version, chunkSize, chunk); + + callback.onError(new HttpMessageNotReadableException("JSON incorrect syntax")); + + callback.onError(new JsonParseException("Json ; expected")); + + callback.onError(new IOException("not found")); + + callback.onError(new RuntimeException("oops it is run time error")); + } +} diff --git a/common/transport/http/src/test/resources/logback-test.xml b/common/transport/http/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..359007da5a --- /dev/null +++ b/common/transport/http/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + logger name="org.thingsboard.server.transport.http.DeviceApiController" level="DEBUG" /> + + + + + + diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index 29df8f7f65..cb391b27e9 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -229,7 +229,7 @@ public class LwM2mClient { this.resources.get(pathRezIdVer).updateLwM2mResource(resource, mode); return true; } else { - LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRezIdVer)); + LwM2mPath pathIds = getLwM2mPathFromString(pathRezIdVer); ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); if (resourceModel != null) { this.resources.put(pathRezIdVer, new ResourceValue(resource, resourceModel)); @@ -257,7 +257,7 @@ public class LwM2mClient { } public String getRezIdByResourceNameAndObjectInstanceId(String resourceName, String pathObjectInstanceIdVer, LwM2mModelProvider modelProvider) { - LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathObjectInstanceIdVer)); + LwM2mPath pathIds = getLwM2mPathFromString(pathObjectInstanceIdVer); if (pathIds.isObjectInstance()) { Set rezIds = modelProvider.getObjectModel(registration) .getObjectModel(pathIds.getObjectId()).resources.entrySet() @@ -271,7 +271,7 @@ public class LwM2mClient { } public ResourceModel getResourceModel(String pathIdVer, LwM2mModelProvider modelProvider) { - LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathIdVer)); + LwM2mPath pathIds = getLwM2mPathFromString(pathIdVer); String verSupportedObject = String.valueOf(registration.getSupportedObject().get(pathIds.getObjectId())); String verRez = getVerFromPathIdVerOrId(pathIdVer); return verRez != null && verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration) @@ -289,7 +289,7 @@ public class LwM2mClient { public ObjectModel getObjectModel(String pathIdVer, LwM2mModelProvider modelProvider) { try { - LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathIdVer)); + LwM2mPath pathIds = getLwM2mPathFromString(pathIdVer); String verSupportedObject = String.valueOf(registration.getSupportedObject().get(pathIds.getObjectId())); String verRez = getVerFromPathIdVerOrId(pathIdVer); return verRez != null && verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration) @@ -309,7 +309,7 @@ public class LwM2mClient { public Collection getNewResourceForInstance(String pathRezIdVer, Object params, LwM2mModelProvider modelProvider, LwM2mValueConverter converter) { - LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRezIdVer)); + LwM2mPath pathIds = getLwM2mPathFromString(pathRezIdVer); Collection resources = ConcurrentHashMap.newKeySet(); Map resourceModels = modelProvider.getObjectModel(registration) .getObjectModel(pathIds.getObjectId()).resources; @@ -329,7 +329,7 @@ public class LwM2mClient { */ public Collection getNewResourcesForInstance(String pathRezIdVer, Object params, LwM2mModelProvider modelProvider, LwM2mValueConverter converter) { - LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRezIdVer)); + LwM2mPath pathIds = getLwM2mPathFromString(pathRezIdVer); Collection resources = ConcurrentHashMap.newKeySet(); Map resourceModels = modelProvider.getObjectModel(registration) .getObjectModel(pathIds.getObjectId()).resources; @@ -370,7 +370,7 @@ public class LwM2mClient { } public String isValidObjectVersion(String path) { - LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(path)); + LwM2mPath pathIds = getLwM2mPathFromString(path); LwM2m.Version verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId()); if (verSupportedObject == null) { return String.format("Specified object id %s absent in the list supported objects of the client or is security object!", pathIds.getObjectId()); @@ -390,7 +390,7 @@ public class LwM2mClient { public void deleteResources(String pathIdVer, LwM2mModelProvider modelProvider) { Set key = getKeysEqualsIdVer(pathIdVer); key.forEach(pathRez -> { - LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRez)); + LwM2mPath pathIds = getLwM2mPathFromString(pathRez); ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); if (resourceModel != null) { this.resources.get(pathRez).setResourceModel(resourceModel); @@ -410,7 +410,7 @@ public class LwM2mClient { } private void saveResourceModel(String pathRez, LwM2mModelProvider modelProvider) { - LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRez)); + LwM2mPath pathIds = getLwM2mPathFromString(pathRez); ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); this.resources.get(pathRez).setResourceModel(resourceModel); } @@ -456,5 +456,9 @@ public class LwM2mClient { return result; } + public LwM2mPath getLwM2mPathFromString(String path) { + return new LwM2mPath(fromVersionedIdToObjectId(path)); + } + } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java index 60afd76ffb..138d0d6af8 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java @@ -147,6 +147,7 @@ public class TbInMemoryRegistrationStore implements RegistrationStore, Startable @Override public UpdatedRegistration updateRegistration(RegistrationUpdate update) { + log.trace("updateRegistration [{}]", update); try { lock.writeLock().lock(); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java index 898fef7b2f..436fb479e9 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java @@ -222,6 +222,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab @Override public UpdatedRegistration updateRegistration(RegistrationUpdate update) { + log.trace("updateRegistration [{}]", update); Lock lock = null; try (var connection = connectionFactory.getConnection()) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java index 30800eb9ee..17fef8acb0 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java @@ -15,8 +15,12 @@ */ package org.thingsboard.server.transport.lwm2m.server.store.util; -import com.fasterxml.jackson.annotation.JsonValue; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.google.protobuf.util.JsonFormat; import lombok.SneakyThrows; import org.eclipse.leshan.core.model.ResourceModel; @@ -25,23 +29,35 @@ import org.eclipse.leshan.core.node.LwM2mNodeException; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mSingleResource; import org.eclipse.leshan.core.node.ObjectLink; +import org.eclipse.leshan.core.util.datatype.ULong; import org.eclipse.leshan.server.redis.serialization.RegistrationSerDes; +import org.thingsboard.server.common.data.device.data.PowerMode; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; +import org.thingsboard.server.transport.lwm2m.server.client.ResourceValue; import java.lang.reflect.Field; +import java.time.Instant; import java.util.Base64; import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + +import static org.thingsboard.common.util.JacksonUtil.toJsonNode; public class LwM2MClientSerDes { public static final String VALUE = "value"; + private static final RegistrationSerDes registrationSerDes = new RegistrationSerDes(); @SneakyThrows public static byte[] serialize(LwM2mClient client) { JsonObject o = new JsonObject(); - o.addProperty("nodeId", "client.getNodeId()"); + o.addProperty("nodeId", client.getNodeId()); o.addProperty("endpoint", client.getEndpoint()); JsonObject resources = new JsonObject(); @@ -92,8 +108,15 @@ public class LwM2MClientSerDes { o.addProperty("pagingTransmissionWindow", client.getPagingTransmissionWindow()); } if (client.getRegistration() != null) { - RegistrationSerDes regDez = new RegistrationSerDes(); - o.addProperty("registration", regDez.jSerialize(client.getRegistration()).toString()); + String registrationAddress = client.getRegistration().getAddress().toString(); + JsonNode registrationNode = registrationSerDes.jSerialize(client.getRegistration()); + if (!registrationAddress.equals(registrationNode.get("transportdata").get("address").asText())){ + ObjectNode actualRegAddress = (ObjectNode)registrationNode.get("transportdata"); + actualRegAddress.put("address", registrationAddress); + ObjectNode actualIdentity = (ObjectNode) actualRegAddress.get("identity"); + actualIdentity.put("address", registrationAddress); + } + o.addProperty("registration", registrationNode.toString()); } o.addProperty("asleep", client.isAsleep()); o.addProperty("lastUplinkTime", client.getLastUplinkTime()); @@ -138,33 +161,35 @@ public class LwM2MClientSerDes { ResourceModel.Type type = ResourceModel.Type.valueOf(o.get("type").getAsString()); if (multiInstances) { Map instances = new HashMap<>(); - o.get("instances").getAsJsonArray().forEach(entry -> { -// instances.put(Integer.valueOf(entry.getAsJsonObject().), parseValue(type, entry.getValue())); - }); + for (Entry entry : o.get("instances").getAsJsonObject().entrySet()) { + JsonObject instance = entry.getValue().getAsJsonObject(); + instances.put(Integer.valueOf(instance.get("id").getAsString()), parseValue(type, instance.get(VALUE))); + } return LwM2mMultipleResource.newResource(id, instances, type); } else { - return LwM2mSingleResource.newResource(id, parseValue(type, (JsonValue) o.get(VALUE))); + return LwM2mSingleResource.newResource(id, parseValue(type, o.get(VALUE))); } } - private static Object parseValue(ResourceModel.Type type, JsonValue value) { + + private static Object parseValue(ResourceModel.Type type, JsonElement value) { switch (type) { case INTEGER: - return value.value(); -// case FLOAT: -// return value.value(); -// case BOOLEAN: -// return value.asBoolean(); -// case OPAQUE: -// return Base64.getDecoder().decode(value.asString()); -// case STRING: -// return value.asString(); -// case TIME: -// return new Date(value.asLong()); -// case OBJLNK: -// return ObjectLink.decodeFromString(value.asString()); -// case UNSIGNED_INTEGER: -// return ULong.valueOf(value.asString()); + return value.getAsInt(); + case FLOAT: + return value.getAsDouble(); + case BOOLEAN: + return value.getAsBoolean(); + case OPAQUE: + return Base64.getDecoder().decode(value.getAsString()); + case STRING: + return value.getAsString(); + case TIME: + return Instant.ofEpochMilli(value.getAsLong()); + case OBJLNK: + return ObjectLink.decodeFromString(value.getAsString()); + case UNSIGNED_INTEGER: + return ULong.valueOf(value.getAsString()); default: throw new LwM2mNodeException(String.format("Type %s is not supported", type.name())); } @@ -212,7 +237,7 @@ public class LwM2MClientSerDes { o.addProperty(VALUE, Base64.getEncoder().encodeToString((byte[]) value)); break; case STRING: - o.addProperty(VALUE, (String) value); + o.addProperty(VALUE, String.valueOf(value)); break; case TIME: o.addProperty(VALUE, ((Date) value).getTime()); @@ -221,7 +246,7 @@ public class LwM2MClientSerDes { o.addProperty(VALUE, ((ObjectLink) value).encodeToString()); break; case UNSIGNED_INTEGER: - o.addProperty(VALUE, value.toString()); + o.addProperty(VALUE, Integer.toUnsignedString((int)value)); break; default: throw new LwM2mNodeException(String.format("Type %s is not supported", type.name())); @@ -230,111 +255,111 @@ public class LwM2MClientSerDes { @SneakyThrows public static LwM2mClient deserialize(byte[] data) { -// JsonObject o = new JsonObject(new String(data))); -// LwM2mClient lwM2mClient = new LwM2mClient(o.get("nodeId").getAsString(), o.get("endpoint").getAsString()); - LwM2mClient lwM2mClient = new LwM2mClient("nodeId", "endpoint"); -// o.get("resources").getAsJsonObject().forEach(entry -> { -// JsonObject resource = entry.getValue().asObject(); -// LwM2mResource lwM2mResource = parseLwM2mResource(resource.get("lwM2mResource").getAsJsonObject()); -// ResourceModel resourceModel = parseResourceModel(resource.get("resourceModel").asObject()); -// ResourceValue resourceValue = new ResourceValue(lwM2mResource, resourceModel); -// lwM2mClient.getResources().put(entry.getName(), resourceValue); -// }); -// -// for (JsonObject.Member entry : o.get("sharedAttributes").asObject()) { -// TransportProtos.TsKvProto.Builder builder = TransportProtos.TsKvProto.newBuilder(); -// JsonFormat.parser().merge(entry.getValue().getAsString(), builder); -// lwM2mClient.getSharedAttributes().put(entry.getName(), builder.build()); -// } -// -// o.get("keyTsLatestMap").asObject().forEach(entry -> { -// lwM2mClient.getKeyTsLatestMap().put(entry.getName(), new AtomicLong(entry.getValue().asLong())); -// }); -// -// lwM2mClient.setState(LwM2MClientState.valueOf(o.get("state").getAsString())); -// -// Class lwM2mClientClass = LwM2mClient.class; -// -// JsonValue session = o.get("session"); -// if (session != null) { -// TransportProtos.SessionInfoProto.Builder builder = TransportProtos.SessionInfoProto.newBuilder(); -// JsonFormat.parser().merge(session.asString(), builder); -// -// Field sessionField = lwM2mClientClass.getDeclaredField("session"); -// sessionField.setAccessible(true); -// sessionField.set(lwM2mClient, builder.build()); -// } -// -// JsonValue tenantId = o.get("tenantId"); -// if (tenantId != null) { -// Field tenantIdField = lwM2mClientClass.getDeclaredField("tenantId"); -// tenantIdField.setAccessible(true); -// tenantIdField.set(lwM2mClient, new TenantId(UUID.fromString(tenantId.asString()))); -// } -// -// JsonValue deviceId = o.get("deviceId"); -// if (tenantId != null) { -// Field deviceIdField = lwM2mClientClass.getDeclaredField("deviceId"); -// deviceIdField.setAccessible(true); -// deviceIdField.set(lwM2mClient, UUID.fromString(deviceId.asString())); -// } -// -// JsonValue profileId = o.get("profileId"); -// if (tenantId != null) { -// Field profileIdField = lwM2mClientClass.getDeclaredField("profileId"); -// profileIdField.setAccessible(true); -// profileIdField.set(lwM2mClient, UUID.fromString(profileId.asString())); -// } -// -// JsonValue powerMode = o.get("powerMode"); -// if (powerMode != null) { -// Field powerModeField = lwM2mClientClass.getDeclaredField("powerMode"); -// powerModeField.setAccessible(true); -// powerModeField.set(lwM2mClient, PowerMode.valueOf(powerMode.asString())); -// } -// -// JsonValue edrxCycle = o.get("edrxCycle"); -// if (edrxCycle != null) { -// Field edrxCycleField = lwM2mClientClass.getDeclaredField("edrxCycle"); -// edrxCycleField.setAccessible(true); -// edrxCycleField.set(lwM2mClient, edrxCycle.asLong()); -// } -// -// JsonValue psmActivityTimer = o.get("psmActivityTimer"); -// if (psmActivityTimer != null) { -// Field psmActivityTimerField = lwM2mClientClass.getDeclaredField("psmActivityTimer"); -// psmActivityTimerField.setAccessible(true); -// psmActivityTimerField.set(lwM2mClient, psmActivityTimer.asLong()); -// } -// -// JsonValue pagingTransmissionWindow = o.get("pagingTransmissionWindow"); -// if (pagingTransmissionWindow != null) { -// Field pagingTransmissionWindowField = lwM2mClientClass.getDeclaredField("pagingTransmissionWindow"); -// pagingTransmissionWindowField.setAccessible(true); -// pagingTransmissionWindowField.set(lwM2mClient, pagingTransmissionWindow.asLong()); -// } -// -// JsonValue registration = o.get("registration"); -// if (registration != null) { -// lwM2mClient.setRegistration(RegistrationSerDes.deserialize(registration.asObject())); -// } -// -// lwM2mClient.setAsleep(o.get("asleep").getAsBoolean()); -// -// Field lastUplinkTimeField = lwM2mClientClass.getDeclaredField("lastUplinkTime"); -// lastUplinkTimeField.setAccessible(true); -// lastUplinkTimeField.setLong(lwM2mClient, o.get("lastUplinkTime").asLong()); -// -// Field firstEdrxDownlinkField = lwM2mClientClass.getDeclaredField("firstEdrxDownlink"); -// firstEdrxDownlinkField.setAccessible(true); -// firstEdrxDownlinkField.setBoolean(lwM2mClient, o.get("firstEdrxDownlink").getAsBoolean()); -// -// lwM2mClient.getRetryAttempts().set(o.get("retryAttempts").asInt()); -// -// JsonValue lastSentRpcId = o.get("lastSentRpcId"); -// if (lastSentRpcId != null) { -// lwM2mClient.setLastSentRpcId(UUID.fromString(lastSentRpcId.asString())); -// } + JsonObject o = JsonParser.parseString(new String(data)).getAsJsonObject(); + LwM2mClient lwM2mClient = new LwM2mClient(o.get("nodeId").getAsString(), o.get("endpoint").getAsString()); + + o.get("resources").getAsJsonObject().entrySet().forEach(entry -> { + JsonObject resource = entry.getValue().getAsJsonObject(); + LwM2mResource lwM2mResource = parseLwM2mResource(resource.get("lwM2mResource").getAsJsonObject()); + ResourceModel resourceModel = parseResourceModel(resource.get("resourceModel").getAsJsonObject()); + ResourceValue resourceValue = new ResourceValue(lwM2mResource, resourceModel); + lwM2mClient.getResources().put(String.valueOf(lwM2mResource.getId()), resourceValue); + }); + + for (Entry entry : o.get("sharedAttributes").getAsJsonObject().entrySet()) { + TransportProtos.TsKvProto.Builder builder = TransportProtos.TsKvProto.newBuilder(); + JsonFormat.parser().merge(entry.getValue().getAsString(), builder); + lwM2mClient.getSharedAttributes().put(entry.getKey(), builder.build()); + } + + o.get("keyTsLatestMap").getAsJsonObject().entrySet().forEach(entry -> { + lwM2mClient.getKeyTsLatestMap().put(entry.getKey(), new AtomicLong(entry.getValue().getAsLong())); + }); + + lwM2mClient.setState(LwM2MClientState.valueOf(o.get("state").getAsString())); + + Class lwM2mClientClass = LwM2mClient.class; + + JsonElement session = o.get("session"); + if (session != null) { + TransportProtos.SessionInfoProto.Builder builder = TransportProtos.SessionInfoProto.newBuilder(); + JsonFormat.parser().merge(session.getAsString(), builder); + + Field sessionField = lwM2mClientClass.getDeclaredField("session"); + sessionField.setAccessible(true); + sessionField.set(lwM2mClient, builder.build()); + } + + JsonElement tenantId = o.get("tenantId"); + if (tenantId != null) { + Field tenantIdField = lwM2mClientClass.getDeclaredField("tenantId"); + tenantIdField.setAccessible(true); + tenantIdField.set(lwM2mClient, new TenantId(UUID.fromString(tenantId.getAsString()))); + } + + JsonElement deviceId = o.get("deviceId"); + if (tenantId != null) { + Field deviceIdField = lwM2mClientClass.getDeclaredField("deviceId"); + deviceIdField.setAccessible(true); + deviceIdField.set(lwM2mClient, UUID.fromString(deviceId.getAsString())); + } + + JsonElement profileId = o.get("profileId"); + if (tenantId != null) { + Field profileIdField = lwM2mClientClass.getDeclaredField("profileId"); + profileIdField.setAccessible(true); + profileIdField.set(lwM2mClient, UUID.fromString(profileId.getAsString())); + } + + JsonElement powerMode = o.get("powerMode"); + if (powerMode != null) { + Field powerModeField = lwM2mClientClass.getDeclaredField("powerMode"); + powerModeField.setAccessible(true); + powerModeField.set(lwM2mClient, PowerMode.valueOf(powerMode.getAsString())); + } + + JsonElement edrxCycle = o.get("edrxCycle"); + if (edrxCycle != null) { + Field edrxCycleField = lwM2mClientClass.getDeclaredField("edrxCycle"); + edrxCycleField.setAccessible(true); + edrxCycleField.set(lwM2mClient, edrxCycle.getAsLong()); + } + + JsonElement psmActivityTimer = o.get("psmActivityTimer"); + if (psmActivityTimer != null) { + Field psmActivityTimerField = lwM2mClientClass.getDeclaredField("psmActivityTimer"); + psmActivityTimerField.setAccessible(true); + psmActivityTimerField.set(lwM2mClient, psmActivityTimer.getAsLong()); + } + + JsonElement pagingTransmissionWindow = o.get("pagingTransmissionWindow"); + if (pagingTransmissionWindow != null) { + Field pagingTransmissionWindowField = lwM2mClientClass.getDeclaredField("pagingTransmissionWindow"); + pagingTransmissionWindowField.setAccessible(true); + pagingTransmissionWindowField.set(lwM2mClient, pagingTransmissionWindow.getAsLong()); + } + + JsonElement registration = o.get("registration"); + if (registration != null) { + lwM2mClient.setRegistration(registrationSerDes.deserialize(toJsonNode(registration.getAsString()))); + } + + lwM2mClient.setAsleep(o.get("asleep").getAsBoolean()); + + Field lastUplinkTimeField = lwM2mClientClass.getDeclaredField("lastUplinkTime"); + lastUplinkTimeField.setAccessible(true); + lastUplinkTimeField.setLong(lwM2mClient, o.get("lastUplinkTime").getAsLong()); + + Field firstEdrxDownlinkField = lwM2mClientClass.getDeclaredField("firstEdrxDownlink"); + firstEdrxDownlinkField.setAccessible(true); + firstEdrxDownlinkField.setBoolean(lwM2mClient, o.get("firstEdrxDownlink").getAsBoolean()); + + lwM2mClient.getRetryAttempts().set(o.get("retryAttempts").getAsInt()); + + JsonElement lastSentRpcId = o.get("lastSentRpcId"); + if (lastSentRpcId != null) { + lwM2mClient.setLastSentRpcId(UUID.fromString(lastSentRpcId.getAsString())); + } return lwM2mClient; } diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServiceTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServiceTest.java deleted file mode 100644 index 9f49f3b138..0000000000 --- a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServiceTest.java +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.transport.lwm2m.bootstrap; - -import org.eclipse.californium.core.network.CoapEndpoint; -import org.eclipse.californium.scandium.config.DtlsConnectorConfig; -import org.junit.Ignore; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; -import org.thingsboard.server.common.transport.TransportService; -import org.thingsboard.server.transport.lwm2m.bootstrap.secure.TbLwM2MDtlsBootstrapCertificateVerifier; -import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MBootstrapSecurityStore; -import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MInMemoryBootstrapConfigStore; -import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig; -import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.when; - -@ExtendWith(MockitoExtension.class) -public class LwM2MTransportBootstrapServiceTest { - - @Mock - private LwM2MTransportServerConfig serverConfig; - @Mock - private LwM2MTransportBootstrapConfig bootstrapConfig; - @Mock - private LwM2MBootstrapSecurityStore lwM2MBootstrapSecurityStore; - @Mock - private LwM2MInMemoryBootstrapConfigStore lwM2MInMemoryBootstrapConfigStore; - @Mock - private TransportService transportService; - @Mock - private TbLwM2MDtlsBootstrapCertificateVerifier certificateVerifier; - - @Disabled // fixme: nick - @Test - public void getLHServer_creates_ConnectionIdGenerator_when_connection_id_length_not_null(){ - final Integer CONNECTION_ID_LENGTH = 6; - when(serverConfig.getDtlsCidLength()).thenReturn(CONNECTION_ID_LENGTH); - var lwM2MBootstrapService = createLwM2MBootstrapService(); - - var server = lwM2MBootstrapService.getLhBootstrapServer(); - var securedEndpoint = (CoapEndpoint) ReflectionTestUtils.getField(server, "securedEndpoint"); - assertThat(securedEndpoint).isNotNull(); - - var config = (DtlsConnectorConfig) ReflectionTestUtils.getField(securedEndpoint.getConnector(), "config"); - assertThat(config).isNotNull(); - assertThat(config.getConnectionIdGenerator()).isNotNull(); - assertThat((Integer) ReflectionTestUtils.getField(config.getConnectionIdGenerator(), "connectionIdLength")) - .isEqualTo(CONNECTION_ID_LENGTH); - } - - @Disabled // fixme: nick - @Test - public void getLHServer_creates_no_ConnectionIdGenerator_when_connection_id_length_is_null(){ - when(serverConfig.getDtlsCidLength()).thenReturn(null); - var lwM2MBootstrapService = createLwM2MBootstrapService(); - - var server = lwM2MBootstrapService.getLhBootstrapServer(); - var securedEndpoint = (CoapEndpoint) ReflectionTestUtils.getField(server, "securedEndpoint"); - assertThat(securedEndpoint).isNotNull(); - - var config = (DtlsConnectorConfig) ReflectionTestUtils.getField(securedEndpoint.getConnector(), "config"); - assertThat(config).isNotNull(); - assertThat(config.getConnectionIdGenerator()).isNull(); - } - - private LwM2MTransportBootstrapService createLwM2MBootstrapService() { - setDefaultConfigVariables(); - return new LwM2MTransportBootstrapService(serverConfig, bootstrapConfig, lwM2MBootstrapSecurityStore, - lwM2MInMemoryBootstrapConfigStore, transportService, certificateVerifier); - } - - private void setDefaultConfigVariables(){ - when(bootstrapConfig.getPort()).thenReturn(5683); - when(bootstrapConfig.getSecurePort()).thenReturn(5684); - when(serverConfig.isRecommendedCiphers()).thenReturn(false); - when(serverConfig.getDtlsRetransmissionTimeout()).thenReturn(9000); - } - - -} \ No newline at end of file diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientTest.java index 5bc334756d..8fe58a5276 100644 --- a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientTest.java +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientTest.java @@ -15,9 +15,10 @@ */ package org.thingsboard.server.transport.lwm2m.server.client; +import org.eclipse.leshan.core.endpoint.EndpointUriUtil; import org.eclipse.leshan.core.link.Link; +import org.eclipse.leshan.core.peer.IpPeer; import org.eclipse.leshan.server.registration.Registration; -import org.junit.Ignore; import org.junit.Test; import org.junit.jupiter.api.Assertions; @@ -25,14 +26,14 @@ import java.net.InetSocketAddress; public class LwM2mClientTest { - @Ignore @Test public void setRegistration() { LwM2mClient client = new LwM2mClient("nodeId", "testEndpoint"); - Registration registration = null; /*new Registration - .Builder("test", "testEndpoint", Identity.unsecure(new InetSocketAddress(1000))) // FIXME: nick + Registration registration = new Registration + .Builder("testId", "testEndpoint", new IpPeer(new InetSocketAddress(1000)), + EndpointUriUtil.createUri("coap://localhost:5685")) .objectLinks(new Link[0]) - .build();*/ + .build(); Assertions.assertDoesNotThrow(() -> client.setRegistration(registration)); } diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDesTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDesTest.java index 43187f2a36..b3473a67b2 100644 --- a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDesTest.java +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDesTest.java @@ -15,13 +15,17 @@ */ package org.thingsboard.server.transport.lwm2m.server.store.util; +import org.eclipse.leshan.core.LwM2m.LwM2mVersion; +import org.eclipse.leshan.core.endpoint.EndpointUriUtil; +import org.eclipse.leshan.core.link.Link; import org.eclipse.leshan.core.node.LwM2mMultipleResource; +import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mSingleResource; +import org.eclipse.leshan.core.peer.IpPeer; import org.eclipse.leshan.core.request.WriteRequest; import org.eclipse.leshan.server.registration.Registration; import org.junit.Assert; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.device.data.PowerMode; @@ -41,9 +45,12 @@ import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; import org.thingsboard.server.transport.lwm2m.server.client.ResourceValue; +import java.net.Inet4Address; +import java.net.InetSocketAddress; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.UUID; @@ -56,10 +63,9 @@ import static org.mockito.Mockito.when; public class LwM2MClientSerDesTest { - @Ignore @Test public void serializeDeserialize() throws Exception { - LwM2mClient client = new LwM2mClient("nodeId", "testEndpoint"); + LwM2mClient client = new LwM2mClient("nodeId", "endpoint"); TransportDeviceInfo tdi = new TransportDeviceInfo(); tdi.setPowerMode(PowerMode.PSM); @@ -78,13 +84,13 @@ public class LwM2MClientSerDesTest { client.init(credentialsResponse, UUID.randomUUID()); - Registration registration = null; // FIXME: nick -// new Registration.Builder("test", "testEndpoint", Identity -// .unsecure(new InetSocketAddress(1000))) -// .supportedContentFormats() -// .supportedObjects(Map.of(15, "1.0", 17, "1.0")) -// .objectLinks(new Link[]{new Link("/")}) -// .build(); + Registration registration = new Registration + .Builder("test", "endpoint", new IpPeer(new InetSocketAddress(Inet4Address.getLoopbackAddress(), 1000)), + EndpointUriUtil.createUri("coap://localhost:5685")) + .supportedContentFormats() + .supportedObjects(Map.of(15, LwM2mVersion.V1_0, 17, LwM2mVersion.V1_0)) + .objectLinks(new Link[] { new Link("/15"), new Link("/17") }) + .build(); client.setRegistration(registration); client.setState(LwM2MClientState.REGISTERED); @@ -130,7 +136,12 @@ public class LwM2MClientSerDesTest { assertEquals(client.getPsmActivityTimer(), desClient.getPsmActivityTimer()); assertEquals(client.getPagingTransmissionWindow(), desClient.getPagingTransmissionWindow()); assertEquals(client.getEdrxCycle(), desClient.getEdrxCycle()); - assertEquals(client.getRegistration(), desClient.getRegistration()); + if (((IpPeer)desClient.getRegistration().getClientTransportData()).getSocketAddress().isUnresolved()) { + String actualReg = desClient.getRegistration().toString().replaceAll("/", ""); + assertEquals(client.getRegistration().toString(), actualReg); + } else { + assertEquals(client.getRegistration(), desClient.getRegistration()); + } assertEquals(client.isAsleep(), desClient.isAsleep()); assertEquals(client.getLastUplinkTime(), desClient.getLastUplinkTime()); assertEquals(client.getSleepTask(), desClient.getSleepTask()); @@ -143,7 +154,11 @@ public class LwM2MClientSerDesTest { Map actualResources = desClient.getResources(); assertNotNull(actualResources); assertEquals(expectedResources.size(), actualResources.size()); - expectedResources.forEach((key, value) -> assertEquals(value.toString(), actualResources.get(key).toString())); + for (Entry entry : expectedResources.entrySet()) { + LwM2mPath expectedPathId = client.getLwM2mPathFromString(entry.getKey().toString()); + String actualOld = actualResources.get(String.valueOf(expectedPathId.getObjectId())).toString(); + String actual = actualOld.replaceAll("\"", ""); + assertEquals(entry.getValue().toString(), actual); + } } - } \ No newline at end of file diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index 4190c6bbc9..6817ec71f0 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -30,6 +30,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.KvEntry; @@ -50,6 +51,7 @@ import java.util.regex.Pattern; /** * Created by Valerii Sosliuk on 5/12/2017. */ +@Slf4j public class JacksonUtil { public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @@ -158,6 +160,20 @@ public class JacksonUtil { } } + public static String toPlainText(String data) { + if (data == null) { + return null; + } + if (data.startsWith("\"") && data.endsWith("\"") && data.length() >= 2) { + final String dataBefore = data; + try { + data = JacksonUtil.fromString(data, String.class); + } catch (Exception ignored) {} + log.trace("Trimming double quotes. Before trim: [{}], after trim: [{}]", dataBefore, data); + } + return data; + } + public static T treeToValue(JsonNode node, Class clazz) { try { return OBJECT_MAPPER.treeToValue(node, clazz); @@ -239,6 +255,10 @@ public class JacksonUtil { return node; } + public static ObjectNode asObject(JsonNode node) { + return node != null && node.isObject() ? ((ObjectNode) node) : newObjectNode(); + } + public static void replaceUuidsRecursively(JsonNode node, Set skippedRootFields, Pattern includedFieldsPattern, UnaryOperator replacer, boolean root) { if (node == null) { return; diff --git a/common/util/src/test/java/org/thingsboard/common/util/JacksonUtilTest.java b/common/util/src/test/java/org/thingsboard/common/util/JacksonUtilTest.java index 7e37d34509..dcddc6e07a 100644 --- a/common/util/src/test/java/org/thingsboard/common/util/JacksonUtilTest.java +++ b/common/util/src/test/java/org/thingsboard/common/util/JacksonUtilTest.java @@ -19,6 +19,9 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Assert; import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.AssetId; @@ -55,4 +58,15 @@ public class JacksonUtilTest { Assert.assertEquals(asset.getName(), result.getName()); Assert.assertEquals(asset.getType(), result.getType()); } + + @ParameterizedTest + @ValueSource(strings = { "", "false", "\"", "\"\"", "\"This is a string with double quotes\"", "Path: /home/developer/test.txt", + "First line\nSecond line\n\nFourth line", "Before\rAfter", "Tab\tSeparated\tValues", "Test\bbackspace", "[]", + "[1, 2, 3]", "{\"key\": \"value\"}", "{\n\"temperature\": 25.5,\n\"humidity\": 50.2\n\"}", "Expression: (a + b) * c", + "世界", "Україна", "\u1F1FA\u1F1E6", "🇺🇦"}) + public void toPlainTextTest(String original) { + String serialized = JacksonUtil.toString(original); + Assertions.assertNotNull(serialized); + Assertions.assertEquals(original, JacksonUtil.toPlainText(serialized)); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index 0e7fe8f108..865306c73b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -56,11 +56,19 @@ public abstract class DaoUtil { } public static Pageable toPageable(PageLink pageLink) { - return toPageable(pageLink, Collections.emptyMap()); + return toPageable(pageLink, true); + } + + public static Pageable toPageable(PageLink pageLink, boolean addDefaultSorting) { + return toPageable(pageLink, Collections.emptyMap(), addDefaultSorting); } public static Pageable toPageable(PageLink pageLink, Map columnMap) { - return PageRequest.of(pageLink.getPage(), pageLink.getPageSize(), pageLink.toSort(pageLink.getSortOrder(), columnMap)); + return toPageable(pageLink, columnMap, true); + } + + public static Pageable toPageable(PageLink pageLink, Map columnMap, boolean addDefaultSorting) { + return PageRequest.of(pageLink.getPage(), pageLink.getPageSize(), pageLink.toSort(pageLink.getSortOrder(), columnMap, addDefaultSorting)); } public static Pageable toPageable(PageLink pageLink, List sortOrders) { @@ -68,7 +76,11 @@ public abstract class DaoUtil { } public static Pageable toPageable(PageLink pageLink, Map columnMap, List sortOrders) { - return PageRequest.of(pageLink.getPage(), pageLink.getPageSize(), pageLink.toSort(sortOrders, columnMap)); + return toPageable(pageLink, columnMap, sortOrders, true); + } + + public static Pageable toPageable(PageLink pageLink, Map columnMap, List sortOrders, boolean addDefaultSorting) { + return PageRequest.of(pageLink.getPage(), pageLink.getPageSize(), pageLink.toSort(sortOrders, columnMap, addDefaultSorting)); } public static List convertDataList(Collection> toDataList) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java index c752ae706d..19b02fc932 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java @@ -80,6 +80,8 @@ public interface AlarmDao extends Dao { PageData findAlarmIdsByAssigneeId(TenantId tenantId, UUID userId, PageLink pageLink); + PageData findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, PageLink pageLink); + void createEntityAlarmRecord(EntityAlarm entityAlarm); List findEntityAlarmRecords(TenantId tenantId, AlarmId id); diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java index ac6f4955d8..2c111151b3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java @@ -82,14 +82,14 @@ public class BaseAlarmCommentService extends AbstractEntityService implements Al @Override public ListenableFuture findAlarmCommentByIdAsync(TenantId tenantId, AlarmCommentId alarmCommentId) { log.trace("Executing findAlarmCommentByIdAsync by alarmCommentId [{}]", alarmCommentId); - validateId(alarmCommentId, "Incorrect alarmCommentId " + alarmCommentId); + validateId(alarmCommentId, id -> "Incorrect alarmCommentId " + id); return alarmCommentDao.findAlarmCommentByIdAsync(tenantId, alarmCommentId.getId()); } @Override public AlarmComment findAlarmCommentById(TenantId tenantId, AlarmCommentId alarmCommentId) { log.trace("Executing findAlarmCommentByIdAsync by alarmCommentId [{}]", alarmCommentId); - validateId(alarmCommentId, "Incorrect alarmCommentId " + alarmCommentId); + validateId(alarmCommentId, id -> "Incorrect alarmCommentId " + id); return alarmCommentDao.findById(tenantId, alarmCommentId.getId()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 72e1b9c7bf..d49cef22b7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -169,7 +169,7 @@ public class BaseAlarmService extends AbstractCachedEntityService findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds) { - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validateEntityDataPageLink(query.getPageLink()); return alarmDao.findAlarmDataByQueryForEntities(tenantId, query, orderedEntityIds); } @@ -261,21 +261,21 @@ public class BaseAlarmService extends AbstractCachedEntityService "Incorrect alarmId " + id); return alarmDao.findAlarmById(tenantId, alarmId.getId()); } @Override public ListenableFuture findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId) { log.trace("Executing findAlarmByIdAsync [{}]", alarmId); - validateId(alarmId, "Incorrect alarmId " + alarmId); + validateId(alarmId, id -> "Incorrect alarmId " + id); return alarmDao.findAlarmByIdAsync(tenantId, alarmId.getId()); } @Override public AlarmInfo findAlarmInfoById(TenantId tenantId, AlarmId alarmId) { log.trace("Executing findAlarmInfoByIdAsync [{}]", alarmId); - validateId(alarmId, "Incorrect alarmId " + alarmId); + validateId(alarmId, id -> "Incorrect alarmId " + id); return alarmDao.findAlarmInfoById(tenantId, alarmId.getId()); } @@ -302,10 +302,16 @@ public class BaseAlarmService extends AbstractCachedEntityService findAlarmIdsByAssigneeId(TenantId tenantId, UserId userId, PageLink pageLink) { log.trace("[{}] Executing findAlarmIdsByAssigneeId [{}]", tenantId, userId); - validateId(userId, "Incorrect userId " + userId); + validateId(userId, id -> "Incorrect userId " + id); return alarmDao.findAlarmIdsByAssigneeId(tenantId, userId.getId(), pageLink); } + @Override + public PageData findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, PageLink pageLink) { + log.trace("[{}] Executing findAlarmsByOriginatorId [{}]", tenantId, originatorId); + return alarmDao.findAlarmIdsByOriginatorId(tenantId, originatorId, pageLink); + } + @Override public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId) { @@ -336,14 +342,14 @@ public class BaseAlarmService extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); return alarmDao.countAlarmsByQuery(tenantId, customerId, query); } @Override public PageData findAlarmTypesByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findAlarmTypesByTenantId, tenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); if (DEFAULT_ALARM_TYPES_PAGE_LINK.equals(pageLink)) { return cache.getAndPutInTransaction(tenantId, () -> alarmDao.findTenantAlarmTypes(tenantId.getId(), pageLink), false); diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index 5f298d5e76..fb5b799ae7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -103,7 +103,7 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService INCORRECT_ASSET_PROFILE_ID + id); return cache.getOrFetchFromDB(AssetProfileCacheKey.fromId(assetProfileId), () -> assetProfileDao.findById(tenantId, assetProfileId.getId()), true, putInCache); } @@ -116,7 +116,7 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService INCORRECT_ASSET_PROFILE_NAME + s); return cache.getOrFetchFromDB(AssetProfileCacheKey.fromName(tenantId, profileName), () -> assetProfileDao.findByName(tenantId, profileName), false, putInCache); } @@ -124,7 +124,7 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService INCORRECT_ASSET_PROFILE_ID + id); return toAssetProfileInfo(findAssetProfileById(tenantId, assetProfileId)); } @@ -179,7 +179,7 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService INCORRECT_ASSET_PROFILE_ID + id); AssetProfile assetProfile = assetProfileDao.findById(tenantId, assetProfileId.getId()); if (assetProfile != null && assetProfile.isDefault()) { throw new DataValidationException("Deletion of Default Asset Profile is prohibited!"); @@ -208,7 +208,7 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService findAssetProfiles(TenantId tenantId, PageLink pageLink) { log.trace("Executing findAssetProfiles tenantId [{}], pageLink [{}]", tenantId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); Validator.validatePageLink(pageLink); return assetProfileDao.findAssetProfiles(tenantId, pageLink); } @@ -216,7 +216,7 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService findAssetProfileInfos(TenantId tenantId, PageLink pageLink) { log.trace("Executing findAssetProfileInfos tenantId [{}], pageLink [{}]", tenantId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); Validator.validatePageLink(pageLink); return assetProfileDao.findAssetProfileInfos(tenantId, pageLink); } @@ -246,7 +246,7 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); AssetProfile assetProfile = new AssetProfile(); assetProfile.setTenantId(tenantId); assetProfile.setDefault(defaultProfile); @@ -258,7 +258,7 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); return cache.getAndPutInTransaction(AssetProfileCacheKey.defaultProfile(tenantId), () -> assetProfileDao.findDefaultAssetProfile(tenantId), true); } @@ -266,14 +266,14 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); return toAssetProfileInfo(findDefaultAssetProfile(tenantId)); } @Override public boolean setDefaultAssetProfile(TenantId tenantId, AssetProfileId assetProfileId) { log.trace("Executing setDefaultAssetProfile [{}]", assetProfileId); - Validator.validateId(assetProfileId, INCORRECT_ASSET_PROFILE_ID + assetProfileId); + Validator.validateId(assetProfileId, id -> INCORRECT_ASSET_PROFILE_ID + id); AssetProfile assetProfile = assetProfileDao.findById(tenantId, assetProfileId.getId()); if (!assetProfile.isDefault()) { assetProfile.setDefault(true); @@ -299,7 +299,7 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); tenantAssetProfilesRemover.removeEntities(tenantId, tenantId); } @@ -322,7 +322,7 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService findAssetProfileNamesByTenantId(TenantId tenantId, boolean activeOnly) { log.trace("Executing findAssetProfileNamesByTenantId, tenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); return assetProfileDao.findTenantAssetProfileNames(tenantId.getId(), activeOnly) .stream().sorted(Comparator.comparing(EntityInfo::getName)) .collect(Collectors.toList()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index 96b104f097..f7342e7634 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -75,7 +75,6 @@ public class BaseAssetService extends AbstractCachedEntityService INCORRECT_ASSET_ID + id); return assetDao.findAssetInfoById(tenantId, assetId.getId()); } @Override public Asset findAssetById(TenantId tenantId, AssetId assetId) { log.trace("Executing findAssetById [{}]", assetId); - validateId(assetId, INCORRECT_ASSET_ID + assetId); + validateId(assetId, id -> INCORRECT_ASSET_ID + id); return assetDao.findById(tenantId, assetId.getId()); } @Override public ListenableFuture findAssetByIdAsync(TenantId tenantId, AssetId assetId) { log.trace("Executing findAssetById [{}]", assetId); - validateId(assetId, INCORRECT_ASSET_ID + assetId); + validateId(assetId, id -> INCORRECT_ASSET_ID + id); return assetDao.findByIdAsync(tenantId, assetId.getId()); } @Override public Asset findAssetByTenantIdAndName(TenantId tenantId, String name) { log.trace("Executing findAssetByTenantIdAndName [{}][{}]", tenantId, name); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); return cache.getAndPutInTransaction(new AssetCacheKey(tenantId, name), () -> assetDao.findAssetsByTenantIdAndName(tenantId.getId(), name) .orElse(null), true); @@ -199,7 +198,7 @@ public class BaseAssetService extends AbstractCachedEntityService INCORRECT_ASSET_ID + id); if (entityViewService.existsByTenantIdAndEntityId(tenantId, assetId)) { throw new DataValidationException("Can't delete asset that has entity views!"); } @@ -223,7 +222,7 @@ public class BaseAssetService extends AbstractCachedEntityService findAssetsByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findAssetsByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validatePageLink(pageLink); return assetDao.findAssetsByTenantId(tenantId.getId(), pageLink); } @@ -231,7 +230,7 @@ public class BaseAssetService extends AbstractCachedEntityService findAssetInfosByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findAssetInfosByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validatePageLink(pageLink); return assetDao.findAssetInfosByTenantId(tenantId.getId(), pageLink); } @@ -239,8 +238,8 @@ public class BaseAssetService extends AbstractCachedEntityService findAssetsByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) { log.trace("Executing findAssetsByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateString(type, "Incorrect type " + type); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateString(type, t -> "Incorrect type " + t); validatePageLink(pageLink); return assetDao.findAssetsByTenantIdAndType(tenantId.getId(), type, pageLink); } @@ -248,8 +247,8 @@ public class BaseAssetService extends AbstractCachedEntityService findAssetInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) { log.trace("Executing findAssetInfosByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateString(type, "Incorrect type " + type); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateString(type, t -> "Incorrect type " + t); validatePageLink(pageLink); return assetDao.findAssetInfosByTenantIdAndType(tenantId.getId(), type, pageLink); } @@ -257,8 +256,8 @@ public class BaseAssetService extends AbstractCachedEntityService findAssetInfosByTenantIdAndAssetProfileId(TenantId tenantId, AssetProfileId assetProfileId, PageLink pageLink) { log.trace("Executing findAssetInfosByTenantIdAndAssetProfileId, tenantId [{}], assetProfileId [{}], pageLink [{}]", tenantId, assetProfileId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(assetProfileId, INCORRECT_ASSET_PROFILE_ID + assetProfileId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(assetProfileId, id -> INCORRECT_ASSET_PROFILE_ID + id); validatePageLink(pageLink); return assetDao.findAssetInfosByTenantIdAndAssetProfileId(tenantId.getId(), assetProfileId.getId(), pageLink); } @@ -266,23 +265,23 @@ public class BaseAssetService extends AbstractCachedEntityService> findAssetsByTenantIdAndIdsAsync(TenantId tenantId, List assetIds) { log.trace("Executing findAssetsByTenantIdAndIdsAsync, tenantId [{}], assetIds [{}]", tenantId, assetIds); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateIds(assetIds, "Incorrect assetIds " + assetIds); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateIds(assetIds, ids -> "Incorrect assetIds " + ids); return assetDao.findAssetsByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(assetIds)); } @Override public void deleteAssetsByTenantId(TenantId tenantId) { log.trace("Executing deleteAssetsByTenantId, tenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); tenantAssetsRemover.removeEntities(tenantId, tenantId); } @Override public PageData findAssetsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findAssetsByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validatePageLink(pageLink); return assetDao.findAssetsByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); } @@ -290,8 +289,8 @@ public class BaseAssetService extends AbstractCachedEntityService findAssetInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findAssetInfosByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validatePageLink(pageLink); return assetDao.findAssetInfosByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); } @@ -299,9 +298,9 @@ public class BaseAssetService extends AbstractCachedEntityService findAssetsByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { log.trace("Executing findAssetsByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); - validateString(type, "Incorrect type " + type); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); + validateString(type, t -> "Incorrect type " + t); validatePageLink(pageLink); return assetDao.findAssetsByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); } @@ -309,9 +308,9 @@ public class BaseAssetService extends AbstractCachedEntityService findAssetInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { log.trace("Executing findAssetInfosByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); - validateString(type, "Incorrect type " + type); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); + validateString(type, t -> "Incorrect type " + t); validatePageLink(pageLink); return assetDao.findAssetInfosByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); } @@ -319,9 +318,9 @@ public class BaseAssetService extends AbstractCachedEntityService findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(TenantId tenantId, CustomerId customerId, AssetProfileId assetProfileId, PageLink pageLink) { log.trace("Executing findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId, tenantId [{}], customerId [{}], assetProfileId [{}], pageLink [{}]", tenantId, customerId, assetProfileId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); - validateId(assetProfileId, INCORRECT_ASSET_PROFILE_ID + assetProfileId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); + validateId(assetProfileId, id -> INCORRECT_ASSET_PROFILE_ID + id); validatePageLink(pageLink); return assetDao.findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(tenantId.getId(), customerId.getId(), assetProfileId.getId(), pageLink); } @@ -329,17 +328,17 @@ public class BaseAssetService extends AbstractCachedEntityService> findAssetsByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List assetIds) { log.trace("Executing findAssetsByTenantIdAndCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], assetIds [{}]", tenantId, customerId, assetIds); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); - validateIds(assetIds, "Incorrect assetIds " + assetIds); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); + validateIds(assetIds, ids -> "Incorrect assetIds " + ids); return assetDao.findAssetsByTenantIdAndCustomerIdAndIdsAsync(tenantId.getId(), customerId.getId(), toUUIDs(assetIds)); } @Override public void unassignCustomerAssets(TenantId tenantId, CustomerId customerId) { log.trace("Executing unassignCustomerAssets, tenantId [{}], customerId [{}]", tenantId, customerId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); customerAssetsUnasigner.removeEntities(tenantId, customerId); } @@ -371,7 +370,7 @@ public class BaseAssetService extends AbstractCachedEntityService> findAssetTypesByTenantId(TenantId tenantId) { log.trace("Executing findAssetTypesByTenantId, tenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); return assetDao.findTenantAssetTypesAsync(tenantId.getId()); } @@ -420,8 +419,8 @@ public class BaseAssetService extends AbstractCachedEntityService findAssetsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink) { log.trace("Executing findAssetsByTenantIdAndEdgeId, tenantId [{}], edgeId [{}], pageLink [{}]", tenantId, edgeId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(edgeId, id -> INCORRECT_EDGE_ID + id); validatePageLink(pageLink); return assetDao.findAssetsByTenantIdAndEdgeId(tenantId.getId(), edgeId.getId(), pageLink); } @@ -429,9 +428,9 @@ public class BaseAssetService extends AbstractCachedEntityService findAssetsByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink) { log.trace("Executing findAssetsByTenantIdAndEdgeIdAndType, tenantId [{}], edgeId [{}], type [{}] pageLink [{}]", tenantId, edgeId, type, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(edgeId, INCORRECT_EDGE_ID + edgeId); - validateString(type, "Incorrect type " + type); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(edgeId, id -> INCORRECT_EDGE_ID + id); + validateString(type, t -> "Incorrect type " + t); validatePageLink(pageLink); return assetDao.findAssetsByTenantIdAndEdgeIdAndType(tenantId.getId(), edgeId.getId(), type, pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeUtils.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeUtils.java index 65e52e06d2..7e705c2278 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeUtils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeUtils.java @@ -28,12 +28,12 @@ public class AttributeUtils { @Deprecated(since = "3.7.0") public static void validate(EntityId id, String scope) { - Validator.validateId(id.getId(), "Incorrect id " + id); - Validator.validateString(scope, "Incorrect scope " + scope); + Validator.validateId(id.getId(), uuid -> "Incorrect id " + uuid); + Validator.validateString(scope, sc -> "Incorrect scope " + sc); } public static void validate(EntityId id, AttributeScope scope) { - Validator.validateId(id.getId(), "Incorrect id " + id); + Validator.validateId(id.getId(), uuid -> "Incorrect id " + uuid); Validator.checkNotNull(scope, "Incorrect scope " + scope); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index 6fb63613dc..5869ad9261 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java @@ -58,28 +58,28 @@ public class BaseAttributesService implements AttributesService { @Override public ListenableFuture> find(TenantId tenantId, EntityId entityId, String scope, String attributeKey) { validate(entityId, scope); - Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey); + Validator.validateString(attributeKey, k -> "Incorrect attribute key " + k); return Futures.immediateFuture(attributesDao.find(tenantId, entityId, AttributeScope.valueOf(scope), attributeKey)); } @Override public ListenableFuture> find(TenantId tenantId, EntityId entityId, AttributeScope scope, String attributeKey) { validate(entityId, scope); - Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey); + Validator.validateString(attributeKey, k -> "Incorrect attribute key " + k); return Futures.immediateFuture(attributesDao.find(tenantId, entityId, scope, attributeKey)); } @Override public ListenableFuture> find(TenantId tenantId, EntityId entityId, String scope, Collection attributeKeys) { validate(entityId, scope); - attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey)); + attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, k -> "Incorrect attribute key " + k)); return Futures.immediateFuture(attributesDao.find(tenantId, entityId, AttributeScope.valueOf(scope), attributeKeys)); } @Override public ListenableFuture> find(TenantId tenantId, EntityId entityId, AttributeScope scope, Collection attributeKeys) { validate(entityId, scope); - attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey)); + attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, k -> "Incorrect attribute key " + k)); return Futures.immediateFuture(attributesDao.find(tenantId, entityId, scope, attributeKeys)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index 1aafdf9583..dcd13fb168 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -117,7 +117,7 @@ public class CachedAttributesService implements AttributesService { @Override public ListenableFuture> find(TenantId tenantId, EntityId entityId, AttributeScope scope, String attributeKey) { validate(entityId, scope); - Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey); + Validator.validateString(attributeKey, k -> "Incorrect attribute key " + k); return cacheExecutor.submit(() -> { AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, attributeKey); @@ -152,7 +152,7 @@ public class CachedAttributesService implements AttributesService { public ListenableFuture> find(TenantId tenantId, EntityId entityId, AttributeScope scope, final Collection attributeKeysNonUnique) { validate(entityId, scope); final var attributeKeys = new LinkedHashSet<>(attributeKeysNonUnique); // deduplicate the attributes - attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey)); + attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, k ->"Incorrect attribute key " + k)); //CacheExecutor for Redis or DirectExecutor for local Caffeine return Futures.transformAsync(cacheExecutor.submit(() -> findCachedAttributes(entityId, scope, attributeKeys)), diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index ddaebef6e8..29b097bfab 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -25,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.StringUtils; @@ -85,31 +86,31 @@ public class AuditLogServiceImpl implements AuditLogService { @Override public PageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink) { log.trace("Executing findAuditLogsByTenantIdAndCustomerId [{}], [{}], [{}]", tenantId, customerId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, "Incorrect customerId " + customerId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> "Incorrect customerId " + id); return auditLogDao.findAuditLogsByTenantIdAndCustomerId(tenantId.getId(), customerId, actionTypes, pageLink); } @Override public PageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, List actionTypes, TimePageLink pageLink) { log.trace("Executing findAuditLogsByTenantIdAndUserId [{}], [{}], [{}]", tenantId, userId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(userId, "Incorrect userId" + userId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(userId, id -> "Incorrect userId" + id); return auditLogDao.findAuditLogsByTenantIdAndUserId(tenantId.getId(), userId, actionTypes, pageLink); } @Override public PageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink) { log.trace("Executing findAuditLogsByTenantIdAndEntityId [{}], [{}], [{}]", tenantId, entityId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateEntityId(entityId, INCORRECT_TENANT_ID + entityId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateEntityId(entityId, id -> "Incorrect entityId" + id); return auditLogDao.findAuditLogsByTenantIdAndEntityId(tenantId.getId(), entityId, actionTypes, pageLink); } @Override public PageData findAuditLogsByTenantId(TenantId tenantId, List actionTypes, TimePageLink pageLink) { log.trace("Executing findAuditLogs [{}]", pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); return auditLogDao.findAuditLogsByTenantId(tenantId.getId(), actionTypes, pageLink); } @@ -200,10 +201,10 @@ public class AuditLogServiceImpl implements AuditLogService { break; case ATTRIBUTES_UPDATED: actionData.put("entityId", entityId.toString()); - String scope = extractParameter(String.class, 0, additionalInfo); + AttributeScope scope = extractParameter(AttributeScope.class, 0, additionalInfo); @SuppressWarnings("unchecked") List attributes = extractParameter(List.class, 1, additionalInfo); - actionData.put("scope", scope); + actionData.put("scope", scope.name()); ObjectNode attrsNode = JacksonUtil.newObjectNode(); if (attributes != null) { for (AttributeKvEntry attr : attributes) { @@ -215,8 +216,8 @@ public class AuditLogServiceImpl implements AuditLogService { case ATTRIBUTES_DELETED: case ATTRIBUTES_READ: actionData.put("entityId", entityId.toString()); - scope = extractParameter(String.class, 0, additionalInfo); - actionData.put("scope", scope); + scope = extractParameter(AttributeScope.class, 0, additionalInfo); + actionData.put("scope", scope.name()); @SuppressWarnings("unchecked") List keys = extractParameter(List.class, 1, additionalInfo); ArrayNode attrsArrayNode = actionData.putArray("attributes"); diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index 633333dcbd..04308cbb2e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -84,21 +84,21 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom @Override public Customer findCustomerById(TenantId tenantId, CustomerId customerId) { log.trace("Executing findCustomerById [{}]", customerId); - Validator.validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + Validator.validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); return customerDao.findById(tenantId, customerId.getId()); } @Override public Optional findCustomerByTenantIdAndTitle(TenantId tenantId, String title) { log.trace("Executing findCustomerByTenantIdAndTitle [{}] [{}]", tenantId, title); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); return customerDao.findCustomersByTenantIdAndTitle(tenantId.getId(), title); } @Override public ListenableFuture findCustomerByIdAsync(TenantId tenantId, CustomerId customerId) { log.trace("Executing findCustomerByIdAsync [{}]", customerId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); return customerDao.findByIdAsync(tenantId, customerId.getId()); } @@ -126,7 +126,7 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom @Transactional public void deleteCustomer(TenantId tenantId, CustomerId customerId) { log.trace("Executing deleteCustomer [{}]", customerId); - Validator.validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + Validator.validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); Customer customer = findCustomerById(tenantId, customerId); if (customer == null) { throw new IncorrectParameterException("Unable to delete non-existent customer."); @@ -147,7 +147,7 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom @Override public Customer findOrCreatePublicCustomer(TenantId tenantId) { log.trace("Executing findOrCreatePublicCustomer, tenantId [{}]", tenantId); - Validator.validateId(tenantId, INCORRECT_CUSTOMER_ID + tenantId); + Validator.validateId(tenantId, id -> INCORRECT_CUSTOMER_ID + id); Optional publicCustomerOpt = customerDao.findCustomersByTenantIdAndTitle(tenantId.getId(), PUBLIC_CUSTOMER_TITLE); if (publicCustomerOpt.isPresent()) { return publicCustomerOpt.get(); @@ -169,7 +169,7 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom @Override public PageData findCustomersByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findCustomersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); - Validator.validateId(tenantId, "Incorrect tenantId " + tenantId); + Validator.validateId(tenantId, id -> "Incorrect tenantId " + id); Validator.validatePageLink(pageLink); return customerDao.findCustomersByTenantId(tenantId.getId(), pageLink); } @@ -177,7 +177,7 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom @Override public void deleteCustomersByTenantId(TenantId tenantId) { log.trace("Executing deleteCustomersByTenantId, tenantId [{}]", tenantId); - Validator.validateId(tenantId, "Incorrect tenantId " + tenantId); + Validator.validateId(tenantId, id -> "Incorrect tenantId " + id); customersByTenantRemover.removeEntities(tenantId, tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 5d2e6658fa..7f90d82af7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -110,21 +110,21 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Override public Dashboard findDashboardById(TenantId tenantId, DashboardId dashboardId) { log.trace("Executing findDashboardById [{}]", dashboardId); - Validator.validateId(dashboardId, INCORRECT_DASHBOARD_ID + dashboardId); + Validator.validateId(dashboardId, id -> INCORRECT_DASHBOARD_ID + id); return dashboardDao.findById(tenantId, dashboardId.getId()); } @Override public ListenableFuture findDashboardByIdAsync(TenantId tenantId, DashboardId dashboardId) { log.trace("Executing findDashboardByIdAsync [{}]", dashboardId); - validateId(dashboardId, INCORRECT_DASHBOARD_ID + dashboardId); + validateId(dashboardId, id -> INCORRECT_DASHBOARD_ID + id); return dashboardDao.findByIdAsync(tenantId, dashboardId.getId()); } @Override public DashboardInfo findDashboardInfoById(TenantId tenantId, DashboardId dashboardId) { log.trace("Executing findDashboardInfoById [{}]", dashboardId); - Validator.validateId(dashboardId, INCORRECT_DASHBOARD_ID + dashboardId); + Validator.validateId(dashboardId, id -> INCORRECT_DASHBOARD_ID + id); return dashboardInfoDao.findById(tenantId, dashboardId.getId()); } @@ -137,7 +137,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Override public ListenableFuture findDashboardInfoByIdAsync(TenantId tenantId, DashboardId dashboardId) { log.trace("Executing findDashboardInfoByIdAsync [{}]", dashboardId); - validateId(dashboardId, INCORRECT_DASHBOARD_ID + dashboardId); + validateId(dashboardId, id -> INCORRECT_DASHBOARD_ID + id); return dashboardInfoDao.findByIdAsync(tenantId, dashboardId.getId()); } @@ -229,7 +229,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Transactional public void deleteDashboard(TenantId tenantId, DashboardId dashboardId) { log.trace("Executing deleteDashboard [{}]", dashboardId); - Validator.validateId(dashboardId, INCORRECT_DASHBOARD_ID + dashboardId); + Validator.validateId(dashboardId, id -> INCORRECT_DASHBOARD_ID + id); deleteEntityRelations(tenantId, dashboardId); try { dashboardDao.removeById(tenantId, dashboardId.getId()); @@ -249,7 +249,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Override public PageData findDashboardsByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findDashboardsByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); Validator.validatePageLink(pageLink); return dashboardInfoDao.findDashboardsByTenantId(tenantId.getId(), pageLink); } @@ -257,7 +257,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Override public PageData findMobileDashboardsByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findMobileDashboardsByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); Validator.validatePageLink(pageLink); return dashboardInfoDao.findMobileDashboardsByTenantId(tenantId.getId(), pageLink); } @@ -265,15 +265,15 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Override public void deleteDashboardsByTenantId(TenantId tenantId) { log.trace("Executing deleteDashboardsByTenantId, tenantId [{}]", tenantId); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); tenantDashboardsRemover.removeEntities(tenantId, tenantId); } @Override public PageData findDashboardsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findDashboardsByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validateId(customerId, "Incorrect customerId " + customerId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + Validator.validateId(customerId, id -> "Incorrect customerId " + id); Validator.validatePageLink(pageLink); return dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); } @@ -281,8 +281,8 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Override public PageData findMobileDashboardsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findMobileDashboardsByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validateId(customerId, "Incorrect customerId " + customerId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + Validator.validateId(customerId, id -> "Incorrect customerId " + id); Validator.validatePageLink(pageLink); return dashboardInfoDao.findMobileDashboardsByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); } @@ -290,7 +290,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Override public void unassignCustomerDashboards(TenantId tenantId, CustomerId customerId) { log.trace("Executing unassignCustomerDashboards, customerId [{}]", customerId); - Validator.validateId(customerId, "Incorrect customerId " + customerId); + Validator.validateId(customerId, id -> "Incorrect customerId " + id); Customer customer = customerDao.findById(tenantId, customerId.getId()); if (customer == null) { throw new DataValidationException("Can't unassign dashboards from non-existent customer!"); @@ -301,7 +301,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Override public void updateCustomerDashboards(TenantId tenantId, CustomerId customerId) { log.trace("Executing updateCustomerDashboards, customerId [{}]", customerId); - Validator.validateId(customerId, "Incorrect customerId " + customerId); + Validator.validateId(customerId, id -> "Incorrect customerId " + id); Customer customer = customerDao.findById(tenantId, customerId.getId()); if (customer == null) { throw new DataValidationException("Can't update dashboards for non-existent customer!"); @@ -351,8 +351,8 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Override public PageData findDashboardsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink) { log.trace("Executing findDashboardsByTenantIdAndEdgeId, tenantId [{}], edgeId [{}], pageLink [{}]", tenantId, edgeId, pageLink); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + Validator.validateId(edgeId, id -> INCORRECT_EDGE_ID + id); Validator.validatePageLink(pageLink); return dashboardInfoDao.findDashboardsByTenantIdAndEdgeId(tenantId.getId(), edgeId.getId(), pageLink); } 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 4367163ded..67af118d0b 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 @@ -88,7 +88,7 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService public JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException { DeviceId deviceId = device.getId(); log.trace("Executing findDevicePublishTelemetryCommands [{}]", deviceId); - validateId(deviceId, INCORRECT_DEVICE_ID + deviceId); + validateId(deviceId, id -> INCORRECT_DEVICE_ID + id); DeviceCredentials creds = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), deviceId); DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(device.getTenantId(), device.getDeviceProfileId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index 60167f04f2..6025e7c820 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -72,14 +72,14 @@ public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService "Incorrect deviceId " + id); return deviceCredentialsDao.findByDeviceId(tenantId, deviceId.getId()); } @Override public DeviceCredentials findDeviceCredentialsByCredentialsId(String credentialsId) { log.trace("Executing findDeviceCredentialsByCredentialsId [{}]", credentialsId); - validateString(credentialsId, "Incorrect credentialsId " + credentialsId); + validateString(credentialsId, id -> "Incorrect credentialsId " + id); return cache.getAndPutInTransaction(credentialsId, () -> deviceCredentialsDao.findByCredentialsId(TenantId.SYS_TENANT_ID, credentialsId), true); // caching null values is essential for permanently invalid requests diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 608da9d28d..7e6d79d9a7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -126,7 +126,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService INCORRECT_DEVICE_PROFILE_ID + id); return cache.getOrFetchFromDB(DeviceProfileCacheKey.fromId(deviceProfileId), () -> deviceProfileDao.findById(tenantId, deviceProfileId.getId()), true, putInCache); } @@ -139,7 +139,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService INCORRECT_DEVICE_PROFILE_NAME + pn); return cache.getOrFetchFromDB(DeviceProfileCacheKey.fromName(tenantId, profileName), () -> deviceProfileDao.findByName(tenantId, profileName), true, putInCache); } @@ -147,7 +147,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService INCORRECT_PROVISION_DEVICE_KEY + dk); return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromProvisionDeviceKey(provisionDeviceKey), () -> deviceProfileDao.findByProvisionDeviceKey(provisionDeviceKey), false); } @@ -155,7 +155,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService INCORRECT_DEVICE_PROFILE_ID + id); return toDeviceProfileInfo(findDeviceProfileById(tenantId, deviceProfileId)); } @@ -222,7 +222,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService INCORRECT_DEVICE_PROFILE_ID + id); DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, deviceProfileId.getId()); if (deviceProfile != null && deviceProfile.isDefault()) { throw new DataValidationException("Deletion of Default Device Profile is prohibited!"); @@ -252,7 +252,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService findDeviceProfiles(TenantId tenantId, PageLink pageLink) { log.trace("Executing findDeviceProfiles tenantId [{}], pageLink [{}]", tenantId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); Validator.validatePageLink(pageLink); return deviceProfileDao.findDeviceProfiles(tenantId, pageLink); } @@ -260,7 +260,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType) { log.trace("Executing findDeviceProfileInfos tenantId [{}], pageLink [{}]", tenantId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); Validator.validatePageLink(pageLink); return deviceProfileDao.findDeviceProfileInfos(tenantId, pageLink, transportType); } @@ -290,7 +290,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); DeviceProfile deviceProfile = new DeviceProfile(); deviceProfile.setTenantId(tenantId); deviceProfile.setDefault(defaultProfile); @@ -313,7 +313,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); return cache.getAndPutInTransaction(DeviceProfileCacheKey.defaultProfile(tenantId), () -> deviceProfileDao.findDefaultDeviceProfile(tenantId), true); } @@ -321,14 +321,14 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); return toDeviceProfileInfo(findDefaultDeviceProfile(tenantId)); } @Override public boolean setDefaultDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId) { log.trace("Executing setDefaultDeviceProfile [{}]", deviceProfileId); - validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId); + validateId(deviceProfileId, id -> INCORRECT_DEVICE_PROFILE_ID + id); DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, deviceProfileId.getId()); if (!deviceProfile.isDefault()) { deviceProfile.setDefault(true); @@ -354,7 +354,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); tenantDeviceProfilesRemover.removeEntities(tenantId, tenantId); } @@ -377,7 +377,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService findDeviceProfileNamesByTenantId(TenantId tenantId, boolean activeOnly) { log.trace("Executing findDeviceProfileNamesByTenantId, tenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); return deviceProfileDao.findTenantDeviceProfileNames(tenantId.getId(), activeOnly) .stream().sorted(Comparator.comparing(EntityInfo::getName)) .collect(Collectors.toList()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index da25bf91d5..ff00db3564 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -126,14 +126,14 @@ public class DeviceServiceImpl extends AbstractCachedEntityService INCORRECT_DEVICE_ID + id); return deviceDao.findDeviceInfoById(tenantId, deviceId.getId()); } @Override public Device findDeviceById(TenantId tenantId, DeviceId deviceId) { log.trace("Executing findDeviceById [{}]", deviceId); - validateId(deviceId, INCORRECT_DEVICE_ID + deviceId); + validateId(deviceId, id -> INCORRECT_DEVICE_ID + id); if (TenantId.SYS_TENANT_ID.equals(tenantId)) { return cache.getAndPutInTransaction(new DeviceCacheKey(deviceId), () -> deviceDao.findById(tenantId, deviceId.getId()), true); @@ -146,7 +146,7 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDeviceByIdAsync(TenantId tenantId, DeviceId deviceId) { log.trace("Executing findDeviceById [{}]", deviceId); - validateId(deviceId, INCORRECT_DEVICE_ID + deviceId); + validateId(deviceId, id -> INCORRECT_DEVICE_ID + id); if (TenantId.SYS_TENANT_ID.equals(tenantId)) { return deviceDao.findByIdAsync(tenantId, deviceId.getId()); } else { @@ -157,7 +157,7 @@ public class DeviceServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); return cache.getAndPutInTransaction(new DeviceCacheKey(tenantId, name), () -> deviceDao.findDeviceByTenantIdAndName(tenantId.getId(), name).orElse(null), true); } @@ -321,7 +321,7 @@ public class DeviceServiceImpl extends AbstractCachedEntityService INCORRECT_DEVICE_ID + id); if (entityViewService.existsByTenantIdAndEntityId(tenantId, deviceId)) { throw new DataValidationException("Can't delete device that has entity views!"); } @@ -347,7 +347,7 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDevicesByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findDevicesByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id ->INCORRECT_TENANT_ID + id); validatePageLink(pageLink); return deviceDao.findDevicesByTenantId(tenantId.getId(), pageLink); } @@ -359,7 +359,7 @@ public class DeviceServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); validatePageLink(pageLink); return deviceDao.findDeviceInfosByFilter(filter, pageLink); @@ -375,8 +375,8 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDevicesByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) { log.trace("Executing findDevicesByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateString(type, "Incorrect type " + type); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateString(type, t -> "Incorrect type " + t); validatePageLink(pageLink); return deviceDao.findDevicesByTenantIdAndType(tenantId.getId(), type, pageLink); } @@ -388,8 +388,8 @@ public class DeviceServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); + validateId(deviceProfileId, id -> INCORRECT_DEVICE_PROFILE_ID + id); validatePageLink(pageLink); return deviceDao.findDevicesByTenantIdAndTypeAndEmptyOtaPackage(tenantId.getId(), deviceProfileId.getId(), type, pageLink); } @@ -397,30 +397,30 @@ public class DeviceServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); + validateId(deviceProfileId, id -> INCORRECT_DEVICE_PROFILE_ID + id); return deviceDao.countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(tenantId.getId(), deviceProfileId.getId(), type); } @Override public ListenableFuture> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List deviceIds) { log.trace("Executing findDevicesByTenantIdAndIdsAsync, tenantId [{}], deviceIds [{}]", tenantId, deviceIds); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateIds(deviceIds, "Incorrect deviceIds " + deviceIds); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateIds(deviceIds, ids -> "Incorrect deviceIds " + ids); return deviceDao.findDevicesByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(deviceIds)); } @Override public List findDevicesByIds(List deviceIds) { log.trace("Executing findDevicesByIdsAsync, deviceIds [{}]", deviceIds); - validateIds(deviceIds, "Incorrect deviceIds " + deviceIds); + validateIds(deviceIds, ids-> "Incorrect deviceIds " + ids); return deviceDao.findDevicesByIds(toUUIDs(deviceIds)); } @Override public ListenableFuture> findDevicesByIdsAsync(List deviceIds) { log.trace("Executing findDevicesByIdsAsync, deviceIds [{}]", deviceIds); - validateIds(deviceIds, "Incorrect deviceIds " + deviceIds); + validateIds(deviceIds, ids-> "Incorrect deviceIds " + ids); return deviceDao.findDevicesByIdsAsync(toUUIDs(deviceIds)); } @@ -428,15 +428,15 @@ public class DeviceServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); tenantDevicesRemover.removeEntities(tenantId, tenantId); } @Override public PageData findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findDevicesByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validatePageLink(pageLink); return deviceDao.findDevicesByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); } @@ -444,9 +444,9 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { log.trace("Executing findDevicesByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); - validateString(type, "Incorrect type " + type); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); + validateString(type, t -> "Incorrect type " + t); validatePageLink(pageLink); return deviceDao.findDevicesByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); } @@ -454,9 +454,9 @@ public class DeviceServiceImpl extends AbstractCachedEntityService> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List deviceIds) { log.trace("Executing findDevicesByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], deviceIds [{}]", tenantId, customerId, deviceIds); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); - validateIds(deviceIds, "Incorrect deviceIds " + deviceIds); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); + validateIds(deviceIds, ids -> "Incorrect deviceIds " + ids); return deviceDao.findDevicesByTenantIdCustomerIdAndIdsAsync(tenantId.getId(), customerId.getId(), toUUIDs(deviceIds)); } @@ -464,8 +464,8 @@ public class DeviceServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); customerDevicesRemover.removeEntities(tenantId, customerId); } @@ -491,7 +491,7 @@ public class DeviceServiceImpl extends AbstractCachedEntityService> findDeviceTypesByTenantId(TenantId tenantId) { log.trace("Executing findDeviceTypesByTenantId, tenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); return deviceDao.findTenantDeviceTypesAsync(tenantId.getId()); } @@ -627,8 +627,8 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDevicesByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink) { log.trace("Executing findDevicesByTenantIdAndEdgeId, tenantId [{}], edgeId [{}], pageLink [{}]", tenantId, edgeId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(edgeId, id -> INCORRECT_EDGE_ID + id); validatePageLink(pageLink); return deviceDao.findDevicesByTenantIdAndEdgeId(tenantId.getId(), edgeId.getId(), pageLink); } @@ -636,9 +636,9 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDevicesByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink) { log.trace("Executing findDevicesByTenantIdAndEdgeIdAndType, tenantId [{}], edgeId [{}], type [{}] pageLink [{}]", tenantId, edgeId, type, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(edgeId, INCORRECT_EDGE_ID + edgeId); - validateString(type, "Incorrect type " + type); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(edgeId, id -> INCORRECT_EDGE_ID + id); + validateString(type, t -> "Incorrect type " + t); validatePageLink(pageLink); return deviceDao.findDevicesByTenantIdAndEdgeIdAndType(tenantId.getId(), edgeId.getId(), type, pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 2db4d2ba92..94aa3380ee 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -21,6 +21,7 @@ import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import jakarta.annotation.Nullable; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; @@ -31,7 +32,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; @@ -71,7 +72,6 @@ import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; -import jakarta.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -138,28 +138,28 @@ public class EdgeServiceImpl extends AbstractCachedEntityService INCORRECT_EDGE_ID + id); return edgeDao.findById(tenantId, edgeId.getId()); } @Override public EdgeInfo findEdgeInfoById(TenantId tenantId, EdgeId edgeId) { log.trace("Executing findEdgeInfoById [{}]", edgeId); - validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + validateId(edgeId, id -> INCORRECT_EDGE_ID + id); return edgeDao.findEdgeInfoById(tenantId, edgeId.getId()); } @Override public ListenableFuture findEdgeByIdAsync(TenantId tenantId, EdgeId edgeId) { log.trace("Executing findEdgeById [{}]", edgeId); - validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + validateId(edgeId, id -> INCORRECT_EDGE_ID + id); return edgeDao.findByIdAsync(tenantId, edgeId.getId()); } @Override public Edge findEdgeByTenantIdAndName(TenantId tenantId, String name) { log.trace("Executing findEdgeByTenantIdAndName [{}][{}]", tenantId, name); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); return cache.getAndPutInTransaction(new EdgeCacheKey(tenantId, name), () -> edgeDao.findEdgeByTenantIdAndName(tenantId.getId(), name) .orElse(null), true); @@ -221,7 +221,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService INCORRECT_EDGE_ID + id); Edge edge = edgeDao.findById(tenantId, edgeId.getId()); @@ -236,7 +236,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService findEdgesByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findEdgesByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validatePageLink(pageLink); return edgeDao.findEdgesByTenantId(tenantId.getId(), pageLink); } @@ -244,8 +244,8 @@ public class EdgeServiceImpl extends AbstractCachedEntityService findEdgesByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) { log.trace("Executing findEdgesByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateString(type, "Incorrect type " + type); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateString(type, t -> "Incorrect type " + t); validatePageLink(pageLink); return edgeDao.findEdgesByTenantIdAndType(tenantId.getId(), type, pageLink); } @@ -253,8 +253,8 @@ public class EdgeServiceImpl extends AbstractCachedEntityService findEdgeInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) { log.trace("Executing findEdgeInfosByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateString(type, "Incorrect type " + type); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateString(type, t -> "Incorrect type " + t); validatePageLink(pageLink); return edgeDao.findEdgeInfosByTenantIdAndType(tenantId.getId(), type, pageLink); } @@ -262,7 +262,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService findEdgeInfosByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findEdgeInfosByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validatePageLink(pageLink); return edgeDao.findEdgeInfosByTenantId(tenantId.getId(), pageLink); } @@ -270,23 +270,23 @@ public class EdgeServiceImpl extends AbstractCachedEntityService> findEdgesByTenantIdAndIdsAsync(TenantId tenantId, List edgeIds) { log.trace("Executing findEdgesByTenantIdAndIdsAsync, tenantId [{}], edgeIds [{}]", tenantId, edgeIds); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateIds(edgeIds, "Incorrect edgeIds " + edgeIds); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateIds(edgeIds, ids -> "Incorrect edgeIds " + ids); return edgeDao.findEdgesByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(edgeIds)); } @Override public void deleteEdgesByTenantId(TenantId tenantId) { log.trace("Executing deleteEdgesByTenantId, tenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); tenantEdgesRemover.removeEntities(tenantId, tenantId); } @Override public PageData findEdgesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findEdgesByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validatePageLink(pageLink); return edgeDao.findEdgesByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); } @@ -294,9 +294,9 @@ public class EdgeServiceImpl extends AbstractCachedEntityService findEdgesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { log.trace("Executing findEdgesByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); - validateString(type, "Incorrect type " + type); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); + validateString(type, t -> "Incorrect type " + t); validatePageLink(pageLink); return edgeDao.findEdgesByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); } @@ -304,8 +304,8 @@ public class EdgeServiceImpl extends AbstractCachedEntityService findEdgeInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findEdgeInfosByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validatePageLink(pageLink); return edgeDao.findEdgeInfosByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); } @@ -313,9 +313,9 @@ public class EdgeServiceImpl extends AbstractCachedEntityService findEdgeInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { log.trace("Executing findEdgeInfosByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); - validateString(type, "Incorrect type " + type); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); + validateString(type, t -> "Incorrect type " + t); validatePageLink(pageLink); return edgeDao.findEdgeInfosByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); } @@ -323,9 +323,9 @@ public class EdgeServiceImpl extends AbstractCachedEntityService> findEdgesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List edgeIds) { log.trace("Executing findEdgesByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], edgeIds [{}]", tenantId, customerId, edgeIds); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); - validateIds(edgeIds, "Incorrect edgeIds " + edgeIds); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); + validateIds(edgeIds, ids -> "Incorrect edgeIds " + ids); return edgeDao.findEdgesByTenantIdCustomerIdAndIdsAsync(tenantId.getId(), customerId.getId(), toUUIDs(edgeIds)); } @@ -333,8 +333,8 @@ public class EdgeServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); customerEdgeUnassigner.removeEntities(tenantId, customerId); } @@ -354,7 +354,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService, List>() { + edges = Futures.transform(edges, new Function<>() { @Nullable @Override public List apply(@Nullable List edgeList) { @@ -370,7 +370,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService> findEdgeTypesByTenantId(TenantId tenantId) { log.trace("Executing findEdgeTypesByTenantId, tenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); ListenableFuture> tenantEdgeTypes = edgeDao.findTenantEdgeTypesAsync(tenantId.getId()); return Futures.transform(tenantEdgeTypes, edgeTypes -> { @@ -400,7 +400,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService findEdgesByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink) { log.trace("Executing findEdgesByTenantIdAndEntityId, tenantId [{}], entityId [{}], pageLink [{}]", tenantId, entityId, pageLink); - Validator.validateId(tenantId, "Incorrect tenantId " + tenantId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validatePageLink(pageLink); return edgeDao.findEdgesByTenantIdAndEntityId(tenantId.getId(), entityId.getId(), entityId.getEntityType(), pageLink); } @@ -408,13 +408,13 @@ public class EdgeServiceImpl extends AbstractCachedEntityService findEdgesByTenantProfileId(TenantProfileId tenantProfileId, PageLink pageLink) { log.trace("Executing findEdgesByTenantProfileId, tenantProfileId [{}], pageLink [{}]", tenantProfileId, pageLink); - Validator.validateId(tenantProfileId, "Incorrect tenantProfileId " + tenantProfileId); + Validator.validateId(tenantProfileId, id -> "Incorrect tenantProfileId " + id); validatePageLink(pageLink); return edgeDao.findEdgesByTenantProfileId(tenantProfileId.getId(), pageLink); } private PaginatedRemover tenantEdgesRemover = - new PaginatedRemover() { + new PaginatedRemover<>() { @Override protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { @@ -427,7 +427,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService customerEdgeUnassigner = new PaginatedRemover() { + private PaginatedRemover customerEdgeUnassigner = new PaginatedRemover<>() { @Override protected PageData findEntities(TenantId tenantId, CustomerId id, PageLink pageLink) { @@ -513,7 +513,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService edgeRuleChains = findEdgeRuleChains(tenantId, edgeId); - List edgeRuleChainIds = edgeRuleChains.stream().map(IdBased::getId).collect(Collectors.toList()); + List edgeRuleChainIds = edgeRuleChains.stream().map(IdBased::getId).toList(); ObjectNode result = JacksonUtil.newObjectNode(); for (RuleChain edgeRuleChain : edgeRuleChains) { List ruleNodes = @@ -522,8 +522,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService connectedRuleChains = ruleNodes.stream() .filter(rn -> rn.getType().equals(tbRuleChainInputNodeClassName)) - .map(rn -> new RuleChainId(UUID.fromString(rn.getConfiguration().get("ruleChainId").asText()))) - .collect(Collectors.toList()); + .map(rn -> new RuleChainId(UUID.fromString(rn.getConfiguration().get("ruleChainId").asText()))).toList(); List missingRuleChains = new ArrayList<>(); for (RuleChainId connectedRuleChain : connectedRuleChains) { if (!edgeRuleChainIds.contains(connectedRuleChain)) { @@ -549,7 +548,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService Futures.immediateFuture(kvEntryOpt.flatMap(KvEntry::getBooleanValue).orElse(false)), MoreExecutors.directExecutor()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index b91e2ed96d..3cdcf5e874 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasCustomerId; import org.thingsboard.server.common.data.HasEmail; import org.thingsboard.server.common.data.HasLabel; @@ -34,13 +35,23 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.EntityCountQuery; 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.EntityFilterType; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityListFilter; +import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.RelationsQueryFilter; import org.thingsboard.server.dao.exception.IncorrectParameterException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.function.Function; +import java.util.stream.Collectors; import static org.thingsboard.server.common.data.id.EntityId.NULL_UUID; import static org.thingsboard.server.dao.service.Validator.validateEntityDataPageLink; @@ -57,6 +68,10 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; public static final CustomerId NULL_CUSTOMER_ID = new CustomerId(NULL_UUID); + private static final int MAX_ENTITY_IDS_SIZE = 1024; + private static final Set EXCLUDED_TYPES_FROM_OPTIMIZATION = Set.of( + EntityFilterType.ENTITY_LIST, EntityFilterType.SINGLE_ENTITY, EntityFilterType.RELATIONS_QUERY); + @Autowired private EntityQueryDao entityQueryDao; @@ -67,8 +82,8 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe @Override public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) { log.trace("Executing countEntitiesByQuery, tenantId [{}], customerId [{}], query [{}]", tenantId, customerId, query); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validateEntityCountQuery(query); return this.entityQueryDao.countEntitiesByQuery(tenantId, customerId, query); } @@ -76,10 +91,23 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe @Override public PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query) { log.trace("Executing findEntityDataByQuery, tenantId [{}], customerId [{}], query [{}]", tenantId, customerId, query); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validateEntityDataQuery(query); - return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, query); + + if (!isValidForOptimization(query)) { + return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, query); + } + + // 1 step - find entity data by filter and sort columns + PageData entityDataByQuery = findEntityIdsByFilterAndSorterColumns(tenantId, customerId, query); + if (entityDataByQuery == null || entityDataByQuery.getData().isEmpty()) { + return entityDataByQuery; + } + + // 2 step - find entity data by entity ids from the 1st step + List result = fetchEntityDataByIdsFromInitialQuery(tenantId, customerId, query, entityDataByQuery.getData()); + return new PageData<>(result, entityDataByQuery.getTotalPages(), entityDataByQuery.getTotalElements(), entityDataByQuery.hasNext()); } @Override @@ -133,8 +161,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe } private CustomerId getCustomerId(HasId entity) { - if (entity instanceof HasCustomerId) { - HasCustomerId hasCustomerId = (HasCustomerId) entity; + if (entity instanceof HasCustomerId hasCustomerId) { CustomerId customerId = hasCustomerId.getCustomerId(); if (customerId == null) { customerId = NULL_CUSTOMER_ID; @@ -176,4 +203,76 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe throw new IncorrectParameterException("Relation query filter root entity should not be blank"); } } + + private boolean isValidForOptimization(EntityDataQuery query) { + if (StringUtils.isNotEmpty(query.getPageLink().getTextSearch())) { + return false; + } + + if (EXCLUDED_TYPES_FROM_OPTIMIZATION.contains(query.getEntityFilter().getType())) { + return false; + } + + if ((query.getEntityFields() == null || query.getEntityFields().isEmpty()) && + (query.getLatestValues() == null || query.getLatestValues().isEmpty())) { + return false; + } + + Set filteringKeys = new HashSet<>(Optional.ofNullable(query.getKeyFilters()).orElse(Collections.emptyList()).stream().map(KeyFilter::getKey).toList()); + Set entityFields = new HashSet<>(Optional.ofNullable(query.getEntityFields()).orElse(Collections.emptyList())); + Set latestValues = new HashSet<>(Optional.ofNullable(query.getLatestValues()).orElse(Collections.emptyList())); + + return !(filteringKeys.containsAll(entityFields) && filteringKeys.containsAll(latestValues)); + } + + private PageData findEntityIdsByFilterAndSorterColumns(TenantId tenantId, CustomerId customerId, EntityDataQuery query) { + List entityFields = null; + List latestValues = null; + if (query.getPageLink().getSortOrder() != null) { + if (query.getEntityFields() != null) { + entityFields = query.getEntityFields().stream() + .filter(entityKey -> entityKey.getKey().equals(query.getPageLink().getSortOrder().getKey().getKey())) + .collect(Collectors.toList()); + } + if (query.getLatestValues() != null) { + latestValues = query.getLatestValues().stream() + .filter(entityKey -> entityKey.getKey().equals(query.getPageLink().getSortOrder().getKey().getKey())) + .collect(Collectors.toList()); + } + } + EntityDataQuery entityQuery = new EntityDataQuery(query.getEntityFilter(), query.getPageLink(), entityFields, latestValues, query.getKeyFilters()); + return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, entityQuery); + } + + private List fetchEntityDataByIdsFromInitialQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query, List initialQueryResult) { + List result = new ArrayList<>(); + + List entityIds = initialQueryResult.stream().map(d -> d.getEntityId().getId().toString()).collect(Collectors.toList()); + EntityType entityType = initialQueryResult.get(0).getEntityId().getEntityType(); + + if (entityIds.size() > MAX_ENTITY_IDS_SIZE) { + List> chunks = new ArrayList<>(); + for (int i = 0; i < entityIds.size(); i += MAX_ENTITY_IDS_SIZE) { + chunks.add(entityIds.subList(i, Math.min(entityIds.size(), i + MAX_ENTITY_IDS_SIZE))); + } + for (List chunk : chunks) { + result.addAll(findEntityDataByEntityIds(tenantId, customerId, query, chunk, entityType, chunk.size())); + } + } else { + result.addAll(findEntityDataByEntityIds(tenantId, customerId, query, entityIds, entityType, query.getPageLink().getPageSize())); + } + return result; + } + + private List findEntityDataByEntityIds(TenantId tenantId, CustomerId customerId, EntityDataQuery query, + List entityIds, EntityType entityType, int pageSize) { + EntityListFilter filter = new EntityListFilter(); + filter.setEntityType(entityType); + filter.setEntityList(entityIds); + + EntityDataPageLink pageLink = new EntityDataPageLink(pageSize, 0, null, query.getPageLink().getSortOrder()); + EntityDataQuery entityQuery = new EntityDataQuery(filter, pageLink, query.getEntityFields(), query.getLatestValues(), null); + return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, entityQuery).getData(); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java index 5804650ec6..9d3c33dcc2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java @@ -145,15 +145,15 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); customerEntityViewsUnAssigner.removeEntities(tenantId, customerId); } @Override public EntityViewInfo findEntityViewInfoById(TenantId tenantId, EntityViewId entityViewId) { log.trace("Executing findEntityViewInfoById [{}]", entityViewId); - validateId(entityViewId, INCORRECT_ENTITY_VIEW_ID + entityViewId); + validateId(entityViewId, id -> INCORRECT_ENTITY_VIEW_ID + id); return entityViewDao.findEntityViewInfoById(tenantId, entityViewId.getId()); } @@ -165,7 +165,7 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService INCORRECT_ENTITY_VIEW_ID + id); return cache.getOrFetchFromDB(EntityViewCacheKey.byId(entityViewId), () -> entityViewDao.findById(tenantId, entityViewId.getId()) , EntityViewCacheValue::getEntityView, v -> new EntityViewCacheValue(v, null), true, putInCache); @@ -174,7 +174,7 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); return cache.getAndPutInTransaction(EntityViewCacheKey.byName(tenantId, name), () -> entityViewDao.findEntityViewByTenantIdAndName(tenantId.getId(), name).orElse(null) , EntityViewCacheValue::getEntityView, v -> new EntityViewCacheValue(v, null), true); @@ -184,7 +184,7 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService findEntityViewByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findEntityViewsByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validatePageLink(pageLink); return entityViewDao.findEntityViewsByTenantId(tenantId.getId(), pageLink); } @@ -192,7 +192,7 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService findEntityViewInfosByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findEntityViewInfosByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validatePageLink(pageLink); return entityViewDao.findEntityViewInfosByTenantId(tenantId.getId(), pageLink); } @@ -200,18 +200,18 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService findEntityViewByTenantIdAndType(TenantId tenantId, PageLink pageLink, String type) { log.trace("Executing findEntityViewByTenantIdAndType, tenantId [{}], pageLink [{}], type [{}]", tenantId, pageLink, type); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validatePageLink(pageLink); - validateString(type, "Incorrect type " + type); + validateString(type, t -> "Incorrect type " + t); return entityViewDao.findEntityViewsByTenantIdAndType(tenantId.getId(), type, pageLink); } @Override public PageData findEntityViewInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) { log.trace("Executing findEntityViewInfosByTenantIdAndType, tenantId [{}], pageLink [{}], type [{}]", tenantId, pageLink, type); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validatePageLink(pageLink); - validateString(type, "Incorrect type " + type); + validateString(type, t -> "Incorrect type " + t); return entityViewDao.findEntityViewInfosByTenantIdAndType(tenantId.getId(), type, pageLink); } @@ -220,8 +220,8 @@ public class EntityViewServiceImpl extends AbstractCachedEntityServiceINCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validatePageLink(pageLink); return entityViewDao.findEntityViewsByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); @@ -231,8 +231,8 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService findEntityViewInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findEntityViewInfosByTenantIdAndCustomerId, tenantId [{}], customerId [{}]," + " pageLink [{}]", tenantId, customerId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validatePageLink(pageLink); return entityViewDao.findEntityViewInfosByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); @@ -242,10 +242,10 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService findEntityViewsByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, PageLink pageLink, String type) { log.trace("Executing findEntityViewsByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}]," + " pageLink [{}], type [{}]", tenantId, customerId, pageLink, type); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validatePageLink(pageLink); - validateString(type, "Incorrect type " + type); + validateString(type, t -> "Incorrect type " + t); return entityViewDao.findEntityViewsByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); } @@ -254,10 +254,10 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService findEntityViewInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { log.trace("Executing findEntityViewInfosByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}]," + " pageLink [{}], type [{}]", tenantId, customerId, pageLink, type); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validatePageLink(pageLink); - validateString(type, "Incorrect type " + type); + validateString(type, t -> "Incorrect type " + t); return entityViewDao.findEntityViewInfosByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); } @@ -291,15 +291,15 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService findEntityViewByIdAsync(TenantId tenantId, EntityViewId entityViewId) { log.trace("Executing findEntityViewById [{}]", entityViewId); - validateId(entityViewId, INCORRECT_ENTITY_VIEW_ID + entityViewId); + validateId(entityViewId, id -> INCORRECT_ENTITY_VIEW_ID + id); return entityViewDao.findByIdAsync(tenantId, entityViewId.getId()); } @Override public ListenableFuture> findEntityViewsByTenantIdAndEntityIdAsync(TenantId tenantId, EntityId entityId) { log.trace("Executing findEntityViewsByTenantIdAndEntityIdAsync, tenantId [{}], entityId [{}]", tenantId, entityId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(entityId.getId(), "Incorrect entityId" + entityId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(entityId.getId(), id -> "Incorrect entityId" + id); return service.submit(() -> cache.getAndPutInTransaction(EntityViewCacheKey.byEntityId(tenantId, entityId), () -> entityViewDao.findEntityViewsByTenantIdAndEntityId(tenantId.getId(), entityId.getId()), @@ -309,8 +309,8 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService findEntityViewsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId) { log.trace("Executing findEntityViewsByTenantIdAndEntityId, tenantId [{}], entityId [{}]", tenantId, entityId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(entityId.getId(), "Incorrect entityId" + entityId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(entityId.getId(), id -> "Incorrect entityId" + id); return cache.getAndPutInTransaction(EntityViewCacheKey.byEntityId(tenantId, entityId), () -> entityViewDao.findEntityViewsByTenantIdAndEntityId(tenantId.getId(), entityId.getId()), @@ -326,7 +326,7 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService INCORRECT_ENTITY_VIEW_ID + id); deleteEntityRelations(tenantId, entityViewId); EntityView entityView = entityViewDao.findById(tenantId, entityViewId.getId()); entityViewDao.removeById(tenantId, entityViewId.getId()); @@ -337,14 +337,14 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); tenantEntityViewRemover.removeEntities(tenantId, tenantId); } @Override public ListenableFuture> findEntityViewTypesByTenantId(TenantId tenantId) { log.trace("Executing findEntityViewTypesByTenantId, tenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); ListenableFuture> tenantEntityViewTypes = entityViewDao.findTenantEntityViewTypesAsync(tenantId.getId()); return Futures.transform(tenantEntityViewTypes, entityViewTypes -> { @@ -402,8 +402,8 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService findEntityViewsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink) { log.trace("Executing findEntityViewsByTenantIdAndEdgeId, tenantId [{}], edgeId [{}], pageLink [{}]", tenantId, edgeId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(edgeId, id -> INCORRECT_EDGE_ID + id); validatePageLink(pageLink); return entityViewDao.findEntityViewsByTenantIdAndEdgeId(tenantId.getId(), edgeId.getId(), pageLink); } @@ -411,9 +411,9 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService findEntityViewsByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink) { log.trace("Executing findEntityViewsByTenantIdAndEdgeIdAndType, tenantId [{}], edgeId [{}], type [{}], pageLink [{}]", tenantId, edgeId, type, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(edgeId, INCORRECT_EDGE_ID + edgeId); - validateString(type, "Incorrect type " + type); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(edgeId, id -> INCORRECT_EDGE_ID + id); + validateString(type, t -> "Incorrect type " + t); validatePageLink(pageLink); return entityViewDao.findEntityViewsByTenantIdAndEdgeIdAndType(tenantId.getId(), edgeId.getId(), type, pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java index 88834eecbc..5314dcd405 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.service.DataValidator; import java.util.List; +import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -106,6 +107,11 @@ public class BaseEventService implements EventService { return convert(entityId.getEntityType(), eventDao.findLatestEvents(tenantId.getId(), entityId.getId(), eventType, limit)); } + @Override + public EventInfo findLatestDebugRuleNodeInEvent(TenantId tenantId, EntityId entityId) { + return convert(entityId.getEntityType(), eventDao.findLatestDebugRuleNodeInEvent(tenantId.getId(), entityId.getId())); + } + @Override public PageData findEventsByFilter(TenantId tenantId, EntityId entityId, EventFilter eventFilter, TimePageLink pageLink) { return convert(entityId.getEntityType(), eventDao.findEventByFilter(tenantId.getId(), entityId.getId(), eventFilter, pageLink)); @@ -140,4 +146,8 @@ public class BaseEventService implements EventService { return list == null ? null : list.stream().map(e -> e.toInfo(entityType)).collect(Collectors.toList()); } + private EventInfo convert(EntityType entityType, Event event) { + return Optional.ofNullable(event).map(e -> e.toInfo(entityType)).orElse(null); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java index 2db5a7012d..6b3febf502 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java @@ -62,6 +62,15 @@ public interface EventDao { */ List findLatestEvents(UUID tenantId, UUID entityId, EventType eventType, int limit); + /** + * Find latest debug IN event by tenantId, entityId. + * + * @param tenantId the tenantId + * @param entityId the entityId + * @return the latest debug IN event + */ + Event findLatestDebugRuleNodeInEvent(UUID tenantId, UUID entityId); + /** * Executes stored procedure to cleanup old events. Uses separate ttl for debug and other events. * @param regularEventExpTs the expiration time of the regular events diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 2fe56f5d1f..ae15c2d029 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -427,6 +427,7 @@ public class ModelConstants { */ public static final String OAUTH2_PARAMS_TABLE_NAME = "oauth2_params"; public static final String OAUTH2_PARAMS_ENABLED_PROPERTY = "enabled"; + public static final String OAUTH2_PARAMS_EDGE_ENABLED_PROPERTY = "edge_enabled"; public static final String OAUTH2_PARAMS_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; public static final String OAUTH2_REGISTRATION_TABLE_NAME = "oauth2_registration"; @@ -610,6 +611,14 @@ public class ModelConstants { public static final String QUEUE_TABLE_NAME = "queue"; public static final String QUEUE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; + /** + * Tenant queue stats constants. + */ + public static final String QUEUE_STATS_TABLE_NAME = "queue_stats"; + public static final String QUEUE_STATS_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + public static final String QUEUE_STATS_QUEUE_NAME_PROPERTY = "queue_name"; + public static final String QUEUE_STATS_SERVICE_ID_PROPERTY = "service_id"; + /** * Notification constants */ @@ -620,6 +629,7 @@ public class ModelConstants { public static final String NOTIFICATION_REQUEST_ID_PROPERTY = "request_id"; public static final String NOTIFICATION_RECIPIENT_ID_PROPERTY = "recipient_id"; public static final String NOTIFICATION_TYPE_PROPERTY = "type"; + public static final String NOTIFICATION_DELIVERY_METHOD_PROPERTY = "delivery_method"; public static final String NOTIFICATION_SUBJECT_PROPERTY = "subject"; public static final String NOTIFICATION_TEXT_PROPERTY = "body"; public static final String NOTIFICATION_ADDITIONAL_CONFIG_PROPERTY = "additional_config"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationEntity.java index 169af3b4c1..7d84f03e46 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationEntity.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.info.NotificationInfo; @@ -54,6 +55,10 @@ public class NotificationEntity extends BaseSqlEntity { @Column(name = ModelConstants.NOTIFICATION_TYPE_PROPERTY, nullable = false) private NotificationType type; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.NOTIFICATION_DELIVERY_METHOD_PROPERTY, nullable = false) + private NotificationDeliveryMethod deliveryMethod; + @Column(name = ModelConstants.NOTIFICATION_SUBJECT_PROPERTY) private String subject; @@ -80,6 +85,7 @@ public class NotificationEntity extends BaseSqlEntity { setRequestId(getUuid(notification.getRequestId())); setRecipientId(getUuid(notification.getRecipientId())); setType(notification.getType()); + setDeliveryMethod(notification.getDeliveryMethod()); setSubject(notification.getSubject()); setText(notification.getText()); setAdditionalConfig(notification.getAdditionalConfig()); @@ -95,6 +101,7 @@ public class NotificationEntity extends BaseSqlEntity { notification.setRequestId(getEntityId(requestId, NotificationRequestId::new)); notification.setRecipientId(getEntityId(recipientId, UserId::new)); notification.setType(type); + notification.setDeliveryMethod(deliveryMethod); notification.setSubject(subject); notification.setText(text); notification.setAdditionalConfig(additionalConfig); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ParamsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ParamsEntity.java index adbbeb7fd8..4983172d3e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ParamsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ParamsEntity.java @@ -39,6 +39,9 @@ public class OAuth2ParamsEntity extends BaseSqlEntity { @Column(name = ModelConstants.OAUTH2_PARAMS_ENABLED_PROPERTY) private Boolean enabled; + @Column(name = ModelConstants.OAUTH2_PARAMS_EDGE_ENABLED_PROPERTY) + private Boolean edgeEnabled; + @Column(name = ModelConstants.OAUTH2_PARAMS_TENANT_ID_PROPERTY) private UUID tenantId; @@ -48,6 +51,7 @@ public class OAuth2ParamsEntity extends BaseSqlEntity { } this.setCreatedTime(oauth2Params.getCreatedTime()); this.enabled = oauth2Params.isEnabled(); + this.edgeEnabled = oauth2Params.isEdgeEnabled(); if (oauth2Params.getTenantId() != null) { this.tenantId = oauth2Params.getTenantId().getId(); } @@ -60,6 +64,7 @@ public class OAuth2ParamsEntity extends BaseSqlEntity { oauth2Params.setCreatedTime(createdTime); oauth2Params.setTenantId(TenantId.fromUUID(tenantId)); oauth2Params.setEnabled(enabled); + oauth2Params.setEdgeEnabled(edgeEnabled); return oauth2Params; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueStatsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueStatsEntity.java new file mode 100644 index 0000000000..5b9ec291a2 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueStatsEntity.java @@ -0,0 +1,69 @@ +/** + * Copyright © 2016-2024 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.dao.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.id.QueueStatsId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.QueueStats; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@Table(name = ModelConstants.QUEUE_STATS_TABLE_NAME) +public class QueueStatsEntity extends BaseSqlEntity { + + @Column(name = ModelConstants.QUEUE_STATS_TENANT_ID_PROPERTY) + private UUID tenantId; + + @Column(name = ModelConstants.QUEUE_STATS_QUEUE_NAME_PROPERTY) + private String queueName; + + @Column(name = ModelConstants.QUEUE_STATS_SERVICE_ID_PROPERTY) + private String serviceId; + + public QueueStatsEntity() { + } + + public QueueStatsEntity(QueueStats queueStats) { + if (queueStats.getId() != null) { + this.setId(queueStats.getId().getId()); + } + this.setCreatedTime(queueStats.getCreatedTime()); + this.tenantId = DaoUtil.getId(queueStats.getTenantId()); + this.queueName = queueStats.getQueueName(); + this.serviceId = queueStats.getServiceId(); + } + + @Override + public QueueStats toData() { + QueueStats queueStats = new QueueStats(new QueueStatsId(getUuid())); + queueStats.setCreatedTime(createdTime); + queueStats.setTenantId(new TenantId(tenantId)); + queueStats.setQueueName(queueName); + queueStats.setServiceId(serviceId); + return queueStats; + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java index b62d9a83ad..38a8cb4178 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -57,29 +58,29 @@ public class DefaultNotificationService implements NotificationService, EntityDa } @Override - public int markAllNotificationsAsRead(TenantId tenantId, UserId recipientId) { - return notificationDao.updateStatusByRecipientId(tenantId, recipientId, NotificationStatus.READ); + public int markAllNotificationsAsRead(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId) { + return notificationDao.updateStatusByDeliveryMethodAndRecipientId(tenantId, deliveryMethod, recipientId, NotificationStatus.READ); } @Override - public PageData findNotificationsByRecipientIdAndReadStatus(TenantId tenantId, UserId recipientId, boolean unreadOnly, PageLink pageLink) { + public PageData findNotificationsByRecipientIdAndReadStatus(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, boolean unreadOnly, PageLink pageLink) { if (unreadOnly) { - return notificationDao.findUnreadByRecipientIdAndPageLink(tenantId, recipientId, pageLink); + return notificationDao.findUnreadByDeliveryMethodAndRecipientIdAndPageLink(tenantId, deliveryMethod, recipientId, pageLink); } else { - return notificationDao.findByRecipientIdAndPageLink(tenantId, recipientId, pageLink); + return notificationDao.findByDeliveryMethodAndRecipientIdAndPageLink(tenantId, deliveryMethod, recipientId, pageLink); } } @Override - public PageData findLatestUnreadNotificationsByRecipientId(TenantId tenantId, UserId recipientId, int limit) { + public PageData findLatestUnreadNotificationsByRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, int limit) { SortOrder sortOrder = new SortOrder(EntityKeyMapping.CREATED_TIME, SortOrder.Direction.DESC); PageLink pageLink = new PageLink(limit, 0, null, sortOrder); - return findNotificationsByRecipientIdAndReadStatus(tenantId, recipientId, true, pageLink); + return findNotificationsByRecipientIdAndReadStatus(tenantId, deliveryMethod, recipientId, true, pageLink); } @Override - public int countUnreadNotificationsByRecipientId(TenantId tenantId, UserId recipientId) { - return notificationDao.countUnreadByRecipientId(tenantId, recipientId); + public int countUnreadNotificationsByRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId) { + return notificationDao.countUnreadByDeliveryMethodAndRecipientId(tenantId, deliveryMethod, recipientId); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java index b719de0192..5b6f464ea6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java @@ -20,7 +20,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AdminSettings; @@ -157,7 +156,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS return new UserNotificationSettings(prefs); } - @Transactional(propagation = Propagation.NOT_SUPPORTED) // so that parent transaction is not aborted on method failure + @Transactional @Override public void createDefaultNotificationConfigs(TenantId tenantId) { NotificationTarget allUsers = createTarget(tenantId, "All users", new AllUsersFilter(), diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java index a92e4f6a4e..90f54a316a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java @@ -42,6 +42,8 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.EntityDaoService; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.user.UserService; import java.util.List; @@ -65,7 +67,10 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl @Override public NotificationTarget saveNotificationTarget(TenantId tenantId, NotificationTarget notificationTarget) { try { - return notificationTargetDao.saveAndFlush(tenantId, notificationTarget); + NotificationTarget savedTarget = notificationTargetDao.saveAndFlush(tenantId, notificationTarget); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entityId(savedTarget.getId()) + .created(notificationTarget.getId() == null).build()); + return savedTarget; } catch (Exception e) { checkConstraintViolation(e, Map.of( "uq_notification_target_name", "Recipients group with such name already exists" @@ -158,19 +163,21 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl @Override public PageData findRecipientsForRuleNotificationTargetConfig(TenantId tenantId, PlatformUsersNotificationTargetConfig targetConfig, RuleOriginatedNotificationInfo info, PageLink pageLink) { switch (targetConfig.getUsersFilter().getType()) { - case ORIGINATOR_ENTITY_OWNER_USERS: + case ORIGINATOR_ENTITY_OWNER_USERS -> { CustomerId customerId = info.getAffectedCustomerId(); if (customerId != null && !customerId.isNullUid()) { return userService.findCustomerUsers(tenantId, customerId, pageLink); } else { return userService.findTenantAdmins(tenantId, pageLink); } - case AFFECTED_USER: + } + case AFFECTED_USER -> { UserId userId = info.getAffectedUserId(); if (userId != null) { return new PageData<>(List.of(userService.findUserById(tenantId, userId)), 1, 1, false); } - case AFFECTED_TENANT_ADMINISTRATORS: + } + case AFFECTED_TENANT_ADMINISTRATORS -> { TenantId affectedTenantId = info.getAffectedTenantId(); if (affectedTenantId == null) { affectedTenantId = tenantId; @@ -178,9 +185,8 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl if (!affectedTenantId.isNullUid()) { return userService.findTenantAdmins(affectedTenantId, pageLink); } - break; - default: - throw new IllegalArgumentException("Recipient type not supported"); + } + default -> throw new IllegalArgumentException("Recipient type not supported"); } return new PageData<>(); } @@ -194,6 +200,7 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl throw new IllegalArgumentException("Recipients group is being used in notification rule"); } notificationTargetDao.removeById(tenantId, id.getId()); + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(id).build()); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java index 17b166c3ec..58ec572b7f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java @@ -29,6 +29,8 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.EntityDaoService; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import java.util.List; import java.util.Map; @@ -55,7 +57,10 @@ public class DefaultNotificationTemplateService extends AbstractEntityService im } } try { - return notificationTemplateDao.saveAndFlush(tenantId, notificationTemplate); + NotificationTemplate savedTemplate = notificationTemplateDao.saveAndFlush(tenantId, notificationTemplate); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entityId(savedTemplate.getId()) + .created(notificationTemplate.getId() == null).build()); + return savedTemplate; } catch (Exception e) { checkConstraintViolation(e, Map.of( "uq_notification_template_name", "Notification template with such name already exists" @@ -82,6 +87,7 @@ public class DefaultNotificationTemplateService extends AbstractEntityService im )); throw e; } + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(id).build()); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationDao.java b/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationDao.java index 02bfe11fde..2233840058 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationDao.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -27,19 +28,17 @@ import org.thingsboard.server.dao.Dao; public interface NotificationDao extends Dao { - PageData findUnreadByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink); + PageData findUnreadByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink); - PageData findByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink); + PageData findByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink); boolean updateStatusByIdAndRecipientId(TenantId tenantId, UserId recipientId, NotificationId notificationId, NotificationStatus status); - int countUnreadByRecipientId(TenantId tenantId, UserId recipientId); - - PageData findByRequestId(TenantId tenantId, NotificationRequestId notificationRequestId, PageLink pageLink); + int countUnreadByDeliveryMethodAndRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId); boolean deleteByIdAndRecipientId(TenantId tenantId, UserId recipientId, NotificationId notificationId); - int updateStatusByRecipientId(TenantId tenantId, UserId recipientId, NotificationStatus status); + int updateStatusByDeliveryMethodAndRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, NotificationStatus status); void deleteByRequestId(TenantId tenantId, NotificationRequestId requestId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java index ed456a93cf..8d0faaca92 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java @@ -64,14 +64,14 @@ public class OAuth2ConfigTemplateServiceImpl extends AbstractEntityService imple @Override public Optional findClientRegistrationTemplateByProviderId(String providerId) { log.trace("Executing findClientRegistrationTemplateByProviderId [{}]", providerId); - validateString(providerId, INCORRECT_CLIENT_REGISTRATION_PROVIDER_ID + providerId); + validateString(providerId, id -> INCORRECT_CLIENT_REGISTRATION_PROVIDER_ID + id); return clientRegistrationTemplateDao.findByProviderId(providerId); } @Override public OAuth2ClientRegistrationTemplate findClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId) { log.trace("Executing findClientRegistrationTemplateById [{}]", templateId); - validateId(templateId, INCORRECT_CLIENT_REGISTRATION_TEMPLATE_ID + templateId); + validateId(templateId, id -> INCORRECT_CLIENT_REGISTRATION_TEMPLATE_ID + id); return clientRegistrationTemplateDao.findById(TenantId.SYS_TENANT_ID, templateId.getId()); } @@ -84,7 +84,7 @@ public class OAuth2ConfigTemplateServiceImpl extends AbstractEntityService imple @Override public void deleteClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId) { log.trace("Executing deleteClientRegistrationTemplateById [{}]", templateId); - validateId(templateId, INCORRECT_CLIENT_REGISTRATION_TEMPLATE_ID + templateId); + validateId(templateId, id -> INCORRECT_CLIENT_REGISTRATION_TEMPLATE_ID + id); clientRegistrationTemplateDao.removeById(TenantId.SYS_TENANT_ID, templateId.getId()); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java index 58be6a0770..6535b54a2c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java @@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.oauth2.SchemeType; import org.thingsboard.server.common.data.oauth2.TenantNameStrategyType; import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; @@ -58,6 +59,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString; @Slf4j @Service public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Service { + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String INCORRECT_CLIENT_REGISTRATION_ID = "Incorrect clientRegistrationId "; public static final String INCORRECT_DOMAIN_NAME = "Incorrect domainName "; @@ -84,7 +86,7 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se } catch (IllegalArgumentException e){ throw new IncorrectParameterException(INCORRECT_DOMAIN_SCHEME); } - validateString(domainName, INCORRECT_DOMAIN_NAME + domainName); + validateString(domainName, dn -> INCORRECT_DOMAIN_NAME + dn); return oauth2RegistrationDao.findEnabledByDomainSchemesDomainNameAndPkgNameAndPlatformType( Arrays.asList(domainScheme, SchemeType.MIXED), domainName, pkgName, platformType) .stream() @@ -116,6 +118,7 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se }); } }); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(TenantId.SYS_TENANT_ID).entity(oauth2Info).build()); } @Override @@ -123,7 +126,8 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se log.trace("Executing findOAuth2Info"); OAuth2Info oauth2Info = new OAuth2Info(); List oauth2ParamsList = oauth2ParamsDao.find(TenantId.SYS_TENANT_ID); - oauth2Info.setEnabled(oauth2ParamsList.stream().anyMatch(param -> param.isEnabled())); + oauth2Info.setEnabled(oauth2ParamsList.stream().anyMatch(OAuth2Params::isEnabled)); + oauth2Info.setEdgeEnabled(oauth2ParamsList.stream().anyMatch(OAuth2Params::isEdgeEnabled)); List oauth2ParamsInfos = new ArrayList<>(); oauth2Info.setOauth2ParamsInfos(oauth2ParamsInfos); oauth2ParamsList.stream().sorted(Comparator.comparing(BaseData::getUuidId)).forEach(oauth2Params -> { @@ -138,14 +142,14 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se @Override public OAuth2Registration findRegistration(UUID id) { log.trace("Executing findRegistration [{}]", id); - validateId(id, INCORRECT_CLIENT_REGISTRATION_ID + id); + validateId(id, uuid -> INCORRECT_CLIENT_REGISTRATION_ID + uuid); return oauth2RegistrationDao.findById(null, id); } @Override public String findAppSecret(UUID id, String pkgName) { log.trace("Executing findAppSecret [{}][{}]", id, pkgName); - validateId(id, INCORRECT_CLIENT_REGISTRATION_ID + id); + validateId(id, uuid -> INCORRECT_CLIENT_REGISTRATION_ID + uuid); validateString(pkgName, "Incorrect package name"); return oauth2RegistrationDao.findAppSecret(id, pkgName); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java index 4e22ea858b..8d30fc905d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java @@ -88,6 +88,7 @@ public class OAuth2Utils { public static OAuth2Params infoToOAuth2Params(OAuth2Info oauth2Info) { OAuth2Params oauth2Params = new OAuth2Params(); oauth2Params.setEnabled(oauth2Info.isEnabled()); + oauth2Params.setEdgeEnabled(oauth2Info.isEdgeEnabled()); oauth2Params.setTenantId(TenantId.SYS_TENANT_ID); return oauth2Params; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index 53a585c358..b2fa1a3788 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -159,14 +159,14 @@ public class BaseOtaPackageService extends AbstractCachedEntityService INCORRECT_OTA_PACKAGE_ID + id); return otaPackageDao.findById(tenantId, otaPackageId.getId()); } @Override public OtaPackageInfo findOtaPackageInfoById(TenantId tenantId, OtaPackageId otaPackageId) { log.trace("Executing findOtaPackageInfoById [{}]", otaPackageId); - validateId(otaPackageId, INCORRECT_OTA_PACKAGE_ID + otaPackageId); + validateId(otaPackageId, id -> INCORRECT_OTA_PACKAGE_ID + id); return cache.getAndPutInTransaction(new OtaPackageCacheKey(otaPackageId), () -> otaPackageInfoDao.findById(tenantId, otaPackageId.getId()), true); } @@ -174,14 +174,14 @@ public class BaseOtaPackageService extends AbstractCachedEntityService findOtaPackageInfoByIdAsync(TenantId tenantId, OtaPackageId otaPackageId) { log.trace("Executing findOtaPackageInfoByIdAsync [{}]", otaPackageId); - validateId(otaPackageId, INCORRECT_OTA_PACKAGE_ID + otaPackageId); + validateId(otaPackageId, id -> INCORRECT_OTA_PACKAGE_ID + id); return otaPackageInfoDao.findByIdAsync(tenantId, otaPackageId.getId()); } @Override public PageData findTenantOtaPackagesByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findTenantOtaPackagesByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validatePageLink(pageLink); return otaPackageInfoDao.findOtaPackageInfoByTenantId(tenantId, pageLink); } @@ -189,7 +189,7 @@ public class BaseOtaPackageService extends AbstractCachedEntityService findTenantOtaPackagesByTenantIdAndDeviceProfileIdAndTypeAndHasData(TenantId tenantId, DeviceProfileId deviceProfileId, OtaPackageType otaPackageType, PageLink pageLink) { log.trace("Executing findTenantOtaPackagesByTenantIdAndHasData, tenantId [{}], pageLink [{}]", tenantId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validatePageLink(pageLink); return otaPackageInfoDao.findOtaPackageInfoByTenantIdAndDeviceProfileIdAndTypeAndHasData(tenantId, deviceProfileId, otaPackageType, pageLink); } @@ -197,7 +197,7 @@ public class BaseOtaPackageService extends AbstractCachedEntityService INCORRECT_OTA_PACKAGE_ID + id); try { otaPackageDao.removeById(tenantId, otaPackageId.getId()); publishEvictEvent(new OtaPackageCacheEvictEvent(otaPackageId)); @@ -226,7 +226,7 @@ public class BaseOtaPackageService extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); tenantOtaPackageRemover.removeEntities(tenantId, tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java new file mode 100644 index 0000000000..2274a782bb --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java @@ -0,0 +1,90 @@ +/** + * Copyright © 2016-2024 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.dao.queue; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; +import org.thingsboard.server.common.data.id.QueueStatsId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.QueueStats; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.service.DataValidator; + +import java.util.List; +import java.util.Optional; + +import static org.thingsboard.server.dao.service.Validator.validateId; + +@Service("QueueStatsDaoService") +@Slf4j +@RequiredArgsConstructor +public class BaseQueueStatsService extends AbstractEntityService implements QueueStatsService { + + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + + private final QueueStatsDao queueStatsDao; + + private final DataValidator queueStatsValidator; + + @Override + public QueueStats save(TenantId tenantId, QueueStats queueStats) { + log.trace("Executing save [{}]", queueStats); + queueStatsValidator.validate(queueStats, QueueStats::getTenantId); + return queueStatsDao.save(tenantId, queueStats); + } + + @Override + public QueueStats findQueueStatsById(TenantId tenantId, QueueStatsId queueStatsId) { + log.trace("Executing findQueueStatsById [{}]", queueStatsId); + validateId(queueStatsId, id -> "Incorrect queueStatsId " + id); + return queueStatsDao.findById(tenantId, queueStatsId.getId()); + } + + @Override + public QueueStats findByTenantIdAndNameAndServiceId(TenantId tenantId, String queueName, String serviceId) { + log.trace("Executing findByTenantIdAndNameAndServiceId, tenantId: [{}], queueName: [{}], serviceId: [{}]", tenantId, queueName, serviceId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + return queueStatsDao.findByTenantIdQueueNameAndServiceId(tenantId, queueName, serviceId); + } + + @Override + public List findByTenantId(TenantId tenantId) { + log.trace("Executing findByTenantId, tenantId: [{}]", tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + return queueStatsDao.findByTenantId(tenantId); + } + + @Override + public void deleteByTenantId(TenantId tenantId) { + log.trace("Executing deleteByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + queueStatsDao.deleteByTenantId(tenantId); + } + + @Override + public Optional> findEntity(TenantId tenantId, EntityId entityId) { + return Optional.ofNullable(findQueueStatsById(tenantId, new QueueStatsId(entityId.getId()))); + } + + @Override + public EntityType getEntityType() { + return EntityType.QUEUE_STATS; + } +} diff --git a/application/src/test/java/org/thingsboard/server/utils/TbSubscriptionUtilsTest.java b/dao/src/main/java/org/thingsboard/server/dao/queue/QueueStatsDao.java similarity index 53% rename from application/src/test/java/org/thingsboard/server/utils/TbSubscriptionUtilsTest.java rename to dao/src/main/java/org/thingsboard/server/dao/queue/QueueStatsDao.java index a3f1f0f0fa..1c3db8bb54 100644 --- a/application/src/test/java/org/thingsboard/server/utils/TbSubscriptionUtilsTest.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/QueueStatsDao.java @@ -13,21 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.utils; +package org.thingsboard.server.dao.queue; -import org.junit.Test; -import org.thingsboard.server.common.data.kv.DataType; -import org.thingsboard.server.service.subscription.TbSubscriptionUtils; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.QueueStats; +import org.thingsboard.server.dao.Dao; -import static org.assertj.core.api.Assertions.assertThat; +import java.util.List; -public class TbSubscriptionUtilsTest { +public interface QueueStatsDao extends Dao { - @Test - public void protoDataTypeSerialization() { - for (DataType dataType : DataType.values()) { - assertThat(TbSubscriptionUtils.fromProto(TbSubscriptionUtils.toProto(dataType))).as(dataType.name()).isEqualTo(dataType); - } - } + QueueStats findByTenantIdQueueNameAndServiceId(TenantId tenantId, String queueName, String serviceId); -} + List findByTenantId(TenantId tenantId); + + void deleteByTenantId(TenantId tenantId); + +} \ No newline at end of file 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 89cfff08d1..ec6af730ea 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 @@ -513,7 +513,7 @@ public class BaseRelationService implements RelationService { @Override public List findRuleNodeToRuleChainRelations(TenantId tenantId, RuleChainType ruleChainType, int limit) { log.trace("Executing findRuleNodeToRuleChainRelations, tenantId [{}], ruleChainType {} and limit {}", tenantId, ruleChainType, limit); - validateId(tenantId, "Invalid tenant id: " + tenantId); + validateId(tenantId, id -> "Invalid tenant id: " + id); return relationDao.findRuleNodeToRuleChainRelations(ruleChainType, limit); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java index ef05229124..953037a864 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java @@ -256,7 +256,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic var tenantId = imageInfo.getTenantId(); var imageId = imageInfo.getId(); log.trace("Executing deleteImage [{}] [{}]", tenantId, imageId); - Validator.validateId(imageId, INCORRECT_RESOURCE_ID + imageId); + Validator.validateId(imageId, id -> INCORRECT_RESOURCE_ID + id); TbImageDeleteResult.TbImageDeleteResultBuilder result = TbImageDeleteResult.builder(); boolean success = true; if (!force) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index a5a692e3d4..37b030996f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -116,14 +116,14 @@ public class BaseResourceService extends AbstractCachedEntityService INCORRECT_RESOURCE_ID + id); return resourceDao.findById(tenantId, resourceId.getId()); } @Override public TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId) { log.trace("Executing findResourceInfoById [{}] [{}]", tenantId, resourceId); - Validator.validateId(resourceId, INCORRECT_RESOURCE_ID + resourceId); + Validator.validateId(resourceId, id -> INCORRECT_RESOURCE_ID + id); return cache.getAndPutInTransaction(new ResourceInfoCacheKey(tenantId, resourceId), () -> resourceInfoDao.findById(tenantId, resourceId.getId()), true); @@ -138,7 +138,7 @@ public class BaseResourceService extends AbstractCachedEntityService findResourceInfoByIdAsync(TenantId tenantId, TbResourceId resourceId) { log.trace("Executing findResourceInfoById [{}] [{}]", tenantId, resourceId); - Validator.validateId(resourceId, INCORRECT_RESOURCE_ID + resourceId); + Validator.validateId(resourceId, id -> INCORRECT_RESOURCE_ID + id); return resourceInfoDao.findByIdAsync(tenantId, resourceId.getId()); } @@ -150,7 +150,7 @@ public class BaseResourceService extends AbstractCachedEntityService INCORRECT_RESOURCE_ID + id); if (!force) { resourceValidator.validateDelete(tenantId, resourceId); } @@ -163,7 +163,7 @@ public class BaseResourceService extends AbstractCachedEntityService findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) { TenantId tenantId = filter.getTenantId(); log.trace("Executing findAllTenantResourcesByTenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); return resourceInfoDao.findAllTenantResourcesByTenantId(filter, pageLink); } @@ -171,35 +171,35 @@ public class BaseResourceService extends AbstractCachedEntityService findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) { TenantId tenantId = filter.getTenantId(); log.trace("Executing findTenantResourcesByTenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); return resourceInfoDao.findTenantResourcesByTenantId(filter, pageLink); } @Override public List findTenantResourcesByResourceTypeAndObjectIds(TenantId tenantId, ResourceType resourceType, String[] objectIds) { log.trace("Executing findTenantResourcesByResourceTypeAndObjectIds [{}][{}][{}]", tenantId, resourceType, objectIds); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, objectIds, null); } @Override public PageData findAllTenantResources(TenantId tenantId, PageLink pageLink) { log.trace("Executing findAllTenantResources [{}][{}]", tenantId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); return resourceDao.findAllByTenantId(tenantId, pageLink); } @Override public PageData findTenantResourcesByResourceTypeAndPageLink(TenantId tenantId, ResourceType resourceType, PageLink pageLink) { log.trace("Executing findTenantResourcesByResourceTypeAndPageLink [{}][{}][{}]", tenantId, resourceType, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, pageLink); } @Override public void deleteResourcesByTenantId(TenantId tenantId) { log.trace("Executing deleteResourcesByTenantId, tenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); tenantResourcesRemover.removeEntities(tenantId, tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java b/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java index 54d7a2170c..2b314e82df 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java @@ -54,38 +54,38 @@ public class BaseRpcService implements RpcService { @Override public void deleteRpc(TenantId tenantId, RpcId rpcId) { log.trace("Executing deleteRpc, tenantId [{}], rpcId [{}]", tenantId, rpcId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(rpcId, INCORRECT_RPC_ID + rpcId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(rpcId, id -> INCORRECT_RPC_ID + id); rpcDao.removeById(tenantId, rpcId.getId()); } @Override public void deleteAllRpcByTenantId(TenantId tenantId) { log.trace("Executing deleteAllRpcByTenantId, tenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); tenantRpcRemover.removeEntities(tenantId, tenantId); } @Override public Rpc findById(TenantId tenantId, RpcId rpcId) { log.trace("Executing findById, tenantId [{}], rpcId [{}]", tenantId, rpcId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(rpcId, INCORRECT_RPC_ID + rpcId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(rpcId, id -> INCORRECT_RPC_ID + id); return rpcDao.findById(tenantId, rpcId.getId()); } @Override public ListenableFuture findRpcByIdAsync(TenantId tenantId, RpcId rpcId) { log.trace("Executing findRpcByIdAsync, tenantId [{}], rpcId: [{}]", tenantId, rpcId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(rpcId, INCORRECT_RPC_ID + rpcId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(rpcId, id -> INCORRECT_RPC_ID + id); return rpcDao.findByIdAsync(tenantId, rpcId.getId()); } @Override public PageData findAllByDeviceIdAndStatus(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink) { log.trace("Executing findAllByDeviceIdAndStatus, tenantId [{}], deviceId [{}], rpcStatus [{}], pageLink [{}]", tenantId, deviceId, rpcStatus, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validatePageLink(pageLink); return rpcDao.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink); } @@ -93,7 +93,7 @@ public class BaseRpcService implements RpcService { @Override public PageData findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink) { log.trace("Executing findAllByDeviceIdAndStatus, tenantId [{}], deviceId [{}], pageLink [{}]", tenantId, deviceId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validatePageLink(pageLink); return rpcDao.findAllByDeviceId(tenantId, deviceId, pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 2857436fc8..8e1af4a531 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -119,7 +119,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC public RuleChain saveRuleChain(RuleChain ruleChain, boolean publishSaveEvent) { ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); try { - RuleChain savedRuleChain = ruleChainDao.save(ruleChain.getTenantId(), ruleChain); + RuleChain savedRuleChain = ruleChainDao.saveAndFlush(ruleChain.getTenantId(), ruleChain); if (ruleChain.getId() == null) { entityCountService.publishCountEntityEvictEvent(ruleChain.getTenantId(), EntityType.RULE_CHAIN); } @@ -645,8 +645,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override public PageData findRuleChainsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink) { log.trace("Executing findRuleChainsByTenantIdAndEdgeId, tenantId [{}], edgeId [{}], pageLink [{}]", tenantId, edgeId, pageLink); - Validator.validateId(tenantId, "Incorrect tenantId " + tenantId); - Validator.validateId(edgeId, "Incorrect edgeId " + edgeId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + Validator.validateId(edgeId, id -> "Incorrect edgeId " + id); Validator.validatePageLink(pageLink); return ruleChainDao.findRuleChainsByTenantIdAndEdgeId(tenantId.getId(), edgeId.getId(), pageLink); } @@ -705,14 +705,14 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override public PageData findAutoAssignToEdgeRuleChainsByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findAutoAssignToEdgeRuleChainsByTenantId, tenantId [{}], pageLink {}", tenantId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); return ruleChainDao.findAutoAssignToEdgeRuleChainsByTenantId(tenantId.getId(), pageLink); } @Override public List findRuleNodesByTenantIdAndType(TenantId tenantId, String type, String search) { log.trace("Executing findRuleNodes, tenantId [{}], type {}, search {}", tenantId, type, search); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validateString(type, "Incorrect type of the rule node"); validateString(search, "Incorrect search text"); return ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId, type, search); @@ -721,7 +721,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override public List findRuleNodesByTenantIdAndType(TenantId tenantId, String type) { log.trace("Executing findRuleNodes, tenantId [{}], type {}", tenantId, type); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validateString(type, "Incorrect type of the rule node"); return ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId, type, ""); } @@ -755,7 +755,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override public List findAllRuleNodesByIds(List ruleNodeIds) { log.trace("Executing findAllRuleNodesByIds, ruleNodeIds {}", ruleNodeIds); - validateIds(ruleNodeIds, "Incorrect ruleNodeIds " + ruleNodeIds); + validateIds(ruleNodeIds, ids -> "Incorrect ruleNodeIds " + ids); assert ruleNodeIds.size() <= 1024; return ruleNodeDao.findAllRuleNodeByIds(ruleNodeIds); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java index b3a121baf6..dab28e298b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java @@ -38,9 +38,10 @@ public class Validator { * This method validate EntityId entity id. If entity id is invalid than throw * IncorrectParameterException exception * - * @param entityId the entityId - * @param errorMessage the error message for exception + * @param entityId the entityId + * @param errorMessage the error message for exception */ + @Deprecated public static void validateEntityId(EntityId entityId, String errorMessage) { if (entityId == null || entityId.getId() == null) { throw new IncorrectParameterException(errorMessage); @@ -51,8 +52,8 @@ public class Validator { * This method validate EntityId entity id. If entity id is invalid than throw * IncorrectParameterException exception * - * @param entityId the entityId - * @param errorMessageFunction the error message for exception that apply entityId + * @param entityId the entityId + * @param errorMessageFunction the error message function for exception that applies entityId */ public static void validateEntityId(EntityId entityId, Function errorMessageFunction) { if (entityId == null || entityId.getId() == null) { @@ -64,8 +65,8 @@ public class Validator { * This method validate String string. If string is invalid than throw * IncorrectParameterException exception * - * @param val the val - * @param errorMessage the error message for exception + * @param val the val + * @param errorMessage the error message for exception */ public static void validateString(String val, String errorMessage) { if (val == null || val.isEmpty()) { @@ -73,12 +74,12 @@ public class Validator { } } - /* + /** * This method validate String string. If string is invalid than throw * IncorrectParameterException exception * - * @param val the value - * @param errorMessageFunction the error message function that apply value + * @param val the value + * @param errorMessageFunction the error message function that applies value */ public static void validateString(String val, Function errorMessageFunction) { if (val == null || val.isEmpty()) { @@ -90,8 +91,8 @@ public class Validator { * This method validate long value. If value isn't positive than throw * IncorrectParameterException exception * - * @param val the val - * @param errorMessage the error message for exception + * @param val the val + * @param errorMessage the error message for exception */ public static void validatePositiveNumber(long val, String errorMessage) { if (val <= 0) { @@ -103,36 +104,78 @@ public class Validator { * This method validate UUID id. If id is null than throw * IncorrectParameterException exception * - * @param id the id - * @param errorMessage the error message for exception + * @param id the id + * @param errorMessage the error message for exception */ + @Deprecated public static void validateId(UUID id, String errorMessage) { if (id == null) { throw new IncorrectParameterException(errorMessage); } } + /** + * This method validate UUID id. If id is null than throw + * IncorrectParameterException exception + * + * @param id the id + * @param errorMessageFunction the error message function for exception that applies id + */ + public static void validateId(UUID id, Function errorMessageFunction) { + if (id == null) { + throw new IncorrectParameterException(errorMessageFunction.apply(id)); + } + } /** * This method validate UUIDBased id. If id is null than throw * IncorrectParameterException exception * - * @param id the id - * @param errorMessage the error message for exception + * @param id the id + * @param errorMessage the error message for exception */ + @Deprecated public static void validateId(UUIDBased id, String errorMessage) { if (id == null || id.getId() == null) { throw new IncorrectParameterException(errorMessage); } } + /** + * This method validate UUIDBased id. If id is null than throw + * IncorrectParameterException exception + * + * @param id the id + * @param errorMessageFunction the error message function for exception that applies id + */ + public static void validateId(UUIDBased id, Function errorMessageFunction) { + if (id == null || id.getId() == null) { + throw new IncorrectParameterException(errorMessageFunction.apply(id)); + } + } + + /** + * This method validate UUIDBased id. If id is null than throw + * IncorrectParameterException exception + * + * @param id the id + * @param ids the list of ids + * @param errorMessageFunction the error message function for exception that applies ids + */ + static void validateId(UUIDBased id, List ids, Function, String> errorMessageFunction) { + if (id == null) { + throw new IncorrectParameterException(errorMessageFunction.apply(ids)); + } + } + /** * This method validate list of UUIDBased ids. If at least one of the ids is null than throw * IncorrectParameterException exception * - * @param ids the list of ids - * @param errorMessage the error message for exception + * @param ids the list of ids + * @param errorMessage the error message for exception */ + @Deprecated public static void validateIds(List ids, String errorMessage) { if (ids == null || ids.isEmpty()) { throw new IncorrectParameterException(errorMessage); @@ -143,11 +186,28 @@ public class Validator { } } + /** + * This method validate list of UUIDBased ids. If at least one of the ids is null than throw + * IncorrectParameterException exception + * + * @param ids the list of ids + * @param errorMessageFunction the error message function for exception that applies ids + */ + public static void validateIds(List ids, Function, String> errorMessageFunction) { + if (ids == null || ids.isEmpty()) { + throw new IncorrectParameterException(errorMessageFunction.apply(ids)); + } else { + for (UUIDBased id : ids) { + validateId(id, ids, errorMessageFunction); + } + } + } + /** * This method validate PageLink page link. If pageLink is invalid than throw * IncorrectParameterException exception * - * @param pageLink the page link + * @param pageLink the page link */ public static void validatePageLink(PageLink pageLink) { if (pageLink == null) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java index 5ef0b7a2ba..2d0cdbd248 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java @@ -47,9 +47,7 @@ public class AssetDataValidator extends DataValidator { @Override protected void validateCreate(TenantId tenantId, Asset asset) { - if (!BaseAssetService.TB_SERVICE_QUEUE.equals(asset.getType())) { - validateNumberOfEntitiesPerTenant(tenantId, EntityType.ASSET); - } + validateNumberOfEntitiesPerTenant(tenantId, EntityType.ASSET); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueStatsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueStatsDataValidator.java new file mode 100644 index 0000000000..92134b98d5 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueStatsDataValidator.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2024 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.dao.service.validator; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.QueueStats; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; + +@Component +public class QueueStatsDataValidator extends DataValidator { + + @Override + protected void validateDataImpl(TenantId tenantId, QueueStats queueStats) { + if (queueStats.getTenantId() == null) { + throw new DataValidationException("Tenant id should be specified!."); + } + if (queueStats.getQueueName() == null) { + throw new DataValidationException("Queue name should be specified!."); + } + if (StringUtils.isEmpty(queueStats.getServiceId())) { + throw new DataValidationException("Service id should be specified!."); + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java index bfc99d1b54..13aaba8751 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java @@ -39,14 +39,14 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { @Override public AdminSettings findAdminSettingsById(TenantId tenantId, AdminSettingsId adminSettingsId) { log.trace("Executing findAdminSettingsById [{}]", adminSettingsId); - Validator.validateId(adminSettingsId, "Incorrect adminSettingsId " + adminSettingsId); + Validator.validateId(adminSettingsId, id -> "Incorrect adminSettingsId " + id); return adminSettingsDao.findById(tenantId, adminSettingsId.getId()); } @Override public AdminSettings findAdminSettingsByKey(TenantId tenantId, String key) { log.trace("Executing findAdminSettingsByKey [{}]", key); - Validator.validateString(key, "Incorrect key " + key); + Validator.validateString(key, k -> "Incorrect key " + k); return findAdminSettingsByTenantIdAndKey(TenantId.SYS_TENANT_ID, key); } @@ -82,7 +82,7 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { @Override public boolean deleteAdminSettingsByTenantIdAndKey(TenantId tenantId, String key) { log.trace("Executing deleteAdminSettings, tenantId [{}], key [{}]", tenantId, key); - Validator.validateString(key, "Incorrect key " + key); + Validator.validateString(key, k -> "Incorrect key " + k); return adminSettingsDao.removeByTenantIdAndKey(tenantId.getId(), key); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index f216da4445..83827973ba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -333,6 +333,9 @@ public interface AlarmRepository extends JpaRepository { @Query("SELECT a.id FROM AlarmEntity a WHERE a.tenantId = :tenantId AND a.assigneeId = :assigneeId") Page findAlarmIdsByAssigneeId(@Param("tenantId") UUID tenantId, @Param("assigneeId") UUID assigneeId, Pageable pageable); + @Query("SELECT a.id FROM AlarmEntity a WHERE a.tenantId = :tenantId AND a.originatorId = :originatorId") + Page findAlarmIdsByOriginatorId(@Param("tenantId") UUID tenantId, @Param("originatorId") UUID originatorId, Pageable pageable); + @Query(value = "SELECT create_or_update_active_alarm(:t_id, :c_id, :a_id, :a_created_ts, :a_o_id, :a_o_type, :a_type, :a_severity, " + ":a_start_ts, :a_end_ts, :a_details, :a_propagate, :a_propagate_to_owner, " + ":a_propagate_to_tenant, :a_propagation_types, :a_creation_enabled)", nativeQuery = true) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index 9430773ff9..cbadbb4951 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -296,6 +296,12 @@ public class JpaAlarmDao extends JpaAbstractDao implements A .mapData(AlarmId::new); } + @Override + public PageData findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, PageLink pageLink) { + return DaoUtil.pageToPageData(alarmRepository.findAlarmIdsByOriginatorId(tenantId.getId(), originatorId.getId(), DaoUtil.toPageable(pageLink))) + .mapData(AlarmId::new); + } + @Override public void createEntityAlarmRecord(EntityAlarm entityAlarm) { log.debug("Saving entity {}", entityAlarm); @@ -383,7 +389,7 @@ public class JpaAlarmDao extends JpaAbstractDao implements A @Override public PageData findTenantAlarmTypes(UUID tenantId, PageLink pageLink) { - Page page = alarmRepository.findTenantAlarmTypes(tenantId, Objects.toString(pageLink.getTextSearch(), ""), toPageable(pageLink)); + Page page = alarmRepository.findTenantAlarmTypes(tenantId, Objects.toString(pageLink.getTextSearch(), ""), toPageable(pageLink, false)); if (page.isEmpty()) { return PageData.emptyPageData(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java index bbe212fb0b..f65e0db1c6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java @@ -189,7 +189,7 @@ public interface AssetRepository extends JpaRepository, Expor @Param("searchText") String searchText, Pageable pageable); - Long countByTenantIdAndTypeIsNot(UUID tenantId, String type); + Long countByTenantId(UUID tenantId); @Query("SELECT externalId FROM AssetEntity WHERE id = :id") UUID getExternalIdById(@Param("id") UUID id); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java index b77559be7b..0ef4370f30 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java @@ -43,7 +43,6 @@ import java.util.Optional; import java.util.UUID; import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityInfosToDto; -import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE; /** * Created by Valerii Sosliuk on 5/19/2017. @@ -244,7 +243,7 @@ public class JpaAssetDao extends JpaAbstractDao implements A @Override public Long countByTenantId(TenantId tenantId) { - return assetRepository.countByTenantIdAndTypeIsNot(tenantId.getId(), TB_SERVICE_QUEUE); + return assetRepository.countByTenantId(tenantId.getId()); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java index 67bde161b6..afd6608774 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java @@ -392,6 +392,11 @@ public class JpaBaseEventDao implements EventDao { return DaoUtil.convertDataList(getEventRepository(eventType).findLatestEvents(tenantId, entityId, limit)); } + @Override + public Event findLatestDebugRuleNodeInEvent(UUID tenantId, UUID entityId) { + return DaoUtil.getData(ruleNodeDebugEventRepository.findLatestDebugRuleNodeInEvent(tenantId, entityId)); + } + @Override public void cleanupEvents(long regularEventExpTs, long debugEventExpTs, boolean cleanupDb) { if (regularEventExpTs > 0) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/RuleNodeDebugEventRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/RuleNodeDebugEventRepository.java index 7c13c923c8..05473d92f7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/RuleNodeDebugEventRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/RuleNodeDebugEventRepository.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.event.RuleNodeDebugEvent; import org.thingsboard.server.dao.model.sql.RuleNodeDebugEventEntity; import java.util.List; +import java.util.Optional; import java.util.UUID; @@ -35,6 +36,9 @@ public interface RuleNodeDebugEventRepository extends EventRepository findLatestEvents(@Param("tenantId") UUID tenantId, @Param("entityId") UUID entityId, @Param("limit") int limit); + @Query(nativeQuery = true, value = "SELECT * FROM rule_node_debug_event e WHERE e.tenant_id = :tenantId AND e.entity_id = :entityId AND e.e_type = 'IN' ORDER BY e.ts DESC LIMIT 1") + Optional findLatestDebugRuleNodeInEvent(@Param("tenantId") UUID tenantId, @Param("entityId") UUID entityId); + @Override @Query("SELECT e FROM RuleNodeDebugEventEntity e WHERE " + "e.tenantId = :tenantId " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java index f095aad1ae..64d663b397 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -51,14 +52,14 @@ public class JpaNotificationDao extends JpaPartitionedAbstractDao findUnreadByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink) { - return DaoUtil.toPageData(notificationRepository.findByRecipientIdAndStatusNot(recipientId.getId(), NotificationStatus.READ, - pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); + public PageData findUnreadByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink) { + return DaoUtil.toPageData(notificationRepository.findByDeliveryMethodAndRecipientIdAndStatusNot(deliveryMethod, + recipientId.getId(), NotificationStatus.READ, pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); } @Override - public PageData findByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink) { - return DaoUtil.toPageData(notificationRepository.findByRecipientId(recipientId.getId(), + public PageData findByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink) { + return DaoUtil.toPageData(notificationRepository.findByDeliveryMethodAndRecipientId(deliveryMethod, recipientId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); } @@ -71,13 +72,8 @@ public class JpaNotificationDao extends JpaPartitionedAbstractDao findByRequestId(TenantId tenantId, NotificationRequestId notificationRequestId, PageLink pageLink) { - return DaoUtil.toPageData(notificationRepository.findByRequestId(notificationRequestId.getId(), DaoUtil.toPageable(pageLink))); + public int countUnreadByDeliveryMethodAndRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId) { + return notificationRepository.countByDeliveryMethodAndRecipientIdAndStatusNot(deliveryMethod, recipientId.getId(), NotificationStatus.READ); } @Override @@ -86,8 +82,8 @@ public class JpaNotificationDao extends JpaPartitionedAbstractDao { - @Query("SELECT n FROM NotificationEntity n WHERE n.recipientId = :recipientId AND n.status <> :status " + + @Query("SELECT n FROM NotificationEntity n WHERE n.deliveryMethod = :deliveryMethod " + + "AND n.recipientId = :recipientId AND n.status <> :status " + "AND (:searchText is NULL OR ilike(n.subject, concat('%', :searchText, '%')) = true " + "OR ilike(n.text, concat('%', :searchText, '%')) = true)") - Page findByRecipientIdAndStatusNot(@Param("recipientId") UUID recipientId, - @Param("status") NotificationStatus status, - @Param("searchText") String searchText, - Pageable pageable); + Page findByDeliveryMethodAndRecipientIdAndStatusNot(@Param("deliveryMethod") NotificationDeliveryMethod deliveryMethod, + @Param("recipientId") UUID recipientId, + @Param("status") NotificationStatus status, + @Param("searchText") String searchText, + Pageable pageable); - @Query("SELECT n FROM NotificationEntity n WHERE n.recipientId = :recipientId " + + @Query("SELECT n FROM NotificationEntity n WHERE n.deliveryMethod = :deliveryMethod AND n.recipientId = :recipientId " + "AND (:searchText is NULL OR ilike(n.subject, concat('%', :searchText, '%')) = true " + "OR ilike(n.text, concat('%', :searchText, '%')) = true)") - Page findByRecipientId(@Param("recipientId") UUID recipientId, - @Param("searchText") String searchText, - Pageable pageable); + Page findByDeliveryMethodAndRecipientId(@Param("deliveryMethod") NotificationDeliveryMethod deliveryMethod, + @Param("recipientId") UUID recipientId, + @Param("searchText") String searchText, + Pageable pageable); @Modifying @Transactional @@ -54,9 +58,7 @@ public interface NotificationRepository extends JpaRepository findByRequestId(UUID requestId, Pageable pageable); + int countByDeliveryMethodAndRecipientIdAndStatusNot(NotificationDeliveryMethod deliveryMethod, UUID recipientId, NotificationStatus status); @Transactional @Modifying @@ -76,8 +78,9 @@ public interface NotificationRepository extends JpaRepository :status") - int updateStatusByRecipientId(@Param("recipientId") UUID recipientId, - @Param("status") NotificationStatus status); + "WHERE n.deliveryMethod = :deliveryMethod AND n.recipientId = :recipientId AND n.status <> :status") + int updateStatusByDeliveryMethodAndRecipientIdAndStatusNot(@Param("deliveryMethod") NotificationDeliveryMethod deliveryMethod, + @Param("recipientId") UUID recipientId, + @Param("status") NotificationStatus status); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index d2b2567bd7..896fde6644 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -244,6 +244,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { entityTableMap.put(EntityType.DEVICE_PROFILE, "device_profile"); entityTableMap.put(EntityType.ASSET_PROFILE, "asset_profile"); entityTableMap.put(EntityType.TENANT_PROFILE, "tenant_profile"); + entityTableMap.put(EntityType.QUEUE_STATS, "queue_stats"); entityNameColumns.put(EntityType.DEVICE, "name"); entityNameColumns.put(EntityType.CUSTOMER, "title"); @@ -262,6 +263,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { entityNameColumns.put(EntityType.TB_RESOURCE, "search_text"); entityNameColumns.put(EntityType.EDGE, "name"); entityNameColumns.put(EntityType.QUEUE, "name"); + entityNameColumns.put(EntityType.QUEUE_STATS, "queue_name"); } public static EntityType[] RELATION_QUERY_ENTITY_TYPES = new EntityType[]{ diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index b21d3a8de5..df285657b3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -75,6 +75,8 @@ public class EntityKeyMapping { public static final String PHONE = "phone"; public static final String ADDITIONAL_INFO = "additionalInfo"; public static final String RELATED_PARENT_ID = "parentId"; + public static final String QUEUE_NAME = "queueName"; + public static final String SERVICE_ID = "serviceId"; public static final List typedEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, ADDITIONAL_INFO); public static final List widgetEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME); @@ -106,6 +108,7 @@ public class EntityKeyMapping { allowedEntityFieldMap.put(EntityType.API_USAGE_STATE, apiUsageStateEntityFields); allowedEntityFieldMap.put(EntityType.DEVICE_PROFILE, Set.of(CREATED_TIME, NAME, TYPE)); allowedEntityFieldMap.put(EntityType.ASSET_PROFILE, Set.of(CREATED_TIME, NAME)); + allowedEntityFieldMap.put(EntityType.QUEUE_STATS, new HashSet<>(Arrays.asList(CREATED_TIME, QUEUE_NAME, SERVICE_ID))); entityFieldColumnMap.put(CREATED_TIME, ModelConstants.CREATED_TIME_PROPERTY); entityFieldColumnMap.put(ENTITY_TYPE, ModelConstants.ENTITY_TYPE_PROPERTY); @@ -126,6 +129,8 @@ public class EntityKeyMapping { entityFieldColumnMap.put(PHONE, ModelConstants.PHONE_PROPERTY); entityFieldColumnMap.put(ADDITIONAL_INFO, ModelConstants.ADDITIONAL_INFO_PROPERTY); entityFieldColumnMap.put(RELATED_PARENT_ID, "parent_id"); + entityFieldColumnMap.put(QUEUE_NAME, ModelConstants.QUEUE_STATS_QUEUE_NAME_PROPERTY); + entityFieldColumnMap.put(SERVICE_ID, ModelConstants.QUEUE_STATS_SERVICE_ID_PROPERTY); Map contactBasedAliases = new HashMap<>(); contactBasedAliases.put(NAME, TITLE); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java new file mode 100644 index 0000000000..ac57d54e90 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2024 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.dao.sql.queue; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.QueueStats; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.QueueStatsEntity; +import org.thingsboard.server.dao.queue.QueueStatsDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; +import org.thingsboard.server.dao.util.SqlDao; + +import java.util.List; +import java.util.UUID; + +@Slf4j +@Component +@SqlDao +public class JpaQueueStatsDao extends JpaAbstractDao implements QueueStatsDao { + + @Autowired + private QueueStatsRepository queueStatsRepository; + + @Override + protected Class getEntityClass() { + return QueueStatsEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return queueStatsRepository; + } + + @Override + public QueueStats findByTenantIdQueueNameAndServiceId(TenantId tenantId, String queueName, String serviceId) { + return DaoUtil.getData(queueStatsRepository.findByTenantIdAndQueueNameAndServiceId(tenantId.getId(), queueName, serviceId)); + } + + @Override + public List findByTenantId(TenantId tenantId) { + return DaoUtil.convertDataList(queueStatsRepository.findByTenantId(tenantId.getId())); + } + + @Override + public void deleteByTenantId(TenantId tenantId) { + queueStatsRepository.deleteByTenantId(tenantId.getId()); + } + +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/QueueStatsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/QueueStatsRepository.java new file mode 100644 index 0000000000..65df3a65fa --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/QueueStatsRepository.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2024 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.dao.sql.queue; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.model.sql.QueueStatsEntity; + +import java.util.List; +import java.util.UUID; + +public interface QueueStatsRepository extends JpaRepository { + + QueueStatsEntity findByTenantIdAndQueueNameAndServiceId(UUID tenantId, String queueName, String serviceId); + + List findByTenantId(UUID tenantId); + + @Transactional + @Modifying + @Query("DELETE FROM QueueStatsEntity t WHERE t.tenantId = :tenantId") + void deleteByTenantId(@Param("tenantId") UUID tenantId); + +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java index 0f773e83d9..a222c1a3df 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java @@ -19,6 +19,8 @@ import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -50,8 +52,6 @@ import org.thingsboard.server.dao.sqlts.latest.TsKvLatestRepository; import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao; import org.thingsboard.server.dao.util.SqlTsLatestAnyDao; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -164,11 +164,6 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme return service.submit(() -> wrapNullTsKvEntry(key, doFindLatestSync(entityId, key))); } - @Override - public TsKvEntry findLatestSync(TenantId tenantId, EntityId entityId, String key) { - return wrapNullTsKvEntry(key, doFindLatestSync(entityId, key)); - } - @Override public ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId) { return getFindAllLatestFuture(entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java index ac7fb90024..7a6f5cd8f8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java @@ -51,6 +51,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId; public class TenantProfileServiceImpl extends AbstractCachedEntityService implements TenantProfileService { private static final String INCORRECT_TENANT_PROFILE_ID = "Incorrect tenantProfileId "; + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; @Autowired private TenantProfileDao tenantProfileDao; @@ -74,7 +75,7 @@ public class TenantProfileServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_PROFILE_ID + id); return cache.getAndPutInTransaction(TenantProfileCacheKey.fromId(tenantProfileId), () -> tenantProfileDao.findById(tenantId, tenantProfileId.getId()), true); } @@ -111,7 +112,8 @@ public class TenantProfileServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); + validateId(tenantProfileId, id -> INCORRECT_TENANT_PROFILE_ID + id); TenantProfile tenantProfile = tenantProfileDao.findById(tenantId, tenantProfileId.getId()); if (tenantProfile != null && tenantProfile.isDefault()) { throw new DataValidationException("Deletion of Default Tenant Profile is prohibited!"); @@ -186,7 +188,8 @@ public class TenantProfileServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); + validateId(tenantProfileId, id -> INCORRECT_TENANT_PROFILE_ID + id); TenantProfile tenantProfile = tenantProfileDao.findById(tenantId, tenantProfileId.getId()); if (!tenantProfile.isDefault()) { tenantProfile.setDefault(true); diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 72db458d66..8b3e65c927 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -49,6 +49,7 @@ import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; +import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rpc.RpcService; import org.thingsboard.server.dao.rule.RuleChainService; @@ -63,6 +64,7 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -131,6 +133,10 @@ public class TenantServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); return cache.getAndPutInTransaction(tenantId, () -> tenantDao.findById(tenantId, tenantId.getId()), true); } @@ -173,26 +179,26 @@ public class TenantServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); return tenantDao.findTenantInfoById(tenantId, tenantId.getId()); } @Override public ListenableFuture findTenantByIdAsync(TenantId callerId, TenantId tenantId) { log.trace("Executing findTenantByIdAsync [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); return tenantDao.findByIdAsync(callerId, tenantId.getId()); } @Override @Transactional public Tenant saveTenant(Tenant tenant) { - return saveTenant(tenant, true); + return saveTenant(tenant, null); } @Override @Transactional - public Tenant saveTenant(Tenant tenant, boolean publishSaveEvent) { + public Tenant saveTenant(Tenant tenant, Consumer defaultEntitiesCreator) { log.trace("Executing saveTenant [{}]", tenant); tenant.setRegion(DEFAULT_TENANT_REGION); if (tenant.getTenantProfileId() == null) { @@ -201,20 +207,20 @@ public class TenantServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); entityViewService.deleteEntityViewsByTenantId(tenantId); widgetsBundleService.deleteWidgetsBundlesByTenantId(tenantId); widgetTypeService.deleteWidgetTypesByTenantId(tenantId); @@ -259,6 +265,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService findLatestSync(TenantId tenantId, EntityId entityId, Collection keys) { - validate(entityId); - List latestEntries = new ArrayList<>(keys.size()); - keys.forEach(key -> Validator.validateString(key, k -> "Incorrect key " + k)); - for (String key : keys) { - latestEntries.add(timeseriesLatestDao.findLatestSync(tenantId, entityId, key)); - } - return latestEntries; - } - @Override public ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId) { validate(entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java index 342d4d9771..7a5904eb6b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java @@ -44,7 +44,6 @@ import org.thingsboard.server.dao.util.NoSqlTsLatestDao; import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.concurrent.ExecutionException; import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; @@ -70,16 +69,6 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes return findLatest(tenantId, entityId, key, rs -> convertResultToTsKvEntry(key, rs.one())); } - @Override - public TsKvEntry findLatestSync(TenantId tenantId, EntityId entityId, String key) { - try { - return findLatest(tenantId, entityId, key, rs -> convertResultToTsKvEntry(key, rs.one())).get(); - } catch (InterruptedException | ExecutionException e) { - log.error("[{}][{}] Failed to get latest entry for key: {} due to: ", tenantId, entityId, key, e); - throw new RuntimeException(e); - } - } - private ListenableFuture findLatest(TenantId tenantId, EntityId entityId, String key, java.util.function.Function function) { BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(getFindLatestStmt().bind()); stmtBuilder.setString(0, entityId.getEntityType().name()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java index aad4074f85..d339f49e11 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java @@ -40,8 +40,6 @@ public interface TimeseriesLatestDao { */ ListenableFuture findLatest(TenantId tenantId, EntityId entityId, String key); - TsKvEntry findLatestSync(TenantId tenantId, EntityId entityId, String key); - ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId); ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry); diff --git a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java index f4f24346d5..52195cf8db 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java @@ -72,14 +72,14 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A @Override public void deleteApiUsageStateByTenantId(TenantId tenantId) { log.trace("Executing deleteUsageRecordsByTenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); apiUsageStateDao.deleteApiUsageStateByTenantId(tenantId); } @Override public void deleteApiUsageStateByEntityId(EntityId entityId) { log.trace("Executing deleteApiUsageStateByEntityId [{}]", entityId); - validateId(entityId.getId(), "Invalid entity id"); + validateId(entityId.getId(), id -> "Invalid entity id " + id); apiUsageStateDao.deleteApiUsageStateByEntityId(entityId); } @@ -87,7 +87,7 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A public ApiUsageState createDefaultApiUsageState(TenantId tenantId, EntityId entityId) { entityId = Objects.requireNonNullElse(entityId, tenantId); log.trace("Executing createDefaultUsageRecord [{}]", entityId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); ApiUsageState apiUsageState = new ApiUsageState(); apiUsageState.setTenantId(tenantId); apiUsageState.setEntityId(entityId); @@ -142,7 +142,7 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A @Override public ApiUsageState update(ApiUsageState apiUsageState) { log.trace("Executing save [{}]", apiUsageState.getTenantId()); - validateId(apiUsageState.getTenantId(), INCORRECT_TENANT_ID + apiUsageState.getTenantId()); + validateId(apiUsageState.getTenantId(), id -> INCORRECT_TENANT_ID + id); validateId(apiUsageState.getId(), "Can't save new usage state. Only update is allowed!"); ApiUsageState savedState = apiUsageStateDao.save(apiUsageState.getTenantId(), apiUsageState); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedState.getTenantId()).entityId(savedState.getId()) @@ -153,21 +153,21 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A @Override public ApiUsageState findTenantApiUsageState(TenantId tenantId) { log.trace("Executing findTenantUsageRecord, tenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); return apiUsageStateDao.findTenantApiUsageState(tenantId.getId()); } @Override public ApiUsageState findApiUsageStateByEntityId(EntityId entityId) { - validateId(entityId.getId(), "Invalid entity id"); + validateId(entityId.getId(), id -> "Invalid entity id " + id); return apiUsageStateDao.findApiUsageStateByEntityId(entityId); } @Override public ApiUsageState findApiUsageStateById(TenantId tenantId, ApiUsageStateId id) { log.trace("Executing findApiUsageStateById, tenantId [{}], apiUsageStateId [{}]", tenantId, id); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(id, "Incorrect apiUsageStateId " + id); + validateId(tenantId, t -> INCORRECT_TENANT_ID + t); + validateId(id, u -> "Incorrect apiUsageStateId " + u); return apiUsageStateDao.findById(tenantId, id.getId()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index e989b02caa..4b804353ad 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -100,7 +100,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @Override public User findUserByEmail(TenantId tenantId, String email) { log.trace("Executing findUserByEmail [{}]", email); - validateString(email, "Incorrect email " + email); + validateString(email, e -> "Incorrect email " + e); if (userLoginCaseSensitive) { return userDao.findByEmail(tenantId, email); } else { @@ -111,22 +111,22 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @Override public User findUserByTenantIdAndEmail(TenantId tenantId, String email) { log.trace("Executing findUserByTenantIdAndEmail [{}][{}]", tenantId, email); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateString(email, "Incorrect email " + email); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateString(email, e -> "Incorrect email " + e); return userDao.findByTenantIdAndEmail(tenantId, email); } @Override public User findUserById(TenantId tenantId, UserId userId) { log.trace("Executing findUserById [{}]", userId); - validateId(userId, INCORRECT_USER_ID + userId); + validateId(userId, id -> INCORRECT_USER_ID + id); return userDao.findById(tenantId, userId.getId()); } @Override public ListenableFuture findUserByIdAsync(TenantId tenantId, UserId userId) { log.trace("Executing findUserByIdAsync [{}]", userId); - validateId(userId, INCORRECT_USER_ID + userId); + validateId(userId, id -> INCORRECT_USER_ID + id); return userDao.findByIdAsync(tenantId, userId.getId()); } @@ -159,21 +159,21 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @Override public UserCredentials findUserCredentialsByUserId(TenantId tenantId, UserId userId) { log.trace("Executing findUserCredentialsByUserId [{}]", userId); - validateId(userId, INCORRECT_USER_ID + userId); + validateId(userId, id -> INCORRECT_USER_ID + id); return userCredentialsDao.findByUserId(tenantId, userId.getId()); } @Override public UserCredentials findUserCredentialsByActivateToken(TenantId tenantId, String activateToken) { log.trace("Executing findUserCredentialsByActivateToken [{}]", activateToken); - validateString(activateToken, "Incorrect activateToken " + activateToken); + validateString(activateToken, t -> "Incorrect activateToken " + t); return userCredentialsDao.findByActivateToken(tenantId, activateToken); } @Override public UserCredentials findUserCredentialsByResetToken(TenantId tenantId, String resetToken) { log.trace("Executing findUserCredentialsByResetToken [{}]", resetToken); - validateString(resetToken, "Incorrect resetToken " + resetToken); + validateString(resetToken, t -> "Incorrect resetToken " + t); return userCredentialsDao.findByResetToken(tenantId, resetToken); } @@ -192,8 +192,8 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @Override public UserCredentials activateUserCredentials(TenantId tenantId, String activateToken, String password) { log.trace("Executing activateUserCredentials activateToken [{}], password [{}]", activateToken, password); - validateString(activateToken, "Incorrect activateToken " + activateToken); - validateString(password, "Incorrect password " + password); + validateString(activateToken, t -> "Incorrect activateToken " + t); + validateString(password, p -> "Incorrect password " + p); UserCredentials userCredentials = userCredentialsDao.findByActivateToken(tenantId, activateToken); if (userCredentials == null) { throw new IncorrectParameterException(String.format("Unable to find user credentials by activateToken [%s]", activateToken)); @@ -259,7 +259,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic Objects.requireNonNull(user, "User is null"); UserId userId = user.getId(); log.trace("[{}] Executing deleteUser [{}]", tenantId, userId); - validateId(userId, INCORRECT_USER_ID + userId); + validateId(userId, id -> INCORRECT_USER_ID + id); userCredentialsDao.removeByUserId(tenantId, userId); userAuthSettingsDao.removeByUserId(userId); deleteEntityRelations(tenantId, userId); @@ -276,7 +276,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @Override public PageData findUsersByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findUsersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validatePageLink(pageLink); return userDao.findByTenantId(tenantId.getId(), pageLink); } @@ -284,7 +284,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @Override public PageData findTenantAdmins(TenantId tenantId, PageLink pageLink) { log.trace("Executing findTenantAdmins, tenantId [{}], pageLink [{}]", tenantId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validatePageLink(pageLink); return userDao.findTenantAdmins(tenantId.getId(), pageLink); } @@ -317,15 +317,15 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @Override public void deleteTenantAdmins(TenantId tenantId) { log.trace("Executing deleteTenantAdmins, tenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); tenantAdminsRemover.removeEntities(tenantId, tenantId); } @Override public PageData findCustomerUsers(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findCustomerUsers, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, "Incorrect customerId " + customerId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> "Incorrect customerId " + id); validatePageLink(pageLink); return userDao.findCustomerUsers(tenantId.getId(), customerId.getId(), pageLink); } @@ -333,24 +333,24 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @Override public PageData findUsersByCustomerIds(TenantId tenantId, List customerIds, PageLink pageLink) { log.trace("Executing findTenantAndCustomerUsers, tenantId [{}], customerIds [{}], pageLink [{}]", tenantId, customerIds, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validatePageLink(pageLink); - customerIds.forEach(customerId -> {validateId(customerId, "Incorrect customerId " + customerId);}); + customerIds.forEach(customerId -> validateId(customerId, id -> "Incorrect customerId " + id)); return userDao.findUsersByCustomerIds(tenantId.getId(), customerIds, pageLink); } @Override public void deleteCustomerUsers(TenantId tenantId, CustomerId customerId) { log.trace("Executing deleteCustomerUsers, customerId [{}]", customerId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - validateId(customerId, "Incorrect customerId " + customerId); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> "Incorrect customerId " + id); customerUsersRemover.removeEntities(tenantId, customerId); } @Override public void setUserCredentialsEnabled(TenantId tenantId, UserId userId, boolean enabled) { log.trace("Executing setUserCredentialsEnabled [{}], [{}]", userId, enabled); - validateId(userId, INCORRECT_USER_ID + userId); + validateId(userId, id -> INCORRECT_USER_ID + id); UserCredentials userCredentials = userCredentialsDao.findByUserId(tenantId, userId.getId()); userCredentials.setEnabled(enabled); saveUserCredentials(tenantId, userCredentials); diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java index adc1ce3a9a..9ed18eac38 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java @@ -48,14 +48,14 @@ public class UserSettingsServiceImpl extends AbstractCachedService INCORRECT_USER_ID + id); return doSaveUserSettings(tenantId, userSettings); } @Override public void updateUserSettings(TenantId tenantId, UserId userId, UserSettingsType type, JsonNode settings) { log.trace("Executing updateUserSettings for user [{}], [{}]", userId, settings); - validateId(userId, INCORRECT_USER_ID + userId); + validateId(userId, id -> INCORRECT_USER_ID + id); var key = new UserSettingsCompositeKey(userId.getId(), type.name()); UserSettings oldSettings = userSettingsDao.findById(tenantId, key); @@ -71,7 +71,7 @@ public class UserSettingsServiceImpl extends AbstractCachedService INCORRECT_USER_ID + id); var key = new UserSettingsCompositeKey(userId.getId(), type.name()); return cache.getAndPutInTransaction(key, @@ -81,7 +81,7 @@ public class UserSettingsServiceImpl extends AbstractCachedService jsonPaths) { log.trace("Executing deleteUserSettings for user [{}]", userId); - validateId(userId, INCORRECT_USER_ID + userId); + validateId(userId, id -> INCORRECT_USER_ID + id); var key = new UserSettingsCompositeKey(userId.getId(), type.name()); UserSettings userSettings = userSettingsDao.findById(tenantId, key); if (userSettings == null) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java index 7f7d4d5ae8..5321e4ba1c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java @@ -71,21 +71,21 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { @Override public WidgetType findWidgetTypeById(TenantId tenantId, WidgetTypeId widgetTypeId) { log.trace("Executing findWidgetTypeById [{}]", widgetTypeId); - Validator.validateId(widgetTypeId, "Incorrect widgetTypeId " + widgetTypeId); + Validator.validateId(widgetTypeId, id -> "Incorrect widgetTypeId " + id); return widgetTypeDao.findWidgetTypeById(tenantId, widgetTypeId.getId()); } @Override public WidgetTypeDetails findWidgetTypeDetailsById(TenantId tenantId, WidgetTypeId widgetTypeId) { log.trace("Executing findWidgetTypeDetailsById [{}]", widgetTypeId); - Validator.validateId(widgetTypeId, "Incorrect widgetTypeId " + widgetTypeId); + Validator.validateId(widgetTypeId, id -> "Incorrect widgetTypeId " + id); return widgetTypeDao.findById(tenantId, widgetTypeId.getId()); } @Override public boolean widgetTypeExistsByTenantIdAndWidgetTypeId(TenantId tenantId, WidgetTypeId widgetTypeId) { log.trace("Executing widgetTypeExistsByTenantIdAndWidgetTypeId, tenantId [{}], widgetTypeId [{}]", tenantId, widgetTypeId); - Validator.validateId(widgetTypeId, "Incorrect widgetTypeId " + widgetTypeId); + Validator.validateId(widgetTypeId, id -> "Incorrect widgetTypeId " + id); return widgetTypeDao.existsByTenantIdAndId(tenantId, widgetTypeId.getId()); } @@ -110,7 +110,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { @Override public void deleteWidgetType(TenantId tenantId, WidgetTypeId widgetTypeId) { log.trace("Executing deleteWidgetType [{}]", widgetTypeId); - Validator.validateId(widgetTypeId, "Incorrect widgetTypeId " + widgetTypeId); + Validator.validateId(widgetTypeId, id -> "Incorrect widgetTypeId " + id); widgetTypeDao.removeById(tenantId, widgetTypeId.getId()); eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(widgetTypeId).build()); } @@ -126,7 +126,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { public PageData findAllTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink) { log.trace("Executing findAllTenantWidgetTypesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], deprecatedFilter [{}], widgetTypes [{}], pageLink [{}]", tenantId, fullSearch, deprecatedFilter, widgetTypes, pageLink); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); Validator.validatePageLink(pageLink); return widgetTypeDao.findAllTenantWidgetTypesByTenantId(tenantId.getId(), fullSearch, deprecatedFilter, widgetTypes, pageLink); } @@ -135,7 +135,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { public PageData findTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink) { log.trace("Executing findTenantWidgetTypesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], deprecatedFilter [{}], widgetTypes [{}], pageLink [{}]", tenantId, fullSearch, deprecatedFilter, widgetTypes, pageLink); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); Validator.validatePageLink(pageLink); return widgetTypeDao.findTenantWidgetTypesByTenantId(tenantId.getId(), fullSearch, deprecatedFilter, widgetTypes, pageLink); } @@ -143,16 +143,16 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { @Override public List findWidgetTypesByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId) { log.trace("Executing findWidgetTypesByWidgetsBundleId, tenantId [{}], widgetsBundleId [{}]", tenantId, widgetsBundleId); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validateId(widgetsBundleId, INCORRECT_WIDGETS_BUNDLE_ID + widgetsBundleId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + Validator.validateId(widgetsBundleId, id -> INCORRECT_WIDGETS_BUNDLE_ID + id); return widgetTypeDao.findWidgetTypesByWidgetsBundleId(tenantId.getId(), widgetsBundleId.getId()); } @Override public List findWidgetTypesDetailsByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId) { log.trace("Executing findWidgetTypesDetailsByWidgetsBundleId, tenantId [{}], widgetsBundleId [{}]", tenantId, widgetsBundleId); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validateId(widgetsBundleId, INCORRECT_WIDGETS_BUNDLE_ID + widgetsBundleId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + Validator.validateId(widgetsBundleId, id -> INCORRECT_WIDGETS_BUNDLE_ID + id); return widgetTypeDao.findWidgetTypesDetailsByWidgetsBundleId(tenantId.getId(), widgetsBundleId.getId()); } @@ -162,8 +162,8 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink) { log.trace("Executing findWidgetTypesInfosByWidgetsBundleId, tenantId [{}], widgetsBundleId [{}], fullSearch [{}], deprecatedFilter [{}], widgetTypes [{}], pageLink [{}]", tenantId, widgetsBundleId, fullSearch, deprecatedFilter, widgetTypes, pageLink); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validateId(widgetsBundleId, INCORRECT_WIDGETS_BUNDLE_ID + widgetsBundleId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + Validator.validateId(widgetsBundleId, id -> INCORRECT_WIDGETS_BUNDLE_ID + id); Validator.validatePageLink(pageLink); return widgetTypeDao.findWidgetTypesInfosByWidgetsBundleId(tenantId.getId(), widgetsBundleId.getId(), fullSearch, deprecatedFilter, widgetTypes, pageLink); } @@ -171,27 +171,27 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { @Override public List findWidgetFqnsByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId) { log.trace("Executing findWidgetTypesInfosByWidgetsBundleId, tenantId [{}], widgetsBundleId [{}]", tenantId, widgetsBundleId); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validateId(widgetsBundleId, INCORRECT_WIDGETS_BUNDLE_ID + widgetsBundleId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + Validator.validateId(widgetsBundleId, id -> INCORRECT_WIDGETS_BUNDLE_ID + id); return widgetTypeDao.findWidgetFqnsByWidgetsBundleId(tenantId.getId(), widgetsBundleId.getId()); } @Override public WidgetType findWidgetTypeByTenantIdAndFqn(TenantId tenantId, String fqn) { log.trace("Executing findWidgetTypeByTenantIdAndFqn, tenantId [{}], fqn [{}]", tenantId, fqn); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validateString(fqn, "Incorrect fqn " + fqn); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + Validator.validateString(fqn, f -> "Incorrect fqn " + f); return widgetTypeDao.findByTenantIdAndFqn(tenantId.getId(), fqn); } @Override public void updateWidgetsBundleWidgetTypes(TenantId tenantId, WidgetsBundleId widgetsBundleId, List widgetTypeIds) { log.trace("Executing updateWidgetsBundleWidgetTypes, tenantId [{}], widgetsBundleId [{}], widgetTypeIds [{}]", tenantId, widgetsBundleId, widgetTypeIds); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validateId(widgetsBundleId, INCORRECT_WIDGETS_BUNDLE_ID + widgetsBundleId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + Validator.validateId(widgetsBundleId, id -> INCORRECT_WIDGETS_BUNDLE_ID + id); Validator.checkNotNull(widgetTypeIds, "Incorrect widgetTypeIds " + widgetTypeIds); if (!widgetTypeIds.isEmpty()) { - validateIds(widgetTypeIds, "Incorrect widgetTypeIds " + widgetTypeIds); + validateIds(widgetTypeIds, ids -> "Incorrect widgetTypeIds " + ids); } List bundleWidgets = new ArrayList<>(); for (int index = 0; index < widgetTypeIds.size(); index++) { @@ -222,7 +222,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { @Override public void deleteWidgetTypesByTenantId(TenantId tenantId) { log.trace("Executing deleteWidgetTypesByTenantId, tenantId [{}]", tenantId); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); tenantWidgetTypeRemover.removeEntities(tenantId, tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java index 9d6960a7d1..2e67406df1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java @@ -66,7 +66,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { @Override public WidgetsBundle findWidgetsBundleById(TenantId tenantId, WidgetsBundleId widgetsBundleId) { log.trace("Executing findWidgetsBundleById [{}]", widgetsBundleId); - Validator.validateId(widgetsBundleId, "Incorrect widgetsBundleId " + widgetsBundleId); + Validator.validateId(widgetsBundleId, id -> "Incorrect widgetsBundleId " + id); return widgetsBundleDao.findById(tenantId, widgetsBundleId.getId()); } @@ -91,7 +91,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { @Override public void deleteWidgetsBundle(TenantId tenantId, WidgetsBundleId widgetsBundleId) { log.trace("Executing deleteWidgetsBundle [{}]", widgetsBundleId); - Validator.validateId(widgetsBundleId, "Incorrect widgetsBundleId " + widgetsBundleId); + Validator.validateId(widgetsBundleId, id -> "Incorrect widgetsBundleId " + id); WidgetsBundle widgetsBundle = findWidgetsBundleById(tenantId, widgetsBundleId); if (widgetsBundle == null) { throw new IncorrectParameterException("Unable to delete non-existent widgets bundle."); @@ -103,8 +103,8 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { @Override public WidgetsBundle findWidgetsBundleByTenantIdAndAlias(TenantId tenantId, String alias) { log.trace("Executing findWidgetsBundleByTenantIdAndAlias, tenantId [{}], alias [{}]", tenantId, alias); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validateString(alias, "Incorrect alias " + alias); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + Validator.validateString(alias, a -> "Incorrect alias " + a); return widgetsBundleDao.findWidgetsBundleByTenantIdAndAlias(tenantId.getId(), alias); } @@ -134,7 +134,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { @Override public PageData findTenantWidgetsBundlesByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findTenantWidgetsBundlesByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); Validator.validatePageLink(pageLink); return widgetsBundleDao.findTenantWidgetsBundlesByTenantId(tenantId.getId(), pageLink); } @@ -142,7 +142,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { @Override public PageData findAllTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink) { log.trace("Executing findAllTenantWidgetsBundlesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], pageLink [{}]", tenantId, fullSearch, pageLink); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); Validator.validatePageLink(pageLink); return widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId.getId(), fullSearch, pageLink); } @@ -150,7 +150,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { @Override public PageData findTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink) { log.trace("Executing findTenantWidgetsBundlesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], pageLink [{}]", tenantId, fullSearch, pageLink); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); Validator.validatePageLink(pageLink); return widgetsBundleDao.findTenantWidgetsBundlesByTenantId(tenantId.getId(), fullSearch, pageLink); } @@ -158,7 +158,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { @Override public List findAllTenantWidgetsBundlesByTenantId(TenantId tenantId) { log.trace("Executing findAllTenantWidgetsBundlesByTenantId, tenantId [{}]", tenantId); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); List widgetsBundles = new ArrayList<>(); PageLink pageLink = new PageLink(DEFAULT_WIDGETS_BUNDLE_LIMIT); PageData pageData; @@ -175,7 +175,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { @Override public void deleteWidgetsBundlesByTenantId(TenantId tenantId) { log.trace("Executing deleteWidgetsBundlesByTenantId, tenantId [{}]", tenantId); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); tenantWidgetsBundleRemover.removeEntities(tenantId, tenantId); } diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index a177c1325f..6b4f1a6c09 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -118,9 +118,9 @@ CREATE INDEX IF NOT EXISTS idx_notification_id ON notification(id); CREATE INDEX IF NOT EXISTS idx_notification_notification_request_id ON notification(request_id); -CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_created_time ON notification(recipient_id, created_time DESC); +CREATE INDEX IF NOT EXISTS idx_notification_delivery_method_recipient_id_created_time ON notification(delivery_method, recipient_id, created_time DESC); -CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ'; +CREATE INDEX IF NOT EXISTS idx_notification_delivery_method_recipient_id_unread ON notification(delivery_method, recipient_id) WHERE status <> 'READ'; CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 2dce04d67f..1199f45181 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -25,7 +25,7 @@ CREATE OR REPLACE PROCEDURE insert_tb_schema_settings() $$ BEGIN IF (SELECT COUNT(*) FROM tb_schema_settings) = 0 THEN - INSERT INTO tb_schema_settings (schema_version) VALUES (3006000); + INSERT INTO tb_schema_settings (schema_version) VALUES (3006004); END IF; END; $$; @@ -559,6 +559,7 @@ CREATE TABLE IF NOT EXISTS key_dictionary CREATE TABLE IF NOT EXISTS oauth2_params ( id uuid NOT NULL CONSTRAINT oauth2_params_pkey PRIMARY KEY, enabled boolean, + edge_enabled boolean, tenant_id uuid, created_time bigint NOT NULL ); @@ -863,6 +864,7 @@ CREATE TABLE IF NOT EXISTS notification ( request_id UUID, recipient_id UUID NOT NULL, type VARCHAR(50) NOT NULL, + delivery_method VARCHAR(50) NOT NULL, subject VARCHAR(255), body VARCHAR(1000) NOT NULL, additional_config VARCHAR(1000), @@ -883,3 +885,12 @@ CREATE TABLE IF NOT EXISTS alarm_types ( CONSTRAINT tenant_id_type_unq_key UNIQUE (tenant_id, type), CONSTRAINT fk_entity_tenant_id FOREIGN KEY (tenant_id) REFERENCES tenant(id) ON DELETE CASCADE ); + +CREATE TABLE IF NOT EXISTS queue_stats ( + id uuid NOT NULL CONSTRAINT queue_stats_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid NOT NULL, + queue_name varchar(255) NOT NULL, + service_id varchar(255) NOT NULL, + CONSTRAINT queue_stats_name_unq_key UNIQUE (tenant_id, queue_name, service_id) +); \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index e8232af692..5eddc78661 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -98,6 +98,10 @@ public abstract class AbstractServiceTest { protected RuleNodeDebugEvent generateEvent(TenantId tenantId, EntityId entityId) throws IOException { + return generateEvent(tenantId, entityId, null); + } + + protected RuleNodeDebugEvent generateEvent(TenantId tenantId, EntityId entityId, String eventType) throws IOException { if (tenantId == null) { tenantId = TenantId.fromUUID(Uuids.timeBased()); } @@ -105,6 +109,7 @@ public abstract class AbstractServiceTest { .tenantId(tenantId) .entityId(entityId.getId()) .serviceId("server A") + .eventType(eventType) .data(JacksonUtil.toString(readFromResource("TestJsonData.json"))) .build(); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java index 216d2abb07..d9b94fb26b 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java @@ -26,7 +26,6 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.ResultSetExtractor; 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.EntityType; import org.thingsboard.server.common.data.StringUtils; @@ -99,6 +98,8 @@ import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; +import static org.thingsboard.server.common.data.query.EntityKeyType.ATTRIBUTE; +import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD; @Slf4j @DaoSqlTest @@ -2113,6 +2114,132 @@ public class EntityServiceTest extends AbstractServiceTest { deviceService.deleteDevicesByTenantId(tenantId); } + @Test + public void testFindEntityQuery_for_5000_devices_with_3000_pageSize() { + int pageSize = 3000; + int expectedDevicesSize = 4000; + int unexpectedDevicesSize = 1000; + + for (int i = 0; i < expectedDevicesSize + unexpectedDevicesSize; i++) { + Device device = new Device(); + device.setTenantId(tenantId); + if (i < expectedDevicesSize) { + device.setName("Device_" + i); // match deviceNameFilter 'D%' + } else { + device.setName("Test_" + i); // does not match deviceNameFilter 'D%' + } + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + Device savedDevice = deviceService.saveDevice(device); + + attributesService.save(tenantId, savedDevice.getId(), AttributeScope.CLIENT_SCOPE, + new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("telemetry", (long) i))); + } + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("default")); + filter.setDeviceNameFilter("D%"); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder(new EntityKey(ATTRIBUTE, "telemetry"), EntityDataSortOrder.Direction.DESC); + + List deviceTypeFilters = createStringKeyFilters("type", ENTITY_FIELD, StringFilterPredicate.StringOperation.EQUAL, "default"); + + List attributeFilters = Collections.singletonList(createNumericKeyFilter("telemetry", ATTRIBUTE, NumericFilterPredicate.NumericOperation.LESS, expectedDevicesSize)); + + List nameFilters = createStringKeyFilters("name", ENTITY_FIELD, StringFilterPredicate.StringOperation.CONTAINS, "Device"); + + List entityFields = Arrays.asList(new EntityKey(ENTITY_FIELD, "name"), new EntityKey(ENTITY_FIELD, "type")); + + // 1. Device type filters: + + // query with textSearch - optimization is not performing + EntityDataPageLink originalPageLink = new EntityDataPageLink(pageSize, 0, "Device", sortOrder); + EntityDataQuery originalQuery = new EntityDataQuery(filter, originalPageLink, entityFields, null, deviceTypeFilters); + PageData originalData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), originalQuery); + + // query without textSearch - optimization is performing + EntityDataPageLink optimizedPageLink = new EntityDataPageLink(pageSize, 0, null, sortOrder); + EntityDataQuery optimizedQuery = new EntityDataQuery(filter, optimizedPageLink, entityFields, null, deviceTypeFilters); + PageData optimizedData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), optimizedQuery); + List loadedEntities = getLoadedEntities(optimizedData, optimizedQuery); + Assert.assertEquals(expectedDevicesSize, loadedEntities.size()); + loadedEntities = getLoadedEntities(originalData, originalQuery); + Assert.assertEquals(expectedDevicesSize, loadedEntities.size()); + Assert.assertEquals(pageSize, optimizedData.getData().size()); + + for (int i = 0; i < pageSize; i++) { + EntityData originalElement = originalData.getData().get(i); + EntityData optimizedElement = optimizedData.getData().get(i); + Assert.assertEquals(originalElement.getEntityId(), optimizedElement.getEntityId()); + originalElement.getLatest().get(ENTITY_FIELD).forEach((key, value) -> { + Assert.assertEquals(value.getValue(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getValue()); + Assert.assertEquals(value.getCount(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getCount()); + }); + } + Assert.assertEquals(originalData.getTotalPages(), optimizedData.getTotalPages()); + Assert.assertEquals(originalData.getTotalElements(), optimizedData.getTotalElements()); + + // 2. Device attribute filters + + // query with textSearch - optimization is not performing + originalPageLink = new EntityDataPageLink(pageSize, 0, "Device", sortOrder); + originalQuery = new EntityDataQuery(filter, originalPageLink, entityFields, null, attributeFilters); + originalData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), originalQuery); + + // query without textSearch - optimization is performing + optimizedPageLink = new EntityDataPageLink(pageSize, 0, null, sortOrder); + optimizedQuery = new EntityDataQuery(filter, optimizedPageLink, entityFields, null, attributeFilters); + optimizedData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), optimizedQuery); + loadedEntities = getLoadedEntities(optimizedData, optimizedQuery); + Assert.assertEquals(expectedDevicesSize, loadedEntities.size()); + loadedEntities = getLoadedEntities(originalData, originalQuery); + Assert.assertEquals(expectedDevicesSize, loadedEntities.size()); + Assert.assertEquals(pageSize, optimizedData.getData().size()); + + for (int i = 0; i < pageSize; i++) { + EntityData originalElement = originalData.getData().get(i); + EntityData optimizedElement = optimizedData.getData().get(i); + Assert.assertEquals(originalElement.getEntityId(), optimizedElement.getEntityId()); + originalElement.getLatest().get(ENTITY_FIELD).forEach((key, value) -> { + Assert.assertEquals(value.getValue(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getValue()); + Assert.assertEquals(value.getCount(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getCount()); + }); + } + Assert.assertEquals(originalData.getTotalPages(), optimizedData.getTotalPages()); + Assert.assertEquals(originalData.getTotalElements(), optimizedData.getTotalElements()); + + // 3. Device name filters + + // query with textSearch - optimization is not performing + originalPageLink = new EntityDataPageLink(pageSize, 0, "Device", sortOrder); + originalQuery = new EntityDataQuery(filter, originalPageLink, entityFields, null, nameFilters); + originalData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), originalQuery); + + // query without textSearch - optimization is performing + optimizedPageLink = new EntityDataPageLink(pageSize, 0, null, sortOrder); + optimizedQuery = new EntityDataQuery(filter, optimizedPageLink, entityFields, null, nameFilters); + optimizedData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), optimizedQuery); + loadedEntities = getLoadedEntities(optimizedData, optimizedQuery); + Assert.assertEquals(expectedDevicesSize, loadedEntities.size()); + loadedEntities = getLoadedEntities(originalData, originalQuery); + Assert.assertEquals(expectedDevicesSize, loadedEntities.size()); + Assert.assertEquals(pageSize, optimizedData.getData().size()); + + for (int i = 0; i < pageSize; i++) { + EntityData originalElement = originalData.getData().get(i); + EntityData optimizedElement = optimizedData.getData().get(i); + Assert.assertEquals(originalElement.getEntityId(), optimizedElement.getEntityId()); + originalElement.getLatest().get(ENTITY_FIELD).forEach((key, value) -> { + Assert.assertEquals(value.getValue(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getValue()); + Assert.assertEquals(value.getCount(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getCount()); + }); + } + Assert.assertEquals(originalData.getTotalPages(), optimizedData.getTotalPages()); + Assert.assertEquals(originalData.getTotalElements(), optimizedData.getTotalElements()); + + deviceService.deleteDevicesByTenantId(tenantId); + } + private Boolean listEqualWithoutOrder(List A, List B) { return A.containsAll(B) && B.containsAll(A); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/OAuth2ServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/OAuth2ServiceTest.java index ae9aa72be9..281709c9af 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/OAuth2ServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/OAuth2ServiceTest.java @@ -47,7 +47,7 @@ import java.util.stream.Collectors; @DaoSqlTest public class OAuth2ServiceTest extends AbstractServiceTest { - private static final OAuth2Info EMPTY_PARAMS = new OAuth2Info(false, Collections.emptyList()); + private static final OAuth2Info EMPTY_PARAMS = new OAuth2Info(false, false, Collections.emptyList()); @Autowired protected OAuth2Service oAuth2Service; @@ -66,7 +66,7 @@ public class OAuth2ServiceTest extends AbstractServiceTest { @Test public void testSaveHttpAndMixedDomainsTogether() { - OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2Info oAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), @@ -87,7 +87,7 @@ public class OAuth2ServiceTest extends AbstractServiceTest { @Test public void testSaveHttpsAndMixedDomainsTogether() { - OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2Info oAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTPS).build(), @@ -153,7 +153,7 @@ public class OAuth2ServiceTest extends AbstractServiceTest { Assert.assertNotNull(foundOAuth2Info); Assert.assertEquals(oAuth2Info, foundOAuth2Info); - OAuth2Info newOAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2Info newOAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( OAuth2DomainInfo.builder().name("another-domain").scheme(SchemeType.HTTPS).build() @@ -194,7 +194,7 @@ public class OAuth2ServiceTest extends AbstractServiceTest { List thirdGroup = Lists.newArrayList( validRegistrationInfo() ); - OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2Info oAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), @@ -318,7 +318,7 @@ public class OAuth2ServiceTest extends AbstractServiceTest { validRegistrationInfo(), validRegistrationInfo() ); - OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2Info oAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), @@ -363,7 +363,7 @@ public class OAuth2ServiceTest extends AbstractServiceTest { @Test public void testGetDisabledOAuth2Clients() { - OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2Info oAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), @@ -402,7 +402,7 @@ public class OAuth2ServiceTest extends AbstractServiceTest { @Test public void testFindAllRegistrations() { - OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2Info oAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), @@ -451,7 +451,7 @@ public class OAuth2ServiceTest extends AbstractServiceTest { @Test public void testFindRegistrationById() { - OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2Info oAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), @@ -495,7 +495,7 @@ public class OAuth2ServiceTest extends AbstractServiceTest { @Test public void testFindAppSecret() { - OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2Info oAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), @@ -547,7 +547,7 @@ public class OAuth2ServiceTest extends AbstractServiceTest { @Test public void testFindClientsByPackageAndPlatform() { - OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2Info oAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), @@ -596,7 +596,7 @@ public class OAuth2ServiceTest extends AbstractServiceTest { } private OAuth2Info createDefaultOAuth2Info() { - return new OAuth2Info(true, Lists.newArrayList( + return new OAuth2Info(true, false, Lists.newArrayList( OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), @@ -664,4 +664,5 @@ public class OAuth2ServiceTest extends AbstractServiceTest { .appSecret(appSecret != null ? appSecret : StringUtils.randomAlphanumeric(24)) .build(); } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java new file mode 100644 index 0000000000..45a49ba5d2 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java @@ -0,0 +1,134 @@ +/** + * Copyright © 2016-2024 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.dao.service; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.QueueStats; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.queue.QueueStatsService; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + + +@DaoSqlTest +public class QueueStatsServiceTest extends AbstractServiceTest { + + @Autowired + QueueStatsService queueStatsService; + + private TenantId tenantId; + + @Before + public void before() throws NoSuchFieldException, IllegalAccessException { + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = tenantService.saveTenant(tenant); + Assert.assertNotNull(savedTenant); + tenantId = savedTenant.getId(); + } + + @After + public void after() { + tenantService.deleteTenant(tenantId); + } + + @Test + public void testSaveQueueStats() { + QueueStats queueStats = new QueueStats(); + queueStats.setTenantId(tenantId); + String queueName = StringUtils.randomAlphabetic(8); + queueStats.setQueueName(queueName); + queueStats.setServiceId(StringUtils.randomAlphabetic(8)); + + QueueStats savedQueueStats = queueStatsService.save(tenantId, queueStats); + Assert.assertNotNull(savedQueueStats); + Assert.assertNotNull(savedQueueStats.getId()); + Assert.assertTrue(savedQueueStats.getCreatedTime() > 0); + Assert.assertEquals(queueStats.getTenantId(), savedQueueStats.getTenantId()); + Assert.assertEquals(savedQueueStats.getQueueName(), queueStats.getQueueName()); + + QueueStats retrievedQueueStatsById = queueStatsService.findQueueStatsById(tenantId, savedQueueStats.getId()); + Assert.assertEquals(retrievedQueueStatsById.getQueueName(), queueName); + + String secondQueueName = StringUtils.randomAlphabetic(8); + queueStats.setQueueName(secondQueueName); + QueueStats savedQueueStats2 = queueStatsService.save(tenantId, queueStats); + QueueStats retrievedQueueStatsById2 = queueStatsService.findQueueStatsById(tenantId, savedQueueStats2.getId()); + Assert.assertEquals(retrievedQueueStatsById2.getQueueName(), secondQueueName); + + List queueStatsList = queueStatsService.findByTenantId(tenantId); + Assert.assertEquals(2, queueStatsList.size()); + assertThat(queueStatsList).containsOnly(retrievedQueueStatsById, retrievedQueueStatsById2); + + queueStatsService.deleteByTenantId(tenantId); + QueueStats retrievedQueueStatsAfterDelete = queueStatsService.findQueueStatsById(tenantId, savedQueueStats.getId()); + Assert.assertNull(retrievedQueueStatsAfterDelete); + } + + @Test + public void testSaveWithNullQueueName() { + QueueStats queueStats = new QueueStats(); + queueStats.setTenantId(tenantId); + queueStats.setQueueName(null); + queueStats.setServiceId(StringUtils.randomAlphabetic(8)); + + Assertions.assertThrows(DataValidationException.class, () -> { + queueStatsService.save(tenantId, queueStats); + }); + } + + @Test + public void testSaveWithNullServiceId() { + QueueStats queueStats = new QueueStats(); + queueStats.setTenantId(tenantId); + queueStats.setQueueName(StringUtils.randomAlphabetic(8)); + queueStats.setServiceId(null); + + Assertions.assertThrows(DataValidationException.class, () -> { + queueStatsService.save(tenantId, queueStats); + }); + } + + @Test + public void testFindByTenantIdAndNameAndServiceId() { + QueueStats queueStats = new QueueStats(); + queueStats.setTenantId(tenantId); + queueStats.setQueueName(StringUtils.randomAlphabetic(8)); + queueStats.setServiceId(StringUtils.randomAlphabetic(8)); + QueueStats savedQueueStats = queueStatsService.save(tenantId, queueStats); + + QueueStats queueStats2 = new QueueStats(); + queueStats2.setTenantId(tenantId); + queueStats2.setQueueName(StringUtils.randomAlphabetic(8)); + queueStats2.setServiceId(StringUtils.randomAlphabetic(8)); + queueStatsService.save(tenantId, queueStats2); + + QueueStats retrievedQueueStatsById = queueStatsService.findByTenantIdAndNameAndServiceId(tenantId, queueStats.getQueueName(), queueStats.getServiceId()); + assertThat(retrievedQueueStatsById).isEqualTo(savedQueueStats); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java index 50f46020d8..86b76bd5a5 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java @@ -40,8 +40,11 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.UUID; import java.util.function.Function; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + /** * Created by igor on 3/13/18. */ @@ -560,4 +563,35 @@ public class RuleChainServiceTest extends AbstractServiceTest { Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } + + @Test + public void testSaveRuleChainWithExistingExternalId() { + RuleChainId externalRuleChainId = new RuleChainId(UUID.fromString("2675d180-e1e5-11ee-9f06-71b6c7dc2cbf")); + + RuleChain ruleChain = getRuleChain(); + ruleChain.setTenantId(tenantId); + ruleChain.setExternalId(externalRuleChainId); + ruleChainService.saveRuleChain(ruleChain); + + assertThatThrownBy(() -> ruleChainService.saveRuleChain(ruleChain)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Rule Chain with such external id already exists!"); + + ruleChainService.deleteRuleChainsByTenantId(tenantId); + } + + private RuleChain getRuleChain() { + String ruleChainStr = "{\n" + + " \"name\": \"Root Rule Chain\",\n" + + " \"type\": \"CORE\",\n" + + " \"firstRuleNodeId\": {\n" + + " \"entityType\": \"RULE_NODE\",\n" + + " \"id\": \"91ad0b00-e779-11ee-9cf0-15d8b6079fdb\"\n" + + " },\n" + + " \"debugMode\": false,\n" + + " \"configuration\": null,\n" + + " \"additionalInfo\": null\n" + + "}"; + return JacksonUtil.fromString(ruleChainStr, RuleChain.class); + } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/ValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/ValidatorTest.java new file mode 100644 index 0000000000..4242cb69ae --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/ValidatorTest.java @@ -0,0 +1,130 @@ +/** + * Copyright © 2016-2024 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.dao.service; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.dao.exception.IncorrectParameterException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ValidatorTest { + + final DeviceId goodDeviceId = new DeviceId(UUID.fromString("18594c15-9f05-4cda-b58e-70172467c3e5")); + final UserId nullUserId = new UserId(null); + + @Test + void validateEntityIdTest() { + Validator.validateEntityId(TenantId.SYS_TENANT_ID, id -> "Incorrect entityId " + id); + Validator.validateEntityId(goodDeviceId, id -> "Incorrect entityId " + id); + + assertThatThrownBy(() -> Validator.validateEntityId(null, id -> "Incorrect entityId " + id)) + .as("EntityId is null") + .isInstanceOf(IncorrectParameterException.class) + .hasMessageContaining("Incorrect entityId null"); + + assertThatThrownBy(() -> Validator.validateEntityId(nullUserId, id -> "Incorrect entityId " + id)) + .as("EntityId with null UUID") + .isInstanceOf(IncorrectParameterException.class) + .hasMessageContaining("Incorrect entityId null"); + } + + @Test + void validateStringTest() { + Validator.validateString("Hello", s -> "Incorrect string " + s); + Validator.validateString(" ", s -> "Incorrect string " + s); + Validator.validateString("\n", s -> "Incorrect string " + s); + + assertThatThrownBy(() -> Validator.validateString(null, s -> "Incorrect string " + s)) + .as("String is null") + .isInstanceOf(IncorrectParameterException.class) + .hasMessageContaining("Incorrect string null"); + + assertThatThrownBy(() -> Validator.validateString("", s -> "Incorrect string " + s)) + .as("String is empty") + .isInstanceOf(IncorrectParameterException.class) + .hasMessage("Incorrect string "); + + assertThatThrownBy(() -> Validator.validateString("", s -> "Incorrect string [" + s + "]")) + .as("String is empty []") + .isInstanceOf(IncorrectParameterException.class) + .hasMessage("Incorrect string []"); + } + + @Test + void validateUUIDIdTest() { + Validator.validateId(UUID.randomUUID(), id -> "Incorrect Id " + id); + + assertThatThrownBy(() -> Validator.validateId((UUID) null, id -> "Incorrect Id " + id)) + .as("Id is null") + .isInstanceOf(IncorrectParameterException.class) + .hasMessageContaining("Incorrect Id null"); + } + + @Test + void validateUUIDBasedIdTest() { + Validator.validateId(TenantId.SYS_TENANT_ID, id -> "Incorrect Id " + id); + Validator.validateId(goodDeviceId, id -> "Incorrect Id " + id); + + assertThatThrownBy(() -> Validator.validateId((UUIDBased) null, id -> "Incorrect Id " + id)) + .as("Id is null") + .isInstanceOf(IncorrectParameterException.class) + .hasMessageContaining("Incorrect Id null"); + + assertThatThrownBy(() -> Validator.validateId(nullUserId, id -> "Incorrect Id " + id)) + .as("Id with null UUIDBased") + .isInstanceOf(IncorrectParameterException.class) + .hasMessageContaining("Incorrect Id null"); + + } + + @Test + void validateIdsTest() { + List list = List.of(goodDeviceId); + Validator.validateIds(list, ids -> "Incorrect Id " + ids); + + assertThatThrownBy(() -> Validator.validateIds(null, id -> "Incorrect Ids " + id)) + .as("Ids are null") + .isInstanceOf(IncorrectParameterException.class) + .hasMessageContaining("Incorrect Ids null"); + + assertThatThrownBy(() -> Validator.validateIds(Collections.emptyList(), ids -> "Incorrect Ids " + ids)) + .as("List is empty") + .isInstanceOf(IncorrectParameterException.class) + .hasMessageContaining("Incorrect Ids []"); + + List badList = new ArrayList<>(2); + badList.add(goodDeviceId); + badList.add(null); + + // Incorrect Ids [18594c15-9f05-4cda-b58e-70172467c3e5, null] + assertThatThrownBy(() -> Validator.validateIds(badList, ids -> "Incorrect Ids " + ids)) + .as("List contains null") + .isInstanceOf(IncorrectParameterException.class) + .hasMessageContaining("Incorrect Ids ") + .hasMessageContaining(goodDeviceId.getId().toString()) + .hasMessageContaining("null"); + + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java index b73728d314..8aa943484d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java @@ -132,8 +132,28 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest { eventService.cleanupEvents(timeBeforeStartTime - 1, timeAfterEndTime + 1, true); } + @Test + public void findLatestDebugRuleNodeInEvent() throws Exception { + CustomerId customerId = new CustomerId(Uuids.timeBased()); + TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); + + Event event1 = saveEventWithProvidedTimeAndEventType(eventTime, "IN", customerId, tenantId); + Event event2 = saveEventWithProvidedTimeAndEventType(eventTime + 1, "IN", customerId, tenantId); + + EventInfo event = eventService.findLatestDebugRuleNodeInEvent(tenantId, customerId); + + Assert.assertNotNull(event); + Assert.assertEquals(event2.getUuidId(), event.getUuidId()); + + eventService.cleanupEvents(timeBeforeStartTime - 1, timeAfterEndTime + 1, true); + } + private Event saveEventWithProvidedTime(long time, EntityId entityId, TenantId tenantId) throws Exception { - RuleNodeDebugEvent event = generateEvent(tenantId, entityId); + return saveEventWithProvidedTimeAndEventType(time, null, entityId, tenantId); + } + + private Event saveEventWithProvidedTimeAndEventType(long time, String eventType, EntityId entityId, TenantId tenantId) throws Exception { + RuleNodeDebugEvent event = generateEvent(tenantId, entityId, eventType); event.setId(new EventId(Uuids.timeBased())); event.setCreatedTime(time); eventService.saveAsync(event).get(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java index 5c908fa1b4..b178d7c78f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.AbstractJpaDaoTest; import org.thingsboard.server.dao.event.EventDao; +import org.thingsboard.server.dao.model.sql.RuleNodeDebugEventEntity; import java.util.List; import java.util.UUID; @@ -42,6 +43,10 @@ public class JpaBaseEventDaoTest extends AbstractJpaDaoTest { @Autowired private EventDao eventDao; + + @Autowired + private RuleNodeDebugEventRepository ruleNodeDebugEventRepository; + UUID tenantId = Uuids.timeBased(); @@ -106,6 +111,23 @@ public class JpaBaseEventDaoTest extends AbstractJpaDaoTest { } + @Test + public void findLatestDebugRuleNodeInEvent() throws Exception { + + UUID entityId = Uuids.timeBased(); + + RuleNodeDebugEventEntity event = getDebugInEventEntity(Uuids.timeBased(), tenantId, entityId); + eventDao.saveAsync(event.toData()).get(1, TimeUnit.MINUTES); + Thread.sleep(2); + RuleNodeDebugEventEntity event2 = getDebugInEventEntity(Uuids.timeBased(), tenantId, entityId); + eventDao.saveAsync(event2.toData()).get(1, TimeUnit.MINUTES); + + RuleNodeDebugEventEntity foundEvent = ruleNodeDebugEventRepository.findLatestDebugRuleNodeInEvent(tenantId, entityId).get(); + assertNotNull("Events expected to be not null", foundEvent); + assertEquals(event2.getEventType(), foundEvent.getEventType()); + assertEquals(event2.getId(), foundEvent.getId()); + } + private Event getStatsEvent(UUID eventId, UUID tenantId, UUID entityId) { StatisticsEvent.StatisticsEventBuilder event = StatisticsEvent.builder(); event.id(eventId); @@ -117,4 +139,13 @@ public class JpaBaseEventDaoTest extends AbstractJpaDaoTest { event.errorsOccurred(0); return event.build(); } + + private RuleNodeDebugEventEntity getDebugInEventEntity(UUID eventId, UUID tenantId, UUID entityId) { + RuleNodeDebugEventEntity event = new RuleNodeDebugEventEntity(); + event.setId(eventId); + event.setEventType("IN"); + event.setEntityId(entityId); + event.setTenantId(tenantId); + return event; + } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java index acff0902f1..e8f3bba9db 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java @@ -21,6 +21,7 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.page.PageData; @@ -43,6 +44,7 @@ import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; @@ -160,6 +162,30 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest { assertEquals(new WidgetTypeInfo(widgetTypeList.get(2)), widgetTypes.getData().get(0)); } + @Test + public void testFindSystemWidgetTypesForSameName() throws InterruptedException { + List sameNameList = new ArrayList<>(); + + for (int i = 0; i < 20; i++) { + Thread.sleep(2); + var widgetType = saveWidgetType(TenantId.SYS_TENANT_ID, "widgetName"); + sameNameList.add(widgetType); + widgetTypeList.add(widgetType); + } + sameNameList.sort(Comparator.comparing(BaseWidgetType::getName).thenComparing((BaseWidgetType baseWidgetType) -> baseWidgetType.getId().getId())); + List expected = sameNameList.stream().map(WidgetTypeInfo::new).collect(Collectors.toList()); + + PageData widgetTypesFirstPage = widgetTypeDao.findSystemWidgetTypes(TenantId.SYS_TENANT_ID, true, DeprecatedFilter.ALL, Collections.singletonList("static"), + new PageLink(10, 0, null, new SortOrder("name"))); + assertEquals(10, widgetTypesFirstPage.getData().size()); + assertThat(widgetTypesFirstPage.getData()).containsExactlyElementsOf(expected.subList(0, 10)); + + PageData widgetTypesSecondPage = widgetTypeDao.findSystemWidgetTypes(TenantId.SYS_TENANT_ID, true, DeprecatedFilter.ALL, Collections.singletonList("static"), + new PageLink(10, 1, null, new SortOrder("name"))); + assertEquals(10, widgetTypesSecondPage.getData().size()); + assertThat(widgetTypesSecondPage.getData()).containsExactlyElementsOf(expected.subList(10, 20)); + } + @Test public void testTagsSearchInFindBySystemWidgetTypes() { for (var entry : SHOULD_FIND_SEARCH_TO_TAGS_MAP.entrySet()) { @@ -355,4 +381,15 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest { assertEquals(0, result.size()); widgetTypeDao.removeById(tenantId, details.getUuidId()); } + + private WidgetTypeDetails saveWidgetType(TenantId tenantId, String name) { + WidgetTypeDetails widgetType = new WidgetTypeDetails(); + widgetType.setTenantId(tenantId); + widgetType.setDescription("WIDGET_TYPE_DESCRIPTION" + StringUtils.randomAlphabetic(7)); + widgetType.setName(name); + var descriptor = JacksonUtil.newObjectNode(); + descriptor.put("type","static"); + widgetType.setDescriptor(descriptor); + return widgetTypeDao.save(TenantId.SYS_TENANT_ID, widgetType); + } } diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 9ed8e015f1..18e80b425d 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -220,6 +220,7 @@ services: image: "${DOCKER_REPO}/${LWM2M_TRANSPORT_DOCKER_NAME}:${TB_VERSION}" ports: - "5685:5685/udp" + - "5686:5686/udp" environment: TB_SERVICE_ID: tb-lwm2m-transport JAVA_OPTS: "${JAVA_OPTS}" diff --git a/docker/haproxy/config/blocklist.txt b/docker/haproxy/config/blocklist.txt new file mode 100644 index 0000000000..ff9429857f --- /dev/null +++ b/docker/haproxy/config/blocklist.txt @@ -0,0 +1,3 @@ +# Blocked subnets and IPs. Use CIDR or IP by one per line +5.136.0.0/13 +217.199.254.1 diff --git a/docker/haproxy/config/haproxy.cfg b/docker/haproxy/config/haproxy.cfg index aa752502bf..757efe692f 100644 --- a/docker/haproxy/config/haproxy.cfg +++ b/docker/haproxy/config/haproxy.cfg @@ -41,6 +41,15 @@ listen stats listen mqtt-in bind *:${MQTT_PORT} mode tcp + + stick-table type ip size 60k expire 60s store conn_cur + + acl trustlist src -f /config/trustlist.txt + acl blocklist src -f /config/blocklist.txt + tcp-request connection accept if trustlist + tcp-request connection reject if blocklist or { src_conn_cur ge 50 } + tcp-request connection track-sc1 src + option clitcpka # For TCP keep-alive timeout client 3h timeout server 3h @@ -52,6 +61,15 @@ listen mqtt-in listen edges-rpc-in bind *:${EDGES_RPC_PORT} mode tcp + + stick-table type ip size 60k expire 60s store conn_cur + + acl trustlist src -f /config/trustlist.txt + acl blocklist src -f /config/blocklist.txt + tcp-request connection accept if trustlist + tcp-request connection reject if blocklist or { src_conn_cur ge 5 } + tcp-request connection track-sc1 src + option clitcpka # For TCP keep-alive timeout client 3h timeout server 3h @@ -63,18 +81,28 @@ listen edges-rpc-in frontend http-in bind *:${HTTP_PORT} alpn h2,http/1.1 + stick-table type ip size 60k expire 60s store conn_cur + + acl trustlist src -f /config/trustlist.txt + acl blocklist src -f /config/blocklist.txt + tcp-request connection accept if trustlist + tcp-request connection reject if blocklist or { src_conn_cur ge 50 } + tcp-request connection track-sc1 src + option forwardfor http-request add-header "X-Forwarded-Proto" "http" acl transport_http_acl path_beg /api/v1/ acl letsencrypt_http_acl path_beg /.well-known/acme-challenge/ + acl tb_images_api_acl path_beg /api/images/ acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /v3/ /static/rulenode/ /oauth2/ /login/oauth2/ /static/widgets/ redirect scheme https if !letsencrypt_http_acl !transport_http_acl { env(FORCE_HTTPS_REDIRECT) -m str true } use_backend letsencrypt_http if letsencrypt_http_acl use_backend tb-http-backend if transport_http_acl + use_backend tb-images-api-backend if tb_images_api_acl use_backend tb-api-backend if tb_api_acl default_backend tb-web-backend @@ -82,14 +110,24 @@ frontend http-in frontend https_in bind *:${HTTPS_PORT} ssl crt /usr/local/etc/haproxy/default.pem crt /usr/local/etc/haproxy/certs.d ciphers ECDHE-RSA-AES256-SHA:RC4-SHA:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM alpn h2,http/1.1 + stick-table type ip size 60k expire 60s store conn_cur + + acl trustlist src -f /config/trustlist.txt + acl blocklist src -f /config/blocklist.txt + tcp-request connection accept if trustlist + tcp-request connection reject if blocklist or { src_conn_cur ge 50 } + tcp-request connection track-sc1 src + option forwardfor http-request add-header "X-Forwarded-Proto" "https" acl transport_http_acl path_beg /api/v1/ + acl tb_images_api_acl path_beg /api/images/ acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /v3/ /static/rulenode/ /oauth2/ /login/oauth2/ /static/widgets/ use_backend tb-http-backend if transport_http_acl + use_backend tb-images-api-backend if tb_images_api_acl use_backend tb-api-backend if tb_api_acl default_backend tb-web-backend @@ -98,24 +136,76 @@ backend letsencrypt_http server letsencrypt_http_srv 127.0.0.1:8080 backend tb-web-backend + timeout queue 60s balance leastconn option tcp-check option log-health-checks - server tbWeb1 tb-web-ui1:8080 check inter 5s resolvers docker_resolver resolve-prefer ipv4 - server tbWeb2 tb-web-ui2:8080 check inter 5s resolvers docker_resolver resolve-prefer ipv4 + server tbWeb1 tb-web-ui1:8080 check inter 5s resolvers docker_resolver resolve-prefer ipv4 maxconn 50 + server tbWeb2 tb-web-ui2:8080 check inter 5s resolvers docker_resolver resolve-prefer ipv4 maxconn 50 http-request set-header X-Forwarded-Port %[dst_port] backend tb-http-backend + timeout queue 60s balance leastconn option tcp-check option log-health-checks - server tbHttp1 tb-http-transport1:8081 check inter 5s resolvers docker_resolver resolve-prefer ipv4 - server tbHttp2 tb-http-transport2:8081 check inter 5s resolvers docker_resolver resolve-prefer ipv4 + server tbHttp1 tb-http-transport1:8081 check inter 5s resolvers docker_resolver resolve-prefer ipv4 maxconn 50 + server tbHttp2 tb-http-transport2:8081 check inter 5s resolvers docker_resolver resolve-prefer ipv4 maxconn 50 + +# Dummy backends for a stick-table purpose only. +# There is only one stick-table per proxy. At the moment of writing this doc, +# it does not seem useful to have multiple tables per proxy. If this happens +# to be required, simply create a dummy backend with a stick-table in it and +# reference it +# https://www.haproxy.com/documentation/haproxy-configuration-manual/latest/#stick-table +backend st_src_rate10s + stick-table type ip size 60k expire 10s store http_req_rate(10s) + +backend st_src_rate1m + stick-table type ip size 60k expire 1m store http_req_rate(1m) backend tb-api-backend + timeout queue 60s balance source option tcp-check option log-health-checks - server tbApi1 tb-core1:8080 check inter 5s resolvers docker_resolver resolve-prefer ipv4 - server tbApi2 tb-core2:8080 check inter 5s resolvers docker_resolver resolve-prefer ipv4 + + http-request track-sc0 src table st_src_rate10s + http-request track-sc1 src table st_src_rate1m + + acl trustlist src -f /config/trustlist.txt + http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 } !trustlist + http-request deny deny_status 429 if { sc_http_req_rate(1) gt 300 } !trustlist + + http-request set-header X-Forwarded-Port %[dst_port] + server tbApi1 tb-core1:8080 check inter 5s resolvers docker_resolver resolve-prefer ipv4 maxconn 50 + server tbApi2 tb-core2:8080 check inter 5s resolvers docker_resolver resolve-prefer ipv4 maxconn 50 + +# Dummy backends for a stick-table purpose only. +# There is only one stick-table per proxy. At the moment of writing this doc, +# it does not seem useful to have multiple tables per proxy. If this happens +# to be required, simply create a dummy backend with a stick-table in it and +# reference it +# https://www.haproxy.com/documentation/haproxy-configuration-manual/latest/#stick-table +backend st_images_src_rate10s + stick-table type ip size 60k expire 10s store http_req_rate(10s) + +backend st_images_src_rate1m + stick-table type ip size 60k expire 1m store http_req_rate(1m) + +backend tb-images-api-backend + timeout queue 60s + balance source + option tcp-check + option log-health-checks + + http-request track-sc0 src table st_images_src_rate10s + http-request track-sc1 src table st_images_src_rate1m + + acl trustlist src -f /config/trustlist.txt + http-request deny deny_status 429 if { sc_http_req_rate(0) gt 1000 } !trustlist + http-request deny deny_status 429 if { sc_http_req_rate(1) gt 3000 } !trustlist + http-request set-header X-Forwarded-Port %[dst_port] + server tbImagesApi1 tb-core1:8080 check inter 10s resolvers docker_resolver resolve-prefer ipv4 maxconn 50 + server tbImagesApi2 tb-core2:8080 check inter 10s resolvers docker_resolver resolve-prefer ipv4 maxconn 50 diff --git a/docker/haproxy/config/trustlist.txt b/docker/haproxy/config/trustlist.txt new file mode 100644 index 0000000000..93716b46d5 --- /dev/null +++ b/docker/haproxy/config/trustlist.txt @@ -0,0 +1,12 @@ +# Trusted list is intended to do not apply any limitations for trustees +# +# Private subnet example +# 10.0.0.0/8 +# Docker-compose subnet +172.16.0.0/12 +# Local network subnet +192.168.0.0/16 +# Allow loopback interface +127.0.0.1 +::1 +# Allow trusted IPs or CIDRs below diff --git a/docker/tb-lwm2m-transport.env b/docker/tb-lwm2m-transport.env index f284803a46..bf1d612410 100644 --- a/docker/tb-lwm2m-transport.env +++ b/docker/tb-lwm2m-transport.env @@ -3,6 +3,8 @@ ZOOKEEPER_URL=zookeeper:2181 LWM2M_BIND_ADDRESS=0.0.0.0 LWM2M_BIND_PORT=5685 +LWM2M_SECURITY_BIND_ADDRESS=0.0.0.0 +LWM2M_SECURITY_BIND_PORT=5686 LWM2M_TIMEOUT=10000 METRICS_ENABLED=true diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index d13bb54557..cbbd76a1f6 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -186,6 +186,10 @@ allure-testng test + + org.eclipse.leshan + leshan-client-cf + diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractLwm2mClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractLwm2mClientTest.java new file mode 100644 index 0000000000..d0a8b53c32 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractLwm2mClientTest.java @@ -0,0 +1,321 @@ +/** + * Copyright © 2016-2024 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; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.eclipse.leshan.client.object.Security; +import org.eclipse.leshan.core.util.Hex; +import org.junit.Assert; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapClientCredentials; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredential; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; +import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecBootstrapClientCredential; +import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCredential; +import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKBootstrapClientCredential; +import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKClientCredential; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; +import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.lwm2m.OtherConfiguration; +import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryMappingConfiguration; +import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBootstrapServerCredential; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.msa.connectivity.lwm2m.LwM2MTestClient; +import org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState; + +import java.net.ServerSocket; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.eclipse.leshan.client.object.Security.psk; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.CLIENT_ENDPOINT_NO_SEC; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.CLIENT_ENDPOINT_PSK; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.CLIENT_LWM2M_SETTINGS; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.CLIENT_PSK_IDENTITY; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.CLIENT_PSK_KEY; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_INIT; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_STARTED; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_SUCCESS; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.OBSERVE_ATTRIBUTES_WITHOUT_PARAMS; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.SECURE_URI; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.SECURITY_NO_SEC; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.resources; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.shortServerId; + +@Slf4j +public class AbstractLwm2mClientTest extends AbstractContainerTest{ + protected ScheduledExecutorService executor; + + protected Security security; + + protected final PageLink pageLink = new PageLink(30); + protected TenantId tenantId; + protected DeviceProfile lwm2mDeviceProfile; + protected Device lwM2MDeviceTest; + protected LwM2MTestClient lwM2MTestClient; + + public final Set expectedStatusesRegistrationLwm2mSuccess = new HashSet<>(Arrays.asList(ON_INIT, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS)); + + public void connectLwm2mClientNoSec() throws Exception { + LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(CLIENT_ENDPOINT_NO_SEC)); + basicTestConnection(SECURITY_NO_SEC, + deviceCredentials, + CLIENT_ENDPOINT_NO_SEC, + "TestConnection Lwm2m NoSec (msa)"); + } + + public void connectLwm2mClientPsk() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_PSK; + String identity = CLIENT_PSK_IDENTITY; + String keyPsk = CLIENT_PSK_KEY; + PSKClientCredential clientCredentials = new PSKClientCredential(); + clientCredentials.setEndpoint(clientEndpoint); + clientCredentials.setIdentity(identity); + clientCredentials.setKey(keyPsk); + Security security = psk(SECURE_URI, + shortServerId, + identity.getBytes(StandardCharsets.UTF_8), + Hex.decodeHex(keyPsk.toCharArray())); + LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecurePsk(clientCredentials); + + basicTestConnection(security, + deviceCredentials, + clientEndpoint, + "TestConnection Lwm2m Rpc (msa)"); + } + + public void basicTestConnection(Security security, + LwM2MDeviceCredentials deviceCredentials, + String clientEndpoint, String alias) throws Exception { + // create lwm2mClient and lwM2MDevice + lwM2MDeviceTest = createDeviceWithCredentials(deviceCredentials, clientEndpoint); + lwM2MTestClient = createNewClient(security, clientEndpoint, executor); + + LwM2MClientState finishState = ON_REGISTRATION_SUCCESS; + await(alias + " - " + ON_REGISTRATION_STARTED) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + log.warn("msa basicTestConnection started -> finishState: [{}] states: {}", finishState, lwM2MTestClient.getClientStates()); + return lwM2MTestClient.getClientStates().contains(finishState) || lwM2MTestClient.getClientStates().contains(ON_REGISTRATION_STARTED); + }); + await(alias + " - " + ON_UPDATE_SUCCESS) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + log.warn("msa basicTestConnection update -> finishState: [{}] states: {}", finishState, lwM2MTestClient.getClientStates()); + return lwM2MTestClient.getClientStates().contains(finishState) || lwM2MTestClient.getClientStates().contains(ON_UPDATE_SUCCESS); + }); + Assert.assertTrue(lwM2MTestClient.getClientStates().containsAll(expectedStatusesRegistrationLwm2mSuccess)); + } + + public LwM2MTestClient createNewClient(Security security, + String endpoint, ScheduledExecutorService executor) throws Exception { + this.executor = executor; + LwM2MTestClient lwM2MTestClient = new LwM2MTestClient(endpoint); + try (ServerSocket socket = new ServerSocket(0)) { + int clientPort = socket.getLocalPort(); + lwM2MTestClient.init(security, clientPort); + } + return lwM2MTestClient; + } + + protected void destroyAfter(){ + clientDestroy(); + deviceDestroy(); + deviceProfileDestroy(); + if (executor != null) { + executor.shutdown(); + } + } + + protected void clientDestroy() { + try { + if (lwM2MTestClient != null) { + lwM2MTestClient.destroy(); + } + } catch (Exception e) { + log.error("Failed client Destroy", e); + } + } + protected void deviceDestroy() { + try { + if (lwM2MDeviceTest != null) { + testRestClient.deleteDeviceIfExists(lwM2MDeviceTest.getId()); + } + } catch (Exception e) { + log.error("Failed device Delete", e); + } + } + + protected void initTest(String deviceProfileName) throws Exception { + if (executor != null) { + executor.shutdown(); + } + executor = Executors.newScheduledThreadPool(10, ThingsBoardThreadFactory.forName("test-scheduled-" + deviceProfileName)); + + lwm2mDeviceProfile = getDeviceProfile(deviceProfileName); + tenantId = lwm2mDeviceProfile.getTenantId(); + + for (String resourceName : resources) { + TbResource lwModel = new TbResource(); + lwModel.setResourceType(ResourceType.LWM2M_MODEL); + lwModel.setTitle(resourceName); + lwModel.setFileName(resourceName); + lwModel.setTenantId(tenantId); + byte[] bytes = IOUtils.toByteArray(AbstractLwm2mClientTest.class.getClassLoader().getResourceAsStream("lwm2m-registry/" + resourceName)); + lwModel.setData(bytes); + testRestClient.postTbResourceIfNotExists(lwModel); + } + } + + protected DeviceProfile getDeviceProfile(String deviceProfileName) throws Exception { + DeviceProfile deviceProfile = getDeviceProfileIfExists(deviceProfileName); + if (deviceProfile == null) { + deviceProfile = testRestClient.postDeviceProfile(createDeviceProfile(deviceProfileName)); + } + return deviceProfile; + } + + protected DeviceProfile getDeviceProfileIfExists(String deviceProfileName) throws Exception { + return testRestClient.getDeviceProfiles(pageLink).getData().stream() + .filter(x -> x.getName().equals(deviceProfileName)) + .findFirst() + .orElse(null); + } + + + protected DeviceProfile createDeviceProfile(String deviceProfileName) throws Exception { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setName(deviceProfileName); + deviceProfile.setType(DeviceProfileType.DEFAULT); + deviceProfile.setTransportType(DeviceTransportType.LWM2M); + deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED); + deviceProfile.setDescription(deviceProfile.getName()); + + DeviceProfileData deviceProfileData = new DeviceProfileData(); + deviceProfileData.setConfiguration(new DefaultDeviceProfileConfiguration()); + deviceProfileData.setProvisionConfiguration(new DisabledDeviceProfileProvisionConfiguration(null)); + deviceProfileData.setTransportConfiguration(getTransportConfiguration()); + deviceProfile.setProfileData(deviceProfileData); + return deviceProfile; + } + + protected void deviceProfileDestroy(){ + try { + if (lwm2mDeviceProfile != null) { + testRestClient.deleteDeviceProfileIfExists(lwm2mDeviceProfile); + } + } catch (Exception e) { + log.error("Failed deviceProfile Delete", e); + } + } + + protected Device createDeviceWithCredentials(LwM2MDeviceCredentials deviceCredentials, String clientEndpoint) throws Exception { + Device device = createDevice(deviceCredentials, clientEndpoint); + return device; + } + + protected Device createDevice(LwM2MDeviceCredentials credentials, String clientEndpoint) throws Exception { + Device device = testRestClient.getDeviceByNameIfExists(clientEndpoint); + if (device == null) { + device = new Device(); + device.setName(clientEndpoint); + device.setDeviceProfileId(lwm2mDeviceProfile.getId()); + device.setTenantId(tenantId); + device = testRestClient.postDevice("", device); + } + + DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); + deviceCredentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS); + deviceCredentials.setCredentialsValue(JacksonUtil.toString(credentials)); + deviceCredentials = testRestClient.postDeviceCredentials(deviceCredentials); + assertThat(deviceCredentials).isNotNull(); + return device; + } + + protected LwM2MDeviceCredentials getDeviceCredentialsNoSec(LwM2MClientCredential clientCredentials) { + LwM2MDeviceCredentials credentials = new LwM2MDeviceCredentials(); + credentials.setClient(clientCredentials); + LwM2MBootstrapClientCredentials bootstrapCredentials = new LwM2MBootstrapClientCredentials(); + NoSecBootstrapClientCredential serverCredentials = new NoSecBootstrapClientCredential(); + bootstrapCredentials.setBootstrapServer(serverCredentials); + bootstrapCredentials.setLwm2mServer(serverCredentials); + credentials.setBootstrap(bootstrapCredentials); + return credentials; + } + + public NoSecClientCredential createNoSecClientCredentials(String clientEndpoint) { + NoSecClientCredential clientCredentials = new NoSecClientCredential(); + clientCredentials.setEndpoint(clientEndpoint); + return clientCredentials; + } + + protected Lwm2mDeviceProfileTransportConfiguration getTransportConfiguration() { + List bootstrapServerCredentials = new ArrayList<>(); + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration(); + TelemetryMappingConfiguration observeAttrConfiguration = JacksonUtil.fromString(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, TelemetryMappingConfiguration.class); + OtherConfiguration clientLwM2mSettings = JacksonUtil.fromString(CLIENT_LWM2M_SETTINGS, OtherConfiguration.class); + transportConfiguration.setBootstrapServerUpdateEnable(true); + transportConfiguration.setObserveAttr(observeAttrConfiguration); + transportConfiguration.setClientLwM2mSettings(clientLwM2mSettings); + transportConfiguration.setBootstrap(bootstrapServerCredentials); + return transportConfiguration; + } + + protected LwM2MDeviceCredentials getDeviceCredentialsSecurePsk(LwM2MClientCredential clientCredentials) { + LwM2MDeviceCredentials credentials = new LwM2MDeviceCredentials(); + credentials.setClient(clientCredentials); + LwM2MBootstrapClientCredentials bootstrapCredentials; + bootstrapCredentials = getBootstrapClientCredentialsPsk(clientCredentials); + credentials.setBootstrap(bootstrapCredentials); + return credentials; + } + + private LwM2MBootstrapClientCredentials getBootstrapClientCredentialsPsk(LwM2MClientCredential clientCredentials) { + LwM2MBootstrapClientCredentials bootstrapCredentials = new LwM2MBootstrapClientCredentials(); + PSKBootstrapClientCredential serverCredentials = new PSKBootstrapClientCredential(); + if (clientCredentials != null) { + serverCredentials.setClientSecretKey(((PSKClientCredential) clientCredentials).getKey()); + serverCredentials.setClientPublicKeyOrId(((PSKClientCredential) clientCredentials).getIdentity()); + } + bootstrapCredentials.setBootstrapServer(serverCredentials); + bootstrapCredentials.setLwm2mServer(serverCredentials); + return bootstrapCredentials; + } +} 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 83c9c927aa..c76627d3a1 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 @@ -23,6 +23,7 @@ import io.restassured.config.RestAssuredConfig; import io.restassured.filter.log.RequestLoggingFilter; import io.restassured.filter.log.ResponseLoggingFilter; import io.restassured.http.ContentType; +import io.restassured.internal.ValidatableResponseImpl; import io.restassured.path.json.JsonPath; import io.restassured.response.ValidatableResponse; import io.restassured.specification.RequestSpecification; @@ -32,6 +33,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EventInfo; +import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; @@ -63,6 +65,7 @@ import java.util.List; import java.util.Map; import static io.restassured.RestAssured.given; +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; import static java.net.HttpURLConnection.HTTP_NOT_FOUND; import static java.net.HttpURLConnection.HTTP_OK; import static org.hamcrest.Matchers.is; @@ -548,6 +551,43 @@ public class TestRestClient { .as(new TypeRef<>() {}); } + public ValidatableResponse postTbResourceIfNotExists(TbResource lwModel) { + return given().spec(requestSpec).body(lwModel) + .post("/api/resource") + .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()) + .then() + .statusCode(anyOf(is(HTTP_OK), is(HTTP_NOT_FOUND))); + } + + public Device getDeviceByNameIfExists(String deviceName) { + ValidatableResponse response = given().spec(requestSpec) + .pathParams("deviceName", deviceName) + .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() + .as(Device.class); + } else { + return null; + } + } + + public DeviceCredentials postDeviceCredentials(DeviceCredentials deviceCredentials) { + return given().spec(requestSpec).body(deviceCredentials) + .post("/api/device/credentials") + .then() + .assertThat() + .statusCode(HTTP_OK) + .extract() + .as(DeviceCredentials.class); + } + private void addTimePageLinkToParam(Map params, TimePageLink pageLink) { this.addPageLinkToParam(params, pageLink); if (pageLink.getStartTime() != null) { diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/Lwm2mClientNoSecTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/Lwm2mClientNoSecTest.java new file mode 100644 index 0000000000..15658140b0 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/Lwm2mClientNoSecTest.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2024 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.connectivity; + +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.thingsboard.server.msa.AbstractLwm2mClientTest; +import org.thingsboard.server.msa.DisableUIListeners; + +import static org.thingsboard.server.msa.ui.utils.Const.TENANT_EMAIL; +import static org.thingsboard.server.msa.ui.utils.Const.TENANT_PASSWORD; + +@DisableUIListeners +public class Lwm2mClientNoSecTest extends AbstractLwm2mClientTest { + + @BeforeMethod + public void setUp() throws Exception { + testRestClient.login(TENANT_EMAIL, TENANT_PASSWORD); + initTest("lwm2m-NoSec"); + } + + @AfterMethod + public void tearDown() { + destroyAfter(); + } + + @Test + public void connectLwm2mClientNoSecWithLwm2mServer() throws Exception { + connectLwm2mClientNoSec(); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/Lwm2mClientPskTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/Lwm2mClientPskTest.java new file mode 100644 index 0000000000..fbd8139d8f --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/Lwm2mClientPskTest.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2024 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.connectivity; + +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.thingsboard.server.msa.AbstractLwm2mClientTest; +import org.thingsboard.server.msa.DisableUIListeners; + +import static org.thingsboard.server.msa.ui.utils.Const.TENANT_EMAIL; +import static org.thingsboard.server.msa.ui.utils.Const.TENANT_PASSWORD; + +@DisableUIListeners +public class Lwm2mClientPskTest extends AbstractLwm2mClientTest { + + @BeforeMethod + public void setUp() throws Exception { + testRestClient.login(TENANT_EMAIL, TENANT_PASSWORD); + initTest("lwm2m-Psk"); + } + + @AfterMethod + public void tearDown() { + destroyAfter(); + } + + @Test + public void connectLwm2mClientPskWithLwm2mServer() throws Exception { + connectLwm2mClientPsk(); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/FwLwM2MDevice.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/FwLwM2MDevice.java new file mode 100644 index 0000000000..2910a2b7da --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/FwLwM2MDevice.java @@ -0,0 +1,157 @@ +/** + * Copyright © 2016-2024 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.connectivity.lwm2m; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.client.resource.BaseInstanceEnabler; +import org.eclipse.leshan.client.servers.LwM2mServer; +import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.node.LwM2mResource; +import org.eclipse.leshan.core.request.argument.Arguments; +import org.eclipse.leshan.core.response.ExecuteResponse; +import org.eclipse.leshan.core.response.ReadResponse; +import org.eclipse.leshan.core.response.WriteResponse; +import org.thingsboard.common.util.ThingsBoardThreadFactory; + +import javax.security.auth.Destroyable; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +@Slf4j +public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { + + private static final List supportedResources = Arrays.asList(0, 1, 2, 3, 5, 6, 7, 9); + + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope")); + + private final AtomicInteger state = new AtomicInteger(0); + + private final AtomicInteger updateResult = new AtomicInteger(0); + + @Override + public ReadResponse read(LwM2mServer identity, int resourceId) { + if (!identity.isSystem()) + log.info("Read on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); + switch (resourceId) { + case 3: + return ReadResponse.success(resourceId, getState()); + case 5: + return ReadResponse.success(resourceId, getUpdateResult()); + case 6: + return ReadResponse.success(resourceId, getPkgName()); + case 7: + return ReadResponse.success(resourceId, getPkgVersion()); + case 9: + return ReadResponse.success(resourceId, getFirmwareUpdateDeliveryMethod()); + default: + return super.read(identity, resourceId); + } + } + + @Override + public ExecuteResponse execute(LwM2mServer identity, int resourceId, Arguments arguments) { + String withArguments = ""; + if (!arguments.isEmpty()) + withArguments = " with arguments " + arguments; + log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withArguments); + + + switch (resourceId) { + case 2: + startUpdating(); + return ExecuteResponse.success(); + default: + return super.execute(identity, resourceId, arguments); + } + } + + @Override + public WriteResponse write(LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) { + log.info("Write on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); + + switch (resourceId) { + case 0: + startDownloading(); + return WriteResponse.success(); + case 1: + startDownloading(); + return WriteResponse.success(); + default: + return super.write(identity, replace, resourceId, value); + } + } + + private int getState() { + return state.get(); + } + + private int getUpdateResult() { + return updateResult.get(); + } + + private String getPkgName() { + return "firmware"; + } + + private String getPkgVersion() { + return "1.0.0"; + } + + private int getFirmwareUpdateDeliveryMethod() { + return 1; + } + + @Override + public List getAvailableResourceIds(ObjectModel model) { + return supportedResources; + } + + @Override + public void destroy() { + scheduler.shutdownNow(); + } + + private void startDownloading() { + scheduler.schedule(() -> { + try { + state.set(1); + fireResourceChange(3); + Thread.sleep(100); + state.set(2); + fireResourceChange(3); + } catch (Exception e) { + } + }, 100, TimeUnit.MILLISECONDS); + } + + private void startUpdating() { + scheduler.schedule(() -> { + try { + state.set(3); + fireResourceChange(3); + Thread.sleep(100); + updateResult.set(1); + fireResourceChange(5); + } catch (Exception e) { + } + }, 100, TimeUnit.MILLISECONDS); + } + +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java new file mode 100644 index 0000000000..4c20199c8e --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java @@ -0,0 +1,337 @@ +/** + * Copyright © 2016-2024 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.connectivity.lwm2m; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.elements.config.Configuration; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.leshan.client.LeshanClient; +import org.eclipse.leshan.client.LeshanClientBuilder; +import org.eclipse.leshan.client.californium.endpoint.CaliforniumClientEndpointFactory; +import org.eclipse.leshan.client.californium.endpoint.CaliforniumClientEndpointsProvider; +import org.eclipse.leshan.client.californium.endpoint.ClientProtocolProvider; +import org.eclipse.leshan.client.californium.endpoint.coap.CoapOscoreProtocolProvider; +import org.eclipse.leshan.client.californium.endpoint.coaps.CoapsClientEndpointFactory; +import org.eclipse.leshan.client.californium.endpoint.coaps.CoapsClientProtocolProvider; +import org.eclipse.leshan.client.endpoint.LwM2mClientEndpointsProvider; +import org.eclipse.leshan.client.engine.DefaultRegistrationEngineFactory; +import org.eclipse.leshan.client.object.Security; +import org.eclipse.leshan.client.object.Server; +import org.eclipse.leshan.client.observer.LwM2mClientObserver; +import org.eclipse.leshan.client.resource.DummyInstanceEnabler; +import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; +import org.eclipse.leshan.client.resource.ObjectsInitializer; +import org.eclipse.leshan.client.resource.listener.ObjectsListenerAdapter; +import org.eclipse.leshan.client.send.ManualDataSender; +import org.eclipse.leshan.client.servers.LwM2mServer; +import org.eclipse.leshan.core.ResponseCode; +import org.eclipse.leshan.core.model.InvalidDDFFileException; +import org.eclipse.leshan.core.model.LwM2mModel; +import org.eclipse.leshan.core.model.ObjectLoader; +import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.model.StaticModel; +import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder; +import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder; +import org.eclipse.leshan.core.request.BootstrapRequest; +import org.eclipse.leshan.core.request.DeregisterRequest; +import org.eclipse.leshan.core.request.RegisterRequest; +import org.eclipse.leshan.core.request.UpdateRequest; +import org.junit.Assert; +import org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH; +import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY; +import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; +import static org.eclipse.leshan.core.LwM2mId.DEVICE; +import static org.eclipse.leshan.core.LwM2mId.FIRMWARE; +import static org.eclipse.leshan.core.LwM2mId.SECURITY; +import static org.eclipse.leshan.core.LwM2mId.SERVER; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_FAILURE; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_STARTED; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_SUCCESS; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_TIMEOUT; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_DEREGISTRATION_FAILURE; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_DEREGISTRATION_STARTED; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_DEREGISTRATION_SUCCESS; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_DEREGISTRATION_TIMEOUT; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_EXPECTED_ERROR; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_INIT; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_FAILURE; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_STARTED; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_TIMEOUT; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_FAILURE; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_STARTED; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_SUCCESS; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_TIMEOUT; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.resources; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.serverId; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.shortServerId; + + +@Slf4j +@Data +public class LwM2MTestClient { + + private final String endpoint; + private LeshanClient leshanClient; + private SimpleLwM2MDevice lwM2MDevice; + + private Set clientStates; + + private FwLwM2MDevice fwLwM2MDevice; + private Map clientDtlsCid; + + public void init(Security security, int clientPort) throws InvalidDDFFileException, IOException { + Assert.assertNull("client already initialized", leshanClient); + + List models = new ArrayList<>(); + for (String resourceName : resources) { + models.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m-registry/" + resourceName), resourceName)); + } + LwM2mModel model = new StaticModel(models); + ObjectsInitializer initializer = new ObjectsInitializer(model); + + // SECURITY + initializer.setInstancesForObject(SECURITY, security); + // SERVER + Server lwm2mServer = new Server(shortServerId, 300); + lwm2mServer.setId(serverId); + initializer.setInstancesForObject(SERVER, lwm2mServer); + + initializer.setInstancesForObject(DEVICE, lwM2MDevice = new SimpleLwM2MDevice()); + initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class); + initializer.setInstancesForObject(FIRMWARE, fwLwM2MDevice = new FwLwM2MDevice()); + + List enablers = initializer.createAll(); + + // Create Californium Endpoints Provider: + // -------------------------------------- + // Define Custom CoAPS protocol provider + CoapsClientProtocolProvider customCoapsProtocolProvider = new CoapsClientProtocolProvider() { + @Override + public CaliforniumClientEndpointFactory createDefaultEndpointFactory() { + return new CoapsClientEndpointFactory() { + + @Override + protected DtlsConnectorConfig.Builder createRootDtlsConnectorConfigBuilder( + Configuration configuration) { + DtlsConnectorConfig.Builder builder = super.createRootDtlsConnectorConfigBuilder(configuration); + return builder; + }; + }; + } + }; + + // Create client endpoints Provider + List protocolProvider = new ArrayList<>(); + + /** + * "Use java-coap for CoAP protocol instead of Californium." + */ + protocolProvider.add(new CoapOscoreProtocolProvider()); + protocolProvider.add(customCoapsProtocolProvider); + CaliforniumClientEndpointsProvider.Builder endpointsBuilder = new CaliforniumClientEndpointsProvider.Builder( + protocolProvider.toArray(new ClientProtocolProvider[protocolProvider.size()])); + + + // Create Californium Configuration + Configuration clientCoapConfig = endpointsBuilder.createDefaultConfiguration(); + + // Set some DTLS stuff + // These configuration values are always overwritten by CLI therefore set them to transient. + clientCoapConfig.setTransient(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY); + clientCoapConfig.setTransient(DTLS_CONNECTION_ID_LENGTH); + boolean supportDeprecatedCiphers = false; + clientCoapConfig.set(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, !supportDeprecatedCiphers); + + // Set Californium Configuration + + endpointsBuilder.setConfiguration(clientCoapConfig); + endpointsBuilder.setClientAddress(new InetSocketAddress(clientPort).getAddress()); + + + // creates EndpointsProvider + List endpointsProvider = new ArrayList<>(); + endpointsProvider.add(endpointsBuilder.build()); + + // Configure Registration Engine + DefaultRegistrationEngineFactory engineFactory = new DefaultRegistrationEngineFactory(); + /** + * Force reconnection/rehandshake on registration update. + */ + int comPeriodInSec = 5; + if (comPeriodInSec > 0) engineFactory.setCommunicationPeriod(comPeriodInSec * 1000); + + /** + * By default client will try to resume DTLS session by using abbreviated Handshake. This option force to always do a full handshake." + */ + boolean reconnectOnUpdate = false; + engineFactory.setReconnectOnUpdate(reconnectOnUpdate); + engineFactory.setResumeOnConnect(true); + + /** + * Client use queue mode. + */ + engineFactory.setQueueMode(false); + + // Create client + LeshanClientBuilder builder = new LeshanClientBuilder(endpoint); + builder.setObjects(enablers); + builder.setEndpointsProviders(endpointsProvider.toArray(new LwM2mClientEndpointsProvider[endpointsProvider.size()])); + builder.setDataSenders(new ManualDataSender()); + builder.setRegistrationEngineFactory(engineFactory); + boolean supportOldFormat = true; + if (supportOldFormat) { + builder.setDecoder(new DefaultLwM2mDecoder(supportOldFormat)); + builder.setEncoder(new DefaultLwM2mEncoder(new LwM2mValueConverterImpl(), supportOldFormat)); + } + + builder.setRegistrationEngineFactory(engineFactory); +// builder.setSharedExecutor(executor); + + clientStates = new HashSet<>(); + clientDtlsCid = new HashMap<>(); + clientStates.add(ON_INIT); + leshanClient = builder.build(); + + LwM2mClientObserver observer = new LwM2mClientObserver() { + @Override + public void onBootstrapStarted(LwM2mServer bsserver, BootstrapRequest request) { + clientStates.add(ON_BOOTSTRAP_STARTED); + } + + @Override + public void onBootstrapSuccess(LwM2mServer bsserver, BootstrapRequest request) { + clientStates.add(ON_BOOTSTRAP_SUCCESS); + } + + @Override + public void onBootstrapFailure(LwM2mServer bsserver, BootstrapRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + clientStates.add(ON_BOOTSTRAP_FAILURE); + } + + @Override + public void onBootstrapTimeout(LwM2mServer bsserver, BootstrapRequest request) { + clientStates.add(ON_BOOTSTRAP_TIMEOUT); + } + + @Override + public void onRegistrationStarted(LwM2mServer server, RegisterRequest request) { + clientStates.add(ON_REGISTRATION_STARTED); + } + + @Override + public void onRegistrationSuccess(LwM2mServer server, RegisterRequest request, String registrationID) { + clientStates.add(ON_REGISTRATION_SUCCESS); + } + + @Override + public void onRegistrationFailure(LwM2mServer server, RegisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + clientStates.add(ON_REGISTRATION_FAILURE); + } + + @Override + public void onRegistrationTimeout(LwM2mServer server, RegisterRequest request) { + clientStates.add(ON_REGISTRATION_TIMEOUT); + } + + @Override + public void onUpdateStarted(LwM2mServer server, UpdateRequest request) { + clientStates.add(ON_UPDATE_STARTED); + } + + @Override + public void onUpdateSuccess(LwM2mServer server, UpdateRequest request) { + clientStates.add(ON_UPDATE_SUCCESS); + } + + @Override + public void onUpdateFailure(LwM2mServer server, UpdateRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + clientStates.add(ON_UPDATE_FAILURE); + } + + @Override + public void onUpdateTimeout(LwM2mServer server, UpdateRequest request) { + clientStates.add(ON_UPDATE_TIMEOUT); + } + + @Override + public void onDeregistrationStarted(LwM2mServer server, DeregisterRequest request) { + clientStates.add(ON_DEREGISTRATION_STARTED); + } + + @Override + public void onDeregistrationSuccess(LwM2mServer server, DeregisterRequest request) { + clientStates.add(ON_DEREGISTRATION_SUCCESS); + } + + @Override + public void onDeregistrationFailure(LwM2mServer server, DeregisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + clientStates.add(ON_DEREGISTRATION_FAILURE); + } + + @Override + public void onDeregistrationTimeout(LwM2mServer server, DeregisterRequest request) { + clientStates.add(ON_DEREGISTRATION_TIMEOUT); + } + + @Override + public void onUnexpectedError(Throwable unexpectedError) { + clientStates.add(ON_EXPECTED_ERROR); + } + }; + this.leshanClient.addObserver(observer); + + // Add some log about object tree life cycle. + this.leshanClient.getObjectTree().addListener(new ObjectsListenerAdapter() { + + @Override + public void objectRemoved(LwM2mObjectEnabler object) { + log.info("Object {} v{} disabled.", object.getId(), object.getObjectModel().version); + } + + @Override + public void objectAdded(LwM2mObjectEnabler object) { + log.info("Object {} v{} enabled.", object.getId(), object.getObjectModel().version); + } + }); + + leshanClient.start(); + } + + public void destroy() { + if (leshanClient != null) { + leshanClient.destroy(true); + } + if (lwM2MDevice != null) { + lwM2MDevice.destroy(); + } + if (fwLwM2MDevice != null) { + fwLwM2MDevice.destroy(); + } + } +} \ No newline at end of file diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2mValueConverterImpl.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2mValueConverterImpl.java new file mode 100644 index 0000000000..b49d54f27e --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2mValueConverterImpl.java @@ -0,0 +1,192 @@ +/** + * Copyright © 2016-2024 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.connectivity.lwm2m; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.model.ResourceModel.Type; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.ObjectLink; +import org.eclipse.leshan.core.node.codec.CodecException; +import org.eclipse.leshan.core.node.codec.LwM2mValueConverter; +import org.eclipse.leshan.core.util.Hex; +import org.thingsboard.server.common.data.StringUtils; + +import java.math.BigInteger; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Base64; +import java.util.Date; + +import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE; + +@Slf4j +public class LwM2mValueConverterImpl implements LwM2mValueConverter { + + private static final LwM2mValueConverterImpl INSTANCE = new LwM2mValueConverterImpl(); + + public static LwM2mValueConverterImpl getInstance() { + return INSTANCE; + } + + @Override + public Object convertValue(Object value, Type currentType, Type expectedType, LwM2mPath resourcePath) + throws CodecException { + if (value == null) { + return null; + } + if (expectedType == null) { + /** unknown resource, trusted value */ + return value; + } + + if (currentType == expectedType) { + /** expected type */ + return value; + } + if (currentType == null) { + currentType = OPAQUE; + } + + switch (expectedType) { + case INTEGER: + switch (currentType) { + case FLOAT: + log.debug("Trying to convert float value [{}] to Integer", value); + Long longValue = ((Double) value).longValue(); + if ((double) value == longValue.doubleValue()) { + return longValue; + } + case STRING: + log.debug("Trying to convert String value [{}] to Integer", value); + return Long.parseLong((String) value); + default: + break; + } + break; + case FLOAT: + switch (currentType) { + case INTEGER: + log.debug("Trying to convert integer value [{}] to float", value); + Double floatValue = ((Long) value).doubleValue(); + if ((long) value == floatValue.longValue()) { + return floatValue; + } + case STRING: + log.debug("Trying to convert String value [{}] to Float", value); + return Float.valueOf((String) value); + default: + break; + } + break; + case BOOLEAN: + switch (currentType) { + case STRING: + log.debug("Trying to convert string value {} to boolean", value); + if (StringUtils.equalsIgnoreCase((String) value, "true")) { + return true; + } else if (StringUtils.equalsIgnoreCase((String) value, "false")) { + return false; + } + break; + case INTEGER: + log.debug("Trying to convert int value {} to boolean", value); + Long val = (Long) value; + if (val == 1) { + return true; + } else if (val == 0) { + return false; + } + break; + default: + break; + } + break; + case TIME: + switch (currentType) { + case INTEGER: + log.debug("Trying to convert long value {} to date", value); + /* let's assume we received the millisecond since 1970/1/1 */ + return new Date(((Number) value).longValue() * 1000L); + case STRING: + log.debug("Trying to convert string value {} to date", value); + /** let's assume we received an ISO 8601 format date */ + try { + return new Date(Long.decode(value.toString())); + /** + DatatypeFactory datatypeFactory = DatatypeFactory.newInstance(); + XMLGregorianCalendar cal = datatypeFactory.newXMLGregorianCalendar((String) value); + return cal.toGregorianCalendar().getTime(); + **/ + } catch (IllegalArgumentException e) { + log.debug("Unable to convert string to date", e); + throw new CodecException("Unable to convert string (%s) to date for resource %s", value, + resourcePath); + } + default: + break; + } + break; + case STRING: + switch (currentType) { + case BOOLEAN: + case INTEGER: + case FLOAT: + return String.valueOf(value); + case TIME: + String DATE_FORMAT = "MMM d, yyyy HH:mm a"; + Long timeValue; + try { + timeValue = ((Date) value).getTime(); + } + catch (Exception e){ + timeValue = new BigInteger((byte [])value).longValue(); + } + DateFormat formatter = new SimpleDateFormat(DATE_FORMAT); + return formatter.format(new Date(timeValue)); + case OPAQUE: + return Hex.encodeHexString((byte[])value); + case OBJLNK: + return ObjectLink.decodeFromString((String) value); + default: + break; + } + break; + case OPAQUE: + if (currentType == Type.STRING) { + /** let's assume we received an hexadecimal string */ + log.debug("Trying to convert hexadecimal/base64 string [{}] to byte array", value); + try { + return Hex.decodeHex(((String)value).toCharArray()); + } catch (IllegalArgumentException e) { + try { + return Base64.getDecoder().decode((String) value); + } catch (IllegalArgumentException ea) { + throw new CodecException("Unable to convert hexastring or base64 [%s] to byte array for resource %s", + value, resourcePath); + } + } + } + break; + case OBJLNK: + if (currentType == Type.STRING) { + return ObjectLink.fromPath(value.toString()); + } + default: + } + throw new CodecException("Invalid value type for resource %s, expected %s, got %s", resourcePath, expectedType, + currentType); + } +} \ No newline at end of file diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/Lwm2mTestHelper.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/Lwm2mTestHelper.java new file mode 100644 index 0000000000..dfb4c71a1e --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/Lwm2mTestHelper.java @@ -0,0 +1,134 @@ +/** + * Copyright © 2016-2024 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.connectivity.lwm2m; + +import org.eclipse.californium.elements.config.Configuration; +import org.eclipse.leshan.client.object.Security; + +import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH; +import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_NODE_ID; +import static org.eclipse.leshan.client.object.Security.noSec; + +public class Lwm2mTestHelper { + + // Models + public static final String[] resources = new String[]{ "0.xml", "1.xml", "2.xml", "3.xml", "5.xml"}; + public static final int serverId = 1; + + public static final int port = 5685; + public static final int securityPort = 5686; + public static final Integer shortServerId = 123; + + public static final String host = "localhost"; + public static final String COAP = "coap://"; + public static final String COAPS = "coaps://"; + public static final String URI = COAP + host + ":" + port; + public static final String SECURE_URI = COAPS + host + ":" + securityPort; + public static final String CLIENT_ENDPOINT_NO_SEC = "LwNoSec00000000"; + public static final Security SECURITY_NO_SEC = noSec(URI, shortServerId); + + public static final String CLIENT_ENDPOINT_PSK = "LwPsk00000000"; + public static final String CLIENT_PSK_IDENTITY = "SOME_PSK_ID"; + public static final String CLIENT_PSK_KEY = "73656372657450534b73656372657450"; + + + public static final String OBSERVE_ATTRIBUTES_WITHOUT_PARAMS = + " {\n" + + " \"keyName\": {},\n" + + " \"observe\": [],\n" + + " \"attribute\": [],\n" + + " \"telemetry\": [],\n" + + " \"attributeLwm2m\": {}\n" + + " }"; + + public static final String CLIENT_LWM2M_SETTINGS = + " {\n" + + " \"edrxCycle\": null,\n" + + " \"powerMode\": \"DRX\",\n" + + " \"fwUpdateResource\": null,\n" + + " \"fwUpdateStrategy\": 1,\n" + + " \"psmActivityTimer\": null,\n" + + " \"swUpdateResource\": null,\n" + + " \"swUpdateStrategy\": 1,\n" + + " \"pagingTransmissionWindow\": null,\n" + + " \"clientOnlyObserveAfterConnect\": 1\n" + + " }"; + + public static final int BINARY_APP_DATA_CONTAINER = 19; + public static final int OBJECT_INSTANCE_ID_0 = 0; + public static final int OBJECT_INSTANCE_ID_1 = 1; + + public enum LwM2MClientState { + + ON_INIT(0, "onInit"), + ON_BOOTSTRAP_STARTED(1, "onBootstrapStarted"), + ON_BOOTSTRAP_SUCCESS(2, "onBootstrapSuccess"), + ON_BOOTSTRAP_FAILURE(3, "onBootstrapFailure"), + ON_BOOTSTRAP_TIMEOUT(4, "onBootstrapTimeout"), + ON_REGISTRATION_STARTED(5, "onRegistrationStarted"), + ON_REGISTRATION_SUCCESS(6, "onRegistrationSuccess"), + ON_REGISTRATION_FAILURE(7, "onRegistrationFailure"), + ON_REGISTRATION_TIMEOUT(7, "onRegistrationTimeout"), + ON_UPDATE_STARTED(8, "onUpdateStarted"), + ON_UPDATE_SUCCESS(9, "onUpdateSuccess"), + ON_UPDATE_FAILURE(10, "onUpdateFailure"), + ON_UPDATE_TIMEOUT(11, "onUpdateTimeout"), + ON_DEREGISTRATION_STARTED(12, "onDeregistrationStarted"), + ON_DEREGISTRATION_SUCCESS(13, "onDeregistrationSuccess"), + ON_DEREGISTRATION_FAILURE(14, "onDeregistrationFailure"), + ON_DEREGISTRATION_TIMEOUT(15, "onDeregistrationTimeout"), + ON_EXPECTED_ERROR(16, "onUnexpectedError"), + ON_READ_CONNECTION_ID (17, "onReadConnection"), + ON_WRITE_CONNECTION_ID (18, "onWriteConnection"); + + public int code; + public String type; + + LwM2MClientState(int code, String type) { + this.code = code; + this.type = type; + } + + public static LwM2MClientState fromLwM2MClientStateByType(String type) { + for (LwM2MClientState to : LwM2MClientState.values()) { + if (to.type.equals(type)) { + return to; + } + } + throw new IllegalArgumentException(String.format("Unsupported Client State type : %s", type)); + } + + public static LwM2MClientState fromLwM2MClientStateByCode(int code) { + for (LwM2MClientState to : LwM2MClientState.values()) { + if (to.code == code) { + return to; + } + } + throw new IllegalArgumentException(String.format("Unsupported Client State code : %s", code)); + } + } + + public static void setDtlsConnectorConfigCidLength(Configuration serverCoapConfig, Integer cIdLength) { + serverCoapConfig.setTransient(DTLS_CONNECTION_ID_LENGTH); + serverCoapConfig.setTransient(DTLS_CONNECTION_ID_NODE_ID); + serverCoapConfig.set(DTLS_CONNECTION_ID_LENGTH, cIdLength); + if ( cIdLength > 4) { + serverCoapConfig.set(DTLS_CONNECTION_ID_NODE_ID, 0); + } else { + serverCoapConfig.set(DTLS_CONNECTION_ID_NODE_ID, null); + } + } +} \ No newline at end of file diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/SimpleLwM2MDevice.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/SimpleLwM2MDevice.java new file mode 100644 index 0000000000..d2aa1af4d2 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/SimpleLwM2MDevice.java @@ -0,0 +1,218 @@ +/** + * Copyright © 2016-2024 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.connectivity.lwm2m; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.client.resource.BaseInstanceEnabler; +import org.eclipse.leshan.client.servers.LwM2mServer; +import org.eclipse.leshan.core.Destroyable; +import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.model.ResourceModel; +import org.eclipse.leshan.core.node.LwM2mResource; +import org.eclipse.leshan.core.request.argument.Arguments; +import org.eclipse.leshan.core.response.ExecuteResponse; +import org.eclipse.leshan.core.response.ReadResponse; +import org.eclipse.leshan.core.response.WriteResponse; + +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.PrimitiveIterator; +import java.util.Random; +import java.util.TimeZone; + +@Slf4j +public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyable { + + + private static final Random RANDOM = new Random(); + private static final int min = 5; + private static final int max = 50; + private static final PrimitiveIterator.OfInt randomIterator = new Random().ints(min,max + 1).iterator(); + private static final List supportedResources = Arrays.asList(0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21); + + + public SimpleLwM2MDevice() { + } + + + + @Override + public ReadResponse read(LwM2mServer identity, int resourceId) { + if (!identity.isSystem()) + log.info("Read on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); + switch (resourceId) { + case 0: + return ReadResponse.success(resourceId, getManufacturer()); + case 1: + return ReadResponse.success(resourceId, getModelNumber()); + case 2: + return ReadResponse.success(resourceId, getSerialNumber()); + case 3: + return ReadResponse.success(resourceId, getFirmwareVersion()); + case 6: + return ReadResponse.success(resourceId, getAvailablePowerSources(), ResourceModel.Type.INTEGER); + case 9: + return ReadResponse.success(resourceId, getBatteryLevel()); + case 10: + return ReadResponse.success(resourceId, getMemoryFree()); + case 11: + Map errorCodes = new HashMap<>(); + errorCodes.put(0, getErrorCode()); + return ReadResponse.success(resourceId, errorCodes, ResourceModel.Type.INTEGER); + case 14: + return ReadResponse.success(resourceId, getUtcOffset()); + case 15: + return ReadResponse.success(resourceId, getTimezone()); + case 16: + return ReadResponse.success(resourceId, getSupportedBinding()); + case 17: + return ReadResponse.success(resourceId, getDeviceType()); + case 18: + return ReadResponse.success(resourceId, getHardwareVersion()); + case 19: + return ReadResponse.success(resourceId, getSoftwareVersion()); + case 20: + return ReadResponse.success(resourceId, getBatteryStatus()); + case 21: + return ReadResponse.success(resourceId, getMemoryTotal()); + default: + return super.read(identity, resourceId); + } + } + + @Override + public ExecuteResponse execute(LwM2mServer identity, int resourceId, Arguments arguments) { + String withArguments = ""; + if (!arguments.isEmpty()) + withArguments = " with arguments " + arguments; + log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withArguments); + return ExecuteResponse.success(); + } + + @Override + public WriteResponse write(LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) { + log.info("Write on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); + + switch (resourceId) { + case 13: + return WriteResponse.notFound(); + case 14: + setUtcOffset((String) value.getValue()); + fireResourceChange(resourceId); + return WriteResponse.success(); + case 15: + setTimezone((String) value.getValue()); + fireResourceChange(resourceId); + return WriteResponse.success(); + default: + return super.write(identity, replace, resourceId, value); + } + } + + private String getManufacturer() { + return "Thingsboard Demo Lwm2mDevice"; + } + + private String getModelNumber() { + return "Model 500"; + } + + private String getSerialNumber() { + return "Thingsboard-500-000-0001"; + } + + private String getFirmwareVersion() { + return "1.0.2"; + } + + private long getErrorCode() { + return 0; + } + + private Map getAvailablePowerSources() { + Map availablePowerSources = new HashMap<>(); + availablePowerSources.put(0, 1L); + availablePowerSources.put(1, 2L); + availablePowerSources.put(2, 5L); + return availablePowerSources; + } + + private int getBatteryLevel() { + return randomIterator.nextInt(); +// return 42; + } + + private long getMemoryFree() { + return Runtime.getRuntime().freeMemory() / 1024; + } + + private String utcOffset = new SimpleDateFormat("X").format(Calendar.getInstance().getTime()); + + private String getUtcOffset() { + return utcOffset; + } + + private void setUtcOffset(String t) { + utcOffset = t; + } + + private String timeZone = TimeZone.getDefault().getID(); + + private String getTimezone() { + return timeZone; + } + + private void setTimezone(String t) { + timeZone = t; + } + + private String getSupportedBinding() { + return "U"; + } + + private String getDeviceType() { + return "Demo"; + } + + private String getHardwareVersion() { + return "1.0.1"; + } + + private String getSoftwareVersion() { + return "1.0.2"; + } + + private int getBatteryStatus() { + return RANDOM.nextInt(7); + } + + private long getMemoryTotal() { + return Runtime.getRuntime().totalMemory() / 1024; + } + + @Override + public List getAvailableResourceIds(ObjectModel model) { + return supportedResources; + } + + @Override + public void destroy() { + } +} diff --git a/msa/black-box-tests/src/test/resources/lwm2m-registry/0.xml b/msa/black-box-tests/src/test/resources/lwm2m-registry/0.xml new file mode 100644 index 0000000000..81e8523880 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/lwm2m-registry/0.xml @@ -0,0 +1,405 @@ + + + + + + + LWM2M Security + + 0 + urn:oma:lwm2m:oma:0:1.2 + 1.1 + 1.2 + Multiple + Mandatory + + + LWM2M Server URI + + Single + Mandatory + String + 0..255 + + + + + Bootstrap-Server + + Single + Mandatory + Boolean + + + + + + Security Mode + + Single + Mandatory + Integer + 0..4 + + + + + Public Key or Identity + + Single + Mandatory + Opaque + + + + + + Server Public Key + + Single + Mandatory + Opaque + + + + + + Secret Key + + Single + Mandatory + Opaque + + + + + + SMS Security Mode + + Single + Optional + Integer + 0..255 + + + + + SMS Binding Key Parameters + + Single + Optional + Opaque + 6 + + + + + SMS Binding Secret Key(s) + + Single + Optional + Opaque + 16,32,48 + + + + + LwM2M Server SMS Number + + Single + Optional + String + + + + + + Short Server ID + + Single + Optional + Integer + 1..65534 + + + + + Client Hold Off Time + + Single + Optional + Integer + + s + + + + Bootstrap-Server Account Timeout + + Single + Optional + Integer + + s + + + + Matching Type + + Single + Optional + Integer + 0..3 + + + + + SNI + + Single + Optional + String + + + + + + Certificate Usage + + Single + Optional + Integer + 0..3 + + + + + DTLS/TLS Ciphersuite + + Multiple + Optional + Integer + + + + + OSCORE Security Mode + + Single + Optional + Objlnk + + + + + + Groups To Use by Client + + Multiple + Optional + Integer + 0..65535 + + + + + Signature Algorithms Supported by Server + + Multiple + Optional + Integer + 0..65535 + + + + Signature Algorithms To Use by Client + + Multiple + Optional + Integer + 0..65535 + + + + + Signature Algorithm Certs Supported by Server + + Multiple + Optional + Integer + 0..65535 + + + + + TLS 1.3 Features To Use by Client + + Single + Optional + Integer + 0..65535 + + + + + TLS Extensions Supported by Server + + Single + Optional + Integer + 0..65535 + + + + + TLS Extensions To Use by Client + + Single + Optional + Integer + 0..65535 + + + + + Secondary LwM2M Server URI + + Multiple + Optional + String + 0..255 + + + + MQTT Server + + Single + Optional + Objlnk + + + + + LwM2M COSE Security + + Multiple + Optional + Objlnk + + + + + RDS Destination Port + + Single + Optional + Integer + 0..15 + + + + RDS Source Port + + Single + Optional + Integer + 0..15 + + + + RDS Application ID + + Single + Optional + String + + + + + + + + diff --git a/msa/black-box-tests/src/test/resources/lwm2m-registry/1.xml b/msa/black-box-tests/src/test/resources/lwm2m-registry/1.xml new file mode 100644 index 0000000000..f31e839c96 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/lwm2m-registry/1.xml @@ -0,0 +1,360 @@ + + + + + + + LwM2M Server + + 1 + urn:oma:lwm2m:oma:1:1.2 + 1.2 + 1.2 + Multiple + Mandatory + + + Short Server ID + R + Single + Mandatory + Integer + 1..65534 + + + + + Lifetime + RW + Single + Mandatory + Integer + + s + + + + Default Minimum Period + RW + Single + Optional + Integer + + s + + + + Default Maximum Period + RW + Single + Optional + Integer + + s + + + + Disable + E + Single + Optional + + + + + + + Disable Timeout + RW + Single + Optional + Integer + + s + + + + Notification Storing When Disabled or Offline + RW + Single + Mandatory + Boolean + + + + + + Binding + RW + Single + Mandatory + String + + + + + + Registration Update Trigger + E + Single + Mandatory + + + + + + + Bootstrap-Request Trigger + E + Single + Optional + + + + + + + APN Link + RW + Single + Optional + Objlnk + + + + + + TLS-DTLS Alert Code + R + Single + Optional + Integer + 0..255 + + + + + Last Bootstrapped + R + Single + Optional + Time + + + + + + Registration Priority Order + R + Single + Optional + Integer + + + + + + Initial Registration Delay Timer + RW + Single + Optional + Integer + + s + + + + Registration Failure Block + R + Single + Optional + Boolean + + + + + + Bootstrap on Registration Failure + R + Single + Optional + Boolean + + + + + + Communication Retry Count + RW + Single + Optional + Integer + + + + + + Communication Retry Timer + RW + Single + Optional + Integer + + s + + + + Communication Sequence Delay Timer + RW + Single + Optional + Integer + + s + + + + Communication Sequence Retry Count + RW + Single + Optional + Integer + + + + + + Trigger + RW + Single + Optional + Boolean + + + + + + Preferred Transport + RW + Single + Optional + String + The possible values are those listed in the LwM2M Core Specification + + + + Mute Send + RW + Single + Optional + Boolean + + + + + + Alternate APN Links + RW + Multiple + Optional + Objlnk + + + + + + Supported Server Versions + RW + Multiple + Optional + String + + + + + + Default Notification Mode + RW + Single + Optional + Integer + 0..1 + + + + + Profile ID Hash Algorithm + RW + Single + Optional + Integer + 0..255 + + + + + + + diff --git a/msa/black-box-tests/src/test/resources/lwm2m-registry/2.xml b/msa/black-box-tests/src/test/resources/lwm2m-registry/2.xml new file mode 100644 index 0000000000..4ea5805b36 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/lwm2m-registry/2.xml @@ -0,0 +1,123 @@ + + + + + + + LwM2M Access Control + + 2 + urn:oma:lwm2m:oma:2:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Object ID + R + Single + Mandatory + Integer + 1..65534 + + + + + Object Instance ID + R + Single + Mandatory + Integer + 0..65535 + + + + + ACL + RW + Multiple + Optional + Integer + 0..31 + + + + + Access Control Owner + RW + Single + Mandatory + Integer + 0..65535 + + + + + + + diff --git a/msa/black-box-tests/src/test/resources/lwm2m-registry/3.xml b/msa/black-box-tests/src/test/resources/lwm2m-registry/3.xml new file mode 100644 index 0000000000..e71c2045c2 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/lwm2m-registry/3.xml @@ -0,0 +1,331 @@ + + + + + + + Device + + 3 + urn:oma:lwm2m:oma:3:1.0 + 1.1 + 1.2 + Single + Mandatory + + + Manufacturer + R + Single + Optional + String + + + + + + Model Number + R + Single + Optional + String + + + + + + Serial Number + R + Single + Optional + String + + + + + + Firmware Version + R + Single + Optional + String + + + + + + Reboot + E + Single + Mandatory + + + + + + + Factory Reset + E + Single + Optional + + + + + + + Available Power Sources + R + Multiple + Optional + Integer + 0..7 + + + + + Power Source Voltage + R + Multiple + Optional + Integer + + + + + + Power Source Current + R + Multiple + Optional + Integer + + + + + + Battery Level + R + Single + Optional + Integer + 0..100 + /100 + + + + Memory Free + R + Single + Optional + Integer + + + + + + Error Code + R + Multiple + Mandatory + Integer + 0..32 + + + + + Reset Error Code + E + Single + Optional + + + + + + + Current Time + RW + Single + Optional + Time + + + + + + UTC Offset + RW + Single + Optional + String + + + + + + Timezone + RW + Single + Optional + String + + + + + + Supported Binding and Modes + R + Single + Mandatory + String + + + + + Device Type + R + Single + Optional + String + + + + + Hardware Version + R + Single + Optional + String + + + + + Software Version + R + Single + Optional + String + + + + + Battery Status + R + Single + Optional + Integer + 0..6 + + + + Memory Total + R + Single + Optional + Integer + + + + + ExtDevInfo + R + Multiple + Optional + Objlnk + + + + + + + diff --git a/msa/black-box-tests/src/test/resources/lwm2m-registry/5.xml b/msa/black-box-tests/src/test/resources/lwm2m-registry/5.xml new file mode 100644 index 0000000000..ddcea323b0 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/lwm2m-registry/5.xml @@ -0,0 +1,204 @@ + + + + + + + Firmware Update V_1_0 + + 5 + urn:oma:lwm2m:oma:5 + 1.0 + 1.2 + Single + Optional + + + Package + W + Single + Mandatory + Opaque + + + + + + Package URI + RW + Single + Mandatory + String + 0..255 + + + + + Update + E + Single + Mandatory + + + + + + + State + R + Single + Mandatory + Integer + 0..3 + + + + + Update Result + R + Single + Mandatory + Integer + 0..9 + + + + + PkgName + R + Single + Optional + String + 0..255 + + + + + PkgVersion + R + Single + Optional + String + 0..255 + + + + + Firmware Update Protocol Support + R + Multiple + Optional + Integer + 0..5 + + + + + Firmware Update Delivery Method + R + Single + Mandatory + Integer + 0..2 + + + + + + + diff --git a/msa/black-box-tests/src/test/resources/tb-transports/lwm2m/conf/logback.xml b/msa/black-box-tests/src/test/resources/tb-transports/lwm2m/conf/logback.xml new file mode 100644 index 0000000000..f8ecd5d96f --- /dev/null +++ b/msa/black-box-tests/src/test/resources/tb-transports/lwm2m/conf/logback.xml @@ -0,0 +1,57 @@ + + + + + + + /var/log/tb-lwm2m-transport/${TB_SERVICE_ID}/tb-lwm2m-transport.log + + /var/log/tb-lwm2m-transport/${TB_SERVICE_ID}/tb-lwm2m-transport.%d{yyyy-MM-dd}.%i.log + 100MB + 30 + 3GB + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index f0869beffa..d0864c013a 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -15,7 +15,7 @@ "dependencies": { "@aws-sdk/client-sqs": "^3.121.0", "@azure/service-bus": "^7.5.1", - "@google-cloud/pubsub": "^3.0.1", + "@google-cloud/pubsub": "^4.3.3", "amqplib": "^0.10.0", "config": "^3.3.7", "express": "^4.18.1", diff --git a/msa/js-executor/yarn.lock b/msa/js-executor/yarn.lock index eb6443e53d..42a32a93c4 100644 --- a/msa/js-executor/yarn.lock +++ b/msa/js-executor/yarn.lock @@ -797,68 +797,68 @@ enabled "2.0.x" kuler "^2.0.0" -"@google-cloud/paginator@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-4.0.0.tgz#9c3e01544717aecb9a922b4269ff298f30a0f1bb" - integrity sha512-wNmCZl+2G2DmgT/VlF+AROf80SoaC/CwS8trwmjNaq26VRNK8yPbU5F/Vy+R9oDAGKWQU2k8+Op5H4kFJVXFaQ== +"@google-cloud/paginator@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-5.0.0.tgz#b8cc62f151685095d11467402cbf417c41bf14e6" + integrity sha512-87aeg6QQcEPxGCOthnpUjvw4xAZ57G7pL8FS0C4e/81fr3FjkpUpibf1s2v5XGyGhUVGF4Jfg7yEcxqn2iUw1w== dependencies: arrify "^2.0.0" extend "^3.0.2" -"@google-cloud/precise-date@^2.0.0": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@google-cloud/precise-date/-/precise-date-2.0.3.tgz#14f6f28ce35dabf3882e7aeab1c9d51bd473faed" - integrity sha512-+SDJ3ZvGkF7hzo6BGa8ZqeK3F6Z4+S+KviC9oOK+XCs3tfMyJCh/4j93XIWINgMMDIh9BgEvlw4306VxlXIlYA== +"@google-cloud/precise-date@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@google-cloud/precise-date/-/precise-date-4.0.0.tgz#e179893a3ad628b17a6fabdfcc9d468753aac11a" + integrity sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA== -"@google-cloud/projectify@^2.0.0": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-2.0.1.tgz#13350ee609346435c795bbfe133a08dfeab78d65" - integrity sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ== +"@google-cloud/projectify@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-4.0.0.tgz#d600e0433daf51b88c1fa95ac7f02e38e80a07be" + integrity sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA== -"@google-cloud/promisify@^2.0.0": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-2.0.2.tgz#81d654b4cb227c65c7ad2f9a7715262febd409ed" - integrity sha512-EvuabjzzZ9E2+OaYf+7P9OAiiwbTxKYL0oGLnREQd+Su2NTQBpomkdlkBowFvyWsaV0d1sSGxrKpSNcrhPqbxg== +"@google-cloud/promisify@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-4.0.0.tgz#a906e533ebdd0f754dca2509933334ce58b8c8b1" + integrity sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g== -"@google-cloud/pubsub@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@google-cloud/pubsub/-/pubsub-3.0.1.tgz#3a6bb9649a1b4309d82d8670a9c01375e622692f" - integrity sha512-dznNbRd/Y8J0C0xvdvCPi3B1msK/dj/Nya+NQZ2doUOLT6eoa261tBwk9umOQs5L5GKcdlqQKbBjrNjDYVbzQA== - dependencies: - "@google-cloud/paginator" "^4.0.0" - "@google-cloud/precise-date" "^2.0.0" - "@google-cloud/projectify" "^2.0.0" - "@google-cloud/promisify" "^2.0.0" - "@opentelemetry/api" "^1.0.0" - "@opentelemetry/semantic-conventions" "^1.0.0" +"@google-cloud/pubsub@^4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@google-cloud/pubsub/-/pubsub-4.3.3.tgz#3d3f947ae8fca1694388592f22f77fdc3008ee8d" + integrity sha512-vJKh9L4dHf1XGSDKS1SB0IpqP/sUajQh4/QwhYasuq/NjzfHSxqSt+CuhrFGb5/gioTWE4gce0sn7h1SW7qESg== + dependencies: + "@google-cloud/paginator" "^5.0.0" + "@google-cloud/precise-date" "^4.0.0" + "@google-cloud/projectify" "^4.0.0" + "@google-cloud/promisify" "^4.0.0" + "@opentelemetry/api" "^1.6.0" + "@opentelemetry/semantic-conventions" "~1.21.0" "@types/duplexify" "^3.6.0" "@types/long" "^4.0.0" arrify "^2.0.0" extend "^3.0.2" - google-auth-library "^8.0.2" - google-gax "^3.0.1" + google-auth-library "^9.3.0" + google-gax "^4.3.1" + heap-js "^2.2.0" is-stream-ended "^0.1.4" lodash.snakecase "^4.1.1" p-defer "^3.0.0" -"@grpc/grpc-js@~1.6.0": - version "1.6.7" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.6.7.tgz#4c4fa998ff719fe859ac19fe977fdef097bb99aa" - integrity sha512-eBM03pu9hd3VqDQG+kHahiG1x80RGkkqqRb1Pchcwqej/KkAH95gAvKs6laqaHCycYaPK+TKuNQnOz9UXYA8qw== +"@grpc/grpc-js@~1.10.0": + version "1.10.6" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.10.6.tgz#1e3eb1af911dc888fbef7452f56a7573b8284d54" + integrity sha512-xP58G7wDQ4TCmN/cMUHh00DS7SRDv/+lC+xFLrTkMIN8h55X5NhZMLYbvy7dSELP15qlI6hPhNCRWVMtZMwqLA== dependencies: - "@grpc/proto-loader" "^0.6.4" - "@types/node" ">=12.12.47" + "@grpc/proto-loader" "^0.7.10" + "@js-sdsl/ordered-map" "^4.4.2" -"@grpc/proto-loader@^0.6.12", "@grpc/proto-loader@^0.6.4": - version "0.6.13" - resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.6.13.tgz#008f989b72a40c60c96cd4088522f09b05ac66bc" - integrity sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g== +"@grpc/proto-loader@^0.7.0", "@grpc/proto-loader@^0.7.10": + version "0.7.12" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.12.tgz#787b58e3e3771df30b1567c057b6ab89e3a42911" + integrity sha512-DCVwMxqYzpUCiDMl7hQ384FqP4T3DbNpXU8pt681l3UWCip1WUiD5JrkImUwCB9a7f2cq4CUTmi5r/xIMRPY1Q== dependencies: - "@types/long" "^4.0.1" lodash.camelcase "^4.3.0" - long "^4.0.0" - protobufjs "^6.11.3" - yargs "^16.2.0" + long "^5.0.0" + protobufjs "^7.2.4" + yargs "^17.7.2" "@jridgewell/resolve-uri@^3.0.3": version "3.0.8" @@ -878,6 +878,11 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@js-sdsl/ordered-map@^4.4.2": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c" + integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw== + "@nodelib/fs.scandir@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" @@ -899,15 +904,20 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" -"@opentelemetry/api@^1.0.0", "@opentelemetry/api@^1.0.1": +"@opentelemetry/api@^1.0.1": version "1.1.0" resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.1.0.tgz#563539048255bbe1a5f4f586a4a10a1bb737f44a" integrity sha512-hf+3bwuBwtXsugA2ULBc95qxrOqP2pOekLz34BJhcAKawt94vfeNyUKpYc0lZQ/3sCP6LqRa7UAdHA7i5UODzQ== -"@opentelemetry/semantic-conventions@^1.0.0": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.3.1.tgz#ba07b864a3c955f061aa30ea3ef7f4ae4449794a" - integrity sha512-wU5J8rUoo32oSef/rFpOT1HIjLjAv3qIDHkw1QIhODV3OpAVHi5oVzlouozg9obUmZKtbZ0qUe/m7FP0y0yBzA== +"@opentelemetry/api@^1.6.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.8.0.tgz#5aa7abb48f23f693068ed2999ae627d2f7d902ec" + integrity sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w== + +"@opentelemetry/semantic-conventions@~1.21.0": + version "1.21.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.21.0.tgz#83f7479c524ab523ac2df702ade30b9724476c72" + integrity sha512-lkC8kZYntxVKr7b8xmjCVUgE0a8xgDakPyDo9uSWavXPyYqLgYYGdEd2j8NxihRyb6UwpX3G/hFUF4/9q2V+/g== "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" @@ -974,6 +984,11 @@ dependencies: defer-to-connect "^1.0.1" +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + "@tsconfig/node10@^1.0.7": version "1.0.9" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" @@ -1015,6 +1030,11 @@ "@types/connect" "*" "@types/node" "*" +"@types/caseless@*": + version "0.12.5" + resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.5.tgz#db9468cb1b1b5a925b8f34822f1669df0c5472f5" + integrity sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg== + "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" @@ -1088,7 +1108,7 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.1.tgz#fdf6f6c6c73d3d8eee9c98a9a0485bc524b048d7" integrity sha512-HnYlg/BRF8uC1FyKRFZwRaCPTPYKa+6I8QiUZFLredaGOou481cgFS4wKRFyKvQtX8xudqkSdBczJHIYSQYKrQ== -"@types/node@>=12.12.47", "@types/node@>=13.7.0": +"@types/node@>=13.7.0": version "17.0.42" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.42.tgz#d7e8f22700efc94d125103075c074396b5f41f9b" integrity sha512-Q5BPGyGKcvQgAMbsr7qEGN/kIPN6zZecYYABeTDBizOsau+2NMdSVTar9UQw21A2+JyA2KRNDYaYrPB0Rpk2oQ== @@ -1108,6 +1128,16 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== +"@types/request@^2.48.8": + version "2.48.12" + resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.12.tgz#0f590f615a10f87da18e9790ac94c29ec4c5ef30" + integrity sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw== + dependencies: + "@types/caseless" "*" + "@types/node" "*" + "@types/tough-cookie" "*" + form-data "^2.5.0" + "@types/serve-static@*": version "1.13.10" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9" @@ -1116,6 +1146,11 @@ "@types/mime" "^1" "@types/node" "*" +"@types/tough-cookie@*": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" + integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== + "@types/tunnel@^0.0.3": version "0.0.3" resolved "https://registry.yarnpkg.com/@types/tunnel/-/tunnel-0.0.3.tgz#f109e730b072b3136347561fc558c9358bb8c6e9" @@ -1167,6 +1202,13 @@ agent-base@6: dependencies: debug "4" +agent-base@^7.0.2: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" + amqplib@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/amqplib/-/amqplib-0.10.0.tgz#766d696f8ceae097ee9eb73e6796999e5d40a1db" @@ -1462,6 +1504,15 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + clone-response@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" @@ -1522,7 +1573,7 @@ colorspace@1.1.x: color "3.0.x" text-hex "1.0.x" -combined-stream@^1.0.8: +combined-stream@^1.0.6, combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -1609,7 +1660,7 @@ debug@2.6.9, debug@~2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.1.1: +debug@4: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -1623,6 +1674,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + decompress-response@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" @@ -1921,11 +1979,6 @@ fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= -fast-text-encoding@^1.0.0, fast-text-encoding@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" - integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== - fast-xml-parser@3.19.0: version "3.19.0" resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz#cb637ec3f3999f51406dd8ff0e6fc4d83e520d01" @@ -1982,6 +2035,15 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +form-data@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + form-data@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" @@ -2081,34 +2143,23 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" -gaxios@^4.0.0: - version "4.3.3" - resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-4.3.3.tgz#d44bdefe52d34b6435cc41214fdb160b64abfc22" - integrity sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA== +gaxios@^6.0.0, gaxios@^6.1.1: + version "6.4.0" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.4.0.tgz#08a42cb44d5123a72efaaf9f786c266e7f18be70" + integrity sha512-apAloYrY4dlBGlhauDAYSZveafb5U6+L9titing1wox6BvWM0TSXBp603zTrLpyLMGkrcFgohnUN150dFN/zOA== dependencies: - abort-controller "^3.0.0" extend "^3.0.2" - https-proxy-agent "^5.0.0" + https-proxy-agent "^7.0.1" is-stream "^2.0.0" - node-fetch "^2.6.7" - -gaxios@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-5.0.0.tgz#df11e5d0a45831dd39eb5fbbba0d6a6b09815e70" - integrity sha512-VD/yc5ln6XU8Ch1hyYY6kRMBE0Yc2np3fPyeJeYHhrPs1i8rgnsApPMWyrugkl7LLoSqpOJVBWlQIa87OAvt8Q== - dependencies: - abort-controller "^3.0.0" - extend "^3.0.2" - https-proxy-agent "^5.0.0" - is-stream "^2.0.0" - node-fetch "^2.6.7" + node-fetch "^2.6.9" + uuid "^9.0.1" -gcp-metadata@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-5.0.0.tgz#a00f999f60a4461401e7c515f8a3267cfb401ee7" - integrity sha512-gfwuX3yA3nNsHSWUL4KG90UulNiq922Ukj3wLTrcnX33BB7PwB1o0ubR8KVvXu9nJH+P5w1j2SQSNNqto+H0DA== +gcp-metadata@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-6.1.0.tgz#9b0dd2b2445258e7597f2024332d20611cbd6b8c" + integrity sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg== dependencies: - gaxios "^5.0.0" + gaxios "^6.0.0" json-bigint "^1.0.0" get-caller-file@^2.0.5: @@ -2178,46 +2229,35 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" -google-auth-library@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-8.0.2.tgz#5fa0f2d3795c3e4019d2bb315ade4454cc9c30b5" - integrity sha512-HoG+nWFAThLovKpvcbYzxgn+nBJPTfAwtq0GxPN821nOO+21+8oP7MoEHfd1sbDulUFFGfcjJr2CnJ4YssHcyg== +google-auth-library@^9.3.0: + version "9.7.0" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.7.0.tgz#dd99a08e2e3f70778de8be4ed8556460e237550a" + integrity sha512-I/AvzBiUXDzLOy4iIZ2W+Zq33W4lcukQv1nl7C8WUA6SQwyQwUwu3waNmWNAvzds//FG8SZ+DnKnW/2k6mQS8A== dependencies: - arrify "^2.0.0" base64-js "^1.3.0" ecdsa-sig-formatter "^1.0.11" - fast-text-encoding "^1.0.0" - gaxios "^5.0.0" - gcp-metadata "^5.0.0" - gtoken "^5.3.2" + gaxios "^6.1.1" + gcp-metadata "^6.1.0" + gtoken "^7.0.0" jws "^4.0.0" - lru-cache "^6.0.0" -google-gax@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-3.1.0.tgz#a6fea06bfd0157969ddc0a6d04c47980978ead02" - integrity sha512-OHKVHZtaYHwxrjiI4BH+IzRWH2Q75sZg+q0/RatxLicbeeCCIug99+NKQiE39B5rLnGENtcbUjmXiqJHbSrkSg== +google-gax@^4.3.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-4.3.2.tgz#417cbee97f2e68d78f641af19c0f15234c0dbd9c" + integrity sha512-2mw7qgei2LPdtGrmd1zvxQviOcduTnsvAWYzCxhOWXK4IQKmQztHnDQwD0ApB690fBQJemFKSU7DnceAy3RLzw== dependencies: - "@grpc/grpc-js" "~1.6.0" - "@grpc/proto-loader" "^0.6.12" + "@grpc/grpc-js" "~1.10.0" + "@grpc/proto-loader" "^0.7.0" "@types/long" "^4.0.0" abort-controller "^3.0.0" duplexify "^4.0.0" - fast-text-encoding "^1.0.3" - google-auth-library "^8.0.2" - is-stream-ended "^0.1.4" + google-auth-library "^9.3.0" node-fetch "^2.6.1" object-hash "^3.0.0" - proto3-json-serializer "^1.0.0" - protobufjs "6.11.3" - retry-request "^5.0.0" - -google-p12-pem@^3.1.3: - version "3.1.4" - resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-3.1.4.tgz#123f7b40da204de4ed1fbf2fd5be12c047fc8b3b" - integrity sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg== - dependencies: - node-forge "^1.3.1" + proto3-json-serializer "^2.0.0" + protobufjs "7.2.6" + retry-request "^7.0.0" + uuid "^9.0.1" got@^9.6.0: version "9.6.0" @@ -2241,13 +2281,12 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== -gtoken@^5.3.2: - version "5.3.2" - resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.3.2.tgz#deb7dc876abe002178e0515e383382ea9446d58f" - integrity sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ== +gtoken@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-7.1.0.tgz#d61b4ebd10132222817f7222b1e6064bd463fc26" + integrity sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw== dependencies: - gaxios "^4.0.0" - google-p12-pem "^3.1.3" + gaxios "^6.0.0" jws "^4.0.0" has-bigints@^1.0.1, has-bigints@^1.0.2: @@ -2301,6 +2340,11 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +heap-js@^2.2.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/heap-js/-/heap-js-2.5.0.tgz#487e268b1733b187ca04eccf52f8387be92b46cb" + integrity sha512-kUGoI3p7u6B41z/dp33G6OaL7J4DRqRYwVmeIlwLClx7yaaAy7hoDExnuejTKtuDwfcatGmddHDEOjf6EyIxtQ== + http-cache-semantics@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" @@ -2317,6 +2361,15 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + https-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" @@ -2325,6 +2378,14 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" +https-proxy-agent@^7.0.1: + version "7.0.4" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz#8e97b841a029ad8ddc8731f26595bad868cb4168" + integrity sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg== + dependencies: + agent-base "^7.0.2" + debug "4" + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -2728,6 +2789,11 @@ long@^4.0.0: resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +long@^5.0.0: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + long@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/long/-/long-5.2.0.tgz#2696dadf4b4da2ce3f6f6b89186085d94d52fd61" @@ -2868,16 +2934,16 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= +ms@2.1.2, ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + ms@2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -ms@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - multistream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/multistream/-/multistream-4.1.0.tgz#7bf00dfd119556fbc153cff3de4c6d477909f5a8" @@ -2903,18 +2969,13 @@ node-abi@^2.21.0: dependencies: semver "^5.4.1" -node-fetch@^2.6.1, node-fetch@^2.6.6, node-fetch@^2.6.7: +node-fetch@^2.6.1, node-fetch@^2.6.6, node-fetch@^2.6.7, node-fetch@^2.6.9: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== dependencies: whatwg-url "^5.0.0" -node-forge@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" - integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== - nodemon@^2.0.16: version "2.0.16" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.16.tgz#d71b31bfdb226c25de34afea53486c8ef225fdef" @@ -3164,17 +3225,17 @@ progress@^2.0.3: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -proto3-json-serializer@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/proto3-json-serializer/-/proto3-json-serializer-1.0.1.tgz#5d2b0b3c85568edb62984c94ce2e726985de44be" - integrity sha512-jtnGKL8EE1mTOl2qgMZJQXbS22dlb2K07i4b5vp8yMGMJ6mFSgBg4Ossu/g9NCuamdYXHjp3XxyWw4tJ0G/uKw== +proto3-json-serializer@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/proto3-json-serializer/-/proto3-json-serializer-2.0.1.tgz#da0b510f6d6e584b1b5c271f045c26728abe71e0" + integrity sha512-8awBvjO+FwkMd6gNoGFZyqkHZXCFd54CIYTb6De7dPaufGJ2XNW+QUNqbMr8MaAocMdb+KpsD4rxEOaTBDCffA== dependencies: - protobufjs "^6.11.3" + protobufjs "^7.2.5" -protobufjs@6.11.3, protobufjs@^6.11.3: - version "6.11.3" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.3.tgz#637a527205a35caa4f3e2a9a4a13ddffe0e7af74" - integrity sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg== +protobufjs@7.2.6, protobufjs@^7.2.4, protobufjs@^7.2.5: + version "7.2.6" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.6.tgz#4a0ccd79eb292717aacf07530a07e0ed20278215" + integrity sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw== dependencies: "@protobufjs/aspromise" "^1.1.2" "@protobufjs/base64" "^1.1.2" @@ -3186,9 +3247,8 @@ protobufjs@6.11.3, protobufjs@^6.11.3: "@protobufjs/path" "^1.1.2" "@protobufjs/pool" "^1.1.0" "@protobufjs/utf8" "^1.1.0" - "@types/long" "^4.0.1" "@types/node" ">=13.7.0" - long "^4.0.0" + long "^5.0.0" proxy-addr@~2.0.7: version "2.0.7" @@ -3363,13 +3423,14 @@ responselike@^1.0.2: dependencies: lowercase-keys "^1.0.0" -retry-request@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-5.0.0.tgz#886ff8ec0e77fffbe66a4d5e90fd8f6646b6eae4" - integrity sha512-vBZdBxUordje9253imlmGtppC5gdcwZmNz7JnU2ui+KKFPk25keR+0c020AVV20oesYxIFOI0Kh3HE88/59ieg== +retry-request@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-7.0.2.tgz#60bf48cfb424ec01b03fca6665dee91d06dd95f3" + integrity sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w== dependencies: - debug "^4.1.1" + "@types/request" "^2.48.8" extend "^3.0.2" + teeny-request "^9.0.0" reusify@^1.0.4: version "1.0.4" @@ -3547,6 +3608,13 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +stream-events@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" + integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg== + dependencies: + stubs "^3.0.0" + stream-meter@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/stream-meter/-/stream-meter-1.0.4.tgz#52af95aa5ea760a2491716704dbff90f73afdd1d" @@ -3594,7 +3662,7 @@ string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string-width@^4.2.2: +string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -3680,6 +3748,11 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= +stubs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" + integrity sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw== + supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -3720,6 +3793,17 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" +teeny-request@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-9.0.0.tgz#18140de2eb6595771b1b02203312dfad79a4716d" + integrity sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g== + dependencies: + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + node-fetch "^2.6.9" + stream-events "^1.0.5" + uuid "^9.0.0" + text-hex@1.0.x: version "1.0.0" resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" @@ -3973,6 +4057,11 @@ uuid@^8.3.0, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@^9.0.0, uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" @@ -4138,6 +4227,11 @@ yargs-parser@^20.2.2: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + yargs@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" @@ -4151,6 +4245,19 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" diff --git a/msa/tb/README.md b/msa/tb/README.md index 6987c42bfa..2ece5a3e2b 100644 --- a/msa/tb/README.md +++ b/msa/tb/README.md @@ -21,7 +21,7 @@ In this example `thingsboard/tb` image will be used. You can choose any other im Execute the following command to run this docker directly: ` -$ docker run -it -p 9090:9090 -p 1883:1883 -p 5683:5683/udp -p 5685:5685/udp -v ~/.mytb-data:/data --name mytb thingsboard/tb +$ docker run -it -p 9090:9090 -p 1883:1883 -p 5683:5683/udp -p 5685:5685/udp -p 5686:5686/udp -v ~/.mytb-data:/data --name mytb thingsboard/tb ` Where: @@ -32,6 +32,7 @@ Where: - `-p 1883:1883` - connect local port 1883 to exposed internal MQTT port 1883 - `-p 5683:5683` - connect local port 5683 to exposed internal COAP port 5683 - `-p 5685:5685` - connect local port 5685 to exposed internal COAP port 5685 (lwm2m) +- `-p 5686:5686` - connect local port 5686 to exposed internal COAPS port 5686 (lwm2m) - `-v ~/.mytb-data:/data` - mounts the host's dir `~/.mytb-data` to ThingsBoard DataBase data directory - `--name mytb` - friendly local name of this machine - `thingsboard/tb` - docker image, can be also `thingsboard/tb-postgres` or `thingsboard/tb-cassandra` @@ -47,6 +48,7 @@ Where: > $ VBoxManage controlvm "default" natpf1 "tcp-port1883,tcp,,1883,,1883" > $ VBoxManage controlvm "default" natpf1 "tcp-port5683,tcp,,5683,,5683" > $ VBoxManage controlvm "default" natpf1 "tcp-port5683,tcp,,5685,,5685" +> $ VBoxManage controlvm "default" natpf1 "tcp-port5683,tcp,,5686,,5686" > ``` After executing `docker run` command you can open `http://{your-host-ip}:9090` in you browser (for ex. `http://localhost:9090`). You should see ThingsBoard login page. diff --git a/msa/tb/docker-cassandra/Dockerfile b/msa/tb/docker-cassandra/Dockerfile index 3d4997dd88..18071b249b 100644 --- a/msa/tb/docker-cassandra/Dockerfile +++ b/msa/tb/docker-cassandra/Dockerfile @@ -87,6 +87,7 @@ EXPOSE 9090 EXPOSE 1883 EXPOSE 5683/udp EXPOSE 5685/udp +EXPOSE 5686/udp VOLUME ["/data"] diff --git a/msa/tb/docker-postgres/Dockerfile b/msa/tb/docker-postgres/Dockerfile index dcf766f00e..984e390496 100644 --- a/msa/tb/docker-postgres/Dockerfile +++ b/msa/tb/docker-postgres/Dockerfile @@ -69,6 +69,7 @@ EXPOSE 9090 EXPOSE 1883 EXPOSE 5683/udp EXPOSE 5685/udp +EXPOSE 5686/udp VOLUME ["/data"] diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index ad0fa66cb9..70fea2c0a5 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -38,7 +38,7 @@ tb-postgres tb-cassandra /usr/share/${pkg.name} - 3.6.3 + 3.6.4 diff --git a/pom.xml b/pom.xml index 70a35d77d2..186e9b9343 100755 --- a/pom.xml +++ b/pom.xml @@ -72,13 +72,13 @@ 1.3.4 4.2.1 2.2.6 - 3.10.0 + 3.11.0 2.0.0-M14 2.9.0 2.3.30 2.0.1 5.5.0 - 3.8.1 + 3.9.2 3.21.9 1.42.1 1.2.0 @@ -98,12 +98,12 @@ 1.18.2 1.69 2.0.1 - 42.5.0 + 42.7.3 org/thingsboard/server/gen/**/*, org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/* 5.0.2 - 0.2.1 + 0.4.2 15.4 +
+ + + +
+ + + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.scss new file mode 100644 index 0000000000..a71d07371e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.scss @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../scss/constants'; + +.tb-comparison-key-row { + .tb-show-field { + width: 40px; + min-width: 40px; + } + + .tb-data-key-input { + flex: 1; + @media #{$mat-gt-xs} { + min-width: 100px; + flex: 1 1 40%; + } + } + + .tb-label-field, .tb-color-field { + display: flex; + flex-direction: row; + place-content: center; + align-items: center; + .tb-inline-field { + flex: 1; + } + } + + .tb-label-field { + flex: 1; + @media #{$mat-gt-xs} { + min-width: 150px; + flex: 1 1 60%; + } + } + + .tb-color-field { + width: 40px; + min-width: 40px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.ts new file mode 100644 index 0000000000..8836a31f8b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.ts @@ -0,0 +1,131 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup +} from '@angular/forms'; +import { + DataKey, + DataKeyComparisonSettings, + DataKeySettingsWithComparison, + DatasourceType +} from '@shared/models/widget.models'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-comparison-key-row', + templateUrl: './comparison-key-row.component.html', + styleUrls: ['./comparison-key-row.component.scss', '../../data-keys.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ComparisonKeyRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ComparisonKeyRowComponent implements ControlValueAccessor, OnInit { + + @Input() + disabled: boolean; + + @Input() + datasourceType: DatasourceType; + + keyFormControl: UntypedFormControl; + + keyRowFormGroup: UntypedFormGroup; + + modelValue: DataKey; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef) { + } + + ngOnInit() { + this.keyFormControl = this.fb.control(null, []); + this.keyRowFormGroup = this.fb.group({ + showValuesForComparison: [null, []], + comparisonValuesLabel: [null, []], + color: [null, []] + }); + this.keyRowFormGroup.valueChanges.subscribe( + () => this.updateModel() + ); + this.keyRowFormGroup.get('showValuesForComparison').valueChanges.subscribe(() => this.updateValidators()); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.keyFormControl.disable({emitEvent: false}); + this.keyRowFormGroup.disable({emitEvent: false}); + } else { + this.keyFormControl.enable({emitEvent: false}); + this.keyRowFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: DataKey): void { + this.modelValue = value; + const comparisonSettings = (value?.settings as DataKeySettingsWithComparison)?.comparisonSettings; + this.keyRowFormGroup.patchValue( + comparisonSettings, {emitEvent: false} + ); + this.keyFormControl.patchValue(deepClone(this.modelValue), {emitEvent: false}); + this.updateValidators(); + this.cd.markForCheck(); + } + + private updateValidators() { + const showValuesForComparison: boolean = this.keyRowFormGroup.get('showValuesForComparison').value; + if (showValuesForComparison) { + this.keyFormControl.enable({emitEvent: false}); + this.keyRowFormGroup.get('comparisonValuesLabel').enable({emitEvent: false}); + this.keyRowFormGroup.get('color').enable({emitEvent: false}); + } else { + this.keyFormControl.disable({emitEvent: false}); + this.keyRowFormGroup.get('comparisonValuesLabel').disable({emitEvent: false}); + this.keyRowFormGroup.get('color').disable({emitEvent: false}); + } + } + + private updateModel() { + const comparisonSettings: DataKeyComparisonSettings = this.keyRowFormGroup.value; + if (!this.modelValue.settings) { + this.modelValue.settings = {}; + } + this.modelValue.settings.comparisonSettings = comparisonSettings; + this.propagateChange(this.modelValue); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.html new file mode 100644 index 0000000000..a202a81cf6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.html @@ -0,0 +1,38 @@ + +
+
+
widgets.time-series-chart.comparison.show
+
datakey.key
+
datakey.label
+
datakey.color
+
+
+
+ + +
+
+
+ + {{ 'widgets.chart.no-series' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.scss new file mode 100644 index 0000000000..1d9689a333 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.scss @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../scss/constants'; + +.tb-comparison-keys-table { + .tb-form-table-header-cell { + + &.tb-show-header { + width: 40px; + min-width: 40px; + } + + &.tb-key-header { + flex: 1; + @media #{$mat-gt-xs} { + min-width: 100px; + flex: 1 1 40%; + } + } + + &.tb-label-header { + flex: 1; + @media #{$mat-gt-xs} { + min-width: 150px; + flex: 1 1 60%; + } + } + + &.tb-color-header { + width: 40px; + min-width: 40px; + } + } + + .tb-form-table-body { + tb-comparison-key-row { + overflow: hidden; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.ts new file mode 100644 index 0000000000..b8241531b3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.ts @@ -0,0 +1,110 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormGroup +} from '@angular/forms'; +import { DataKey, DatasourceType } from '@shared/models/widget.models'; + +@Component({ + selector: 'tb-comparison-keys-table', + templateUrl: './comparison-keys-table.component.html', + styleUrls: ['./comparison-keys-table.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ComparisonKeysTableComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ComparisonKeysTableComponent implements ControlValueAccessor, OnInit { + + @Input() + disabled: boolean; + + @Input() + datasourceType: DatasourceType; + + keysListFormGroup: UntypedFormGroup; + + get noKeys(): boolean { + const keys: DataKey[] = this.keysListFormGroup.get('keys').value; + return keys.length === 0; + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder) { + } + + ngOnInit() { + this.keysListFormGroup = this.fb.group({ + keys: [this.fb.array([]), []] + }); + this.keysListFormGroup.valueChanges.subscribe( + () => { + const keys: DataKey[] = this.keysListFormGroup.get('keys').value; + this.propagateChange(keys); + } + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.keysListFormGroup.disable({emitEvent: false}); + } else { + this.keysListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DataKey[] | undefined): void { + this.keysListFormGroup.setControl('keys', this.prepareKeysFormArray(value), {emitEvent: false}); + } + + keysFormArray(): UntypedFormArray { + return this.keysListFormGroup.get('keys') as UntypedFormArray; + } + + trackByKey(_index: number, keyControl: AbstractControl): any { + return keyControl; + } + + private prepareKeysFormArray(keys: DataKey[] | undefined): UntypedFormArray { + const keysControls: Array = []; + if (keys) { + keys.forEach((key) => { + keysControls.push(this.fb.control(key, [])); + }); + } + return this.fb.array(keysControls); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html index 5be56204fa..4045d1d928 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html @@ -75,6 +75,24 @@ {{ 'widgets.range-chart.data-zoom' | translate }} + + + +
+
widgets.range-chart.range-chart-appearance
+
+
widget-config.units-short
+ + +
+
+
widget-config.decimals-short
+ + + +
{{ 'widgets.range-chart.range-colors' | translate }}
@@ -87,24 +105,155 @@ formControlName="outOfRangeColor">
+
+ + {{ 'widgets.range-chart.show-range-thresholds' | translate }} + + + +
{{ 'widgets.range-chart.fill-area' | translate }}
-
-
widget-config.units-short
- - -
-
-
widget-config.decimals-short
- - +
+
widgets.range-chart.fill-area-opacity
+ + +
+
+
widgets.time-series-chart.series.line.line
+
+ + {{ 'widgets.time-series-chart.series.line.show-line' | translate }} + +
+
+ + {{ 'widgets.time-series-chart.series.line.step-line' | translate }} + + + + + {{ lineSeriesStepTypeTranslations.get(stepType) | translate }} + + + +
+
+ + {{ 'widgets.time-series-chart.series.line.smooth-line' | translate }} + +
+
+
widgets.time-series-chart.line-type
+ + + + {{ timeSeriesLineTypeTranslations.get(lineType) | translate }} + + +
+
+
widgets.time-series-chart.line-width
+ + + +
+
+
+
widgets.time-series-chart.series.point.points
+
+ + {{ 'widgets.time-series-chart.series.point.show-points' | translate }} + +
+
+ +
+ {{ 'widgets.time-series-chart.series.point.point-label' | translate }} +
+
+
+ + + + {{ seriesLabelPositionTranslations.get(position) | translate }} + + + + + + + +
+
+
+ + {{ 'widgets.time-series-chart.series.point.point-label-background' | translate }} + + + +
+
+
widgets.time-series-chart.series.point.point-shape
+ + + + {{ echartsShapeTranslations.get(shape) | translate }} + + + +
+
+
widgets.time-series-chart.series.point.point-size
+ + + +
+
+
+
widgets.time-series-chart.axis.y-axis
+ + +
+
+
widgets.time-series-chart.axis.x-axis
+ + +
+ +
@@ -204,6 +353,9 @@
+ +
widget-config.card-appearance
@@ -223,6 +375,12 @@
+
+
{{ 'widget-config.card-padding' | translate }}
+ + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts index 0add69096b..9c39ef8f27 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts @@ -14,20 +14,26 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, Injector } from '@angular/core'; +import { Component, Injector } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; -import { DataKey, legendPositions, legendPositionTranslationMap, WidgetConfig, } from '@shared/models/widget.models'; +import { + DataKey, + Datasource, + legendPositions, + legendPositionTranslationMap, + WidgetConfig, +} from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { getTimewindowConfig, setTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component'; -import { formatValue, isUndefined } from '@core/utils'; +import { formatValue, isUndefined, mergeDeep } from '@core/utils'; import { cssSizeToStrSize, DateFormatProcessor, @@ -38,6 +44,15 @@ import { rangeChartDefaultSettings, RangeChartWidgetSettings } from '@home/components/widget/lib/chart/range-chart-widget.models'; +import { + lineSeriesStepTypes, + lineSeriesStepTypeTranslations, + seriesLabelPositions, + seriesLabelPositionTranslations, + timeSeriesLineTypes, + timeSeriesLineTypeTranslations +} from '@home/components/widget/lib/chart/time-series-chart.models'; +import { echartsShapes, echartsShapeTranslations } from '@home/components/widget/lib/chart/echarts-widget.models'; @Component({ selector: 'tb-range-chart-basic-config', @@ -46,12 +61,39 @@ import { }) export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent { + public get datasource(): Datasource { + const datasources: Datasource[] = this.rangeChartWidgetConfigForm.get('datasources').value; + if (datasources && datasources.length) { + return datasources[0]; + } else { + return null; + } + } + + lineSeriesStepTypes = lineSeriesStepTypes; + + lineSeriesStepTypeTranslations = lineSeriesStepTypeTranslations; + + timeSeriesLineTypes = timeSeriesLineTypes; + + timeSeriesLineTypeTranslations = timeSeriesLineTypeTranslations; + + seriesLabelPositions = seriesLabelPositions; + + seriesLabelPositionTranslations = seriesLabelPositionTranslations; + + echartsShapes = echartsShapes; + + echartsShapeTranslations = echartsShapeTranslations; + legendPositions = legendPositions; legendPositionTranslationMap = legendPositionTranslationMap; rangeChartWidgetConfigForm: UntypedFormGroup; + pointLabelPreviewFn = this._pointLabelPreviewFn.bind(this); + tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this); tooltipDatePreviewFn = this._tooltipDatePreviewFn.bind(this); @@ -72,7 +114,8 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent { } protected onConfigSet(configData: WidgetConfigComponentData) { - const settings: RangeChartWidgetSettings = {...rangeChartDefaultSettings, ...(configData.config.settings || {})}; + const settings: RangeChartWidgetSettings = mergeDeep({} as RangeChartWidgetSettings, + rangeChartDefaultSettings, configData.config.settings as RangeChartWidgetSettings); const iconSize = resolveCssSize(configData.config.iconSize); this.rangeChartWidgetConfigForm = this.fb.group({ timewindowConfig: [getTimewindowConfig(configData.config), []], @@ -90,11 +133,41 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent { iconColor: [configData.config.iconColor, []], dataZoom: [settings.dataZoom, []], + + units: [configData.config.units, []], + decimals: [configData.config.decimals, []], rangeColors: [settings.rangeColors, []], outOfRangeColor: [settings.outOfRangeColor, []], + showRangeThresholds: [settings.showRangeThresholds, []], + rangeThreshold: [settings.rangeThreshold, []], fillArea: [settings.fillArea, []], - units: [configData.config.units, []], - decimals: [configData.config.decimals, []], + fillAreaOpacity: [settings.fillAreaOpacity, [Validators.min(0), Validators.max(1)]], + + showLine: [settings.showLine, []], + step: [settings.step, []], + stepType: [settings.stepType, []], + smooth: [settings.smooth, []], + lineType: [settings.lineType, []], + lineWidth: [settings.lineWidth, [Validators.min(0)]], + + showPoints: [settings.showPoints, []], + showPointLabel: [settings.showPointLabel, []], + pointLabelPosition: [settings.pointLabelPosition, []], + pointLabelFont: [settings.pointLabelFont, []], + pointLabelColor: [settings.pointLabelColor, []], + enablePointLabelBackground: [settings.enablePointLabelBackground, []], + pointLabelBackground: [settings.pointLabelBackground, []], + pointShape: [settings.pointShape, []], + pointSize: [settings.pointSize, [Validators.min(0)]], + + grid: [settings.grid, []], + + yAxis: [settings.yAxis, []], + xAxis: [settings.xAxis, []], + + thresholds: [settings.thresholds, []], + + animation: [settings.animation, []], showLegend: [settings.showLegend, []], legendPosition: [settings.legendPosition, []], @@ -117,6 +190,7 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent { cardButtons: [this.getCardButtons(configData.config), []], borderRadius: [configData.config.borderRadius, []], + padding: [settings.padding, []], actions: [configData.config.actions || {}, []] }); @@ -138,12 +212,44 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent { this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + this.widgetConfig.config.settings.dataZoom = config.dataZoom; + + this.widgetConfig.config.units = config.units; + this.widgetConfig.config.decimals = config.decimals; + this.widgetConfig.config.settings.rangeColors = config.rangeColors; this.widgetConfig.config.settings.outOfRangeColor = config.outOfRangeColor; + this.widgetConfig.config.settings.showRangeThresholds = config.showRangeThresholds; + this.widgetConfig.config.settings.rangeThreshold = config.rangeThreshold; this.widgetConfig.config.settings.fillArea = config.fillArea; - this.widgetConfig.config.units = config.units; - this.widgetConfig.config.decimals = config.decimals; + this.widgetConfig.config.settings.fillAreaOpacity = config.fillAreaOpacity; + + this.widgetConfig.config.settings.showLine = config.showLine; + this.widgetConfig.config.settings.step = config.step; + this.widgetConfig.config.settings.stepType = config.stepType; + this.widgetConfig.config.settings.smooth = config.smooth; + this.widgetConfig.config.settings.lineType = config.lineType; + this.widgetConfig.config.settings.lineWidth = config.lineWidth; + + this.widgetConfig.config.settings.showPoints = config.showPoints; + this.widgetConfig.config.settings.showPointLabel = config.showPointLabel; + this.widgetConfig.config.settings.pointLabelPosition = config.pointLabelPosition; + this.widgetConfig.config.settings.pointLabelFont = config.pointLabelFont; + this.widgetConfig.config.settings.pointLabelColor = config.pointLabelColor; + this.widgetConfig.config.settings.enablePointLabelBackground = config.enablePointLabelBackground; + this.widgetConfig.config.settings.pointLabelBackground = config.pointLabelBackground; + this.widgetConfig.config.settings.pointShape = config.pointShape; + this.widgetConfig.config.settings.pointSize = config.pointSize; + + this.widgetConfig.config.settings.grid = config.grid; + + this.widgetConfig.config.settings.yAxis = config.yAxis; + this.widgetConfig.config.settings.xAxis = config.xAxis; + + this.widgetConfig.config.settings.thresholds = config.thresholds; + + this.widgetConfig.config.settings.animation = config.animation; this.widgetConfig.config.settings.showLegend = config.showLegend; this.widgetConfig.config.settings.legendPosition = config.legendPosition; @@ -165,18 +271,26 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent { this.setCardButtons(config.cardButtons, this.widgetConfig.config); this.widgetConfig.config.borderRadius = config.borderRadius; + this.widgetConfig.config.settings.padding = config.padding; this.widgetConfig.config.actions = config.actions; return this.widgetConfig; } protected validatorTriggers(): string[] { - return ['showTitle', 'showIcon', 'showLegend', 'showTooltip', 'tooltipShowDate']; + return ['showTitle', 'showIcon', 'showRangeThresholds', 'fillArea', 'showLine', + 'step', 'showPointLabel', 'enablePointLabelBackground', 'showLegend', 'showTooltip', 'tooltipShowDate']; } protected updateValidators(emitEvent: boolean, trigger?: string) { const showTitle: boolean = this.rangeChartWidgetConfigForm.get('showTitle').value; const showIcon: boolean = this.rangeChartWidgetConfigForm.get('showIcon').value; + const showRangeThresholds: boolean = this.rangeChartWidgetConfigForm.get('showRangeThresholds').value; + const fillArea: boolean = this.rangeChartWidgetConfigForm.get('fillArea').value; + const showLine: boolean = this.rangeChartWidgetConfigForm.get('showLine').value; + const step: boolean = this.rangeChartWidgetConfigForm.get('step').value; + const showPointLabel: boolean = this.rangeChartWidgetConfigForm.get('showPointLabel').value; + const enablePointLabelBackground: boolean = this.rangeChartWidgetConfigForm.get('enablePointLabelBackground').value; const showLegend: boolean = this.rangeChartWidgetConfigForm.get('showLegend').value; const showTooltip: boolean = this.rangeChartWidgetConfigForm.get('showTooltip').value; const tooltipShowDate: boolean = this.rangeChartWidgetConfigForm.get('tooltipShowDate').value; @@ -208,6 +322,54 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent { this.rangeChartWidgetConfigForm.get('iconColor').disable(); } + if (showRangeThresholds) { + this.rangeChartWidgetConfigForm.get('rangeThreshold').enable(); + } else { + this.rangeChartWidgetConfigForm.get('rangeThreshold').disable(); + } + + if (fillArea) { + this.rangeChartWidgetConfigForm.get('fillAreaOpacity').enable(); + } else { + this.rangeChartWidgetConfigForm.get('fillAreaOpacity').disable(); + } + + if (showLine) { + this.rangeChartWidgetConfigForm.get('step').enable({emitEvent: false}); + if (step) { + this.rangeChartWidgetConfigForm.get('stepType').enable(); + this.rangeChartWidgetConfigForm.get('smooth').disable(); + } else { + this.rangeChartWidgetConfigForm.get('stepType').disable(); + this.rangeChartWidgetConfigForm.get('smooth').enable(); + } + this.rangeChartWidgetConfigForm.get('lineType').enable(); + this.rangeChartWidgetConfigForm.get('lineWidth').enable(); + } else { + this.rangeChartWidgetConfigForm.get('step').disable({emitEvent: false}); + this.rangeChartWidgetConfigForm.get('stepType').disable(); + this.rangeChartWidgetConfigForm.get('smooth').disable(); + this.rangeChartWidgetConfigForm.get('lineType').disable(); + this.rangeChartWidgetConfigForm.get('lineWidth').disable(); + } + if (showPointLabel) { + this.rangeChartWidgetConfigForm.get('pointLabelPosition').enable(); + this.rangeChartWidgetConfigForm.get('pointLabelFont').enable(); + this.rangeChartWidgetConfigForm.get('pointLabelColor').enable(); + this.rangeChartWidgetConfigForm.get('enablePointLabelBackground').enable({emitEvent: false}); + if (enablePointLabelBackground) { + this.rangeChartWidgetConfigForm.get('pointLabelBackground').enable(); + } else { + this.rangeChartWidgetConfigForm.get('pointLabelBackground').disable(); + } + } else { + this.rangeChartWidgetConfigForm.get('pointLabelPosition').disable(); + this.rangeChartWidgetConfigForm.get('pointLabelFont').disable(); + this.rangeChartWidgetConfigForm.get('pointLabelColor').disable(); + this.rangeChartWidgetConfigForm.get('enablePointLabelBackground').disable({emitEvent: false}); + this.rangeChartWidgetConfigForm.get('pointLabelBackground').disable(); + } + if (showLegend) { this.rangeChartWidgetConfigForm.get('legendPosition').enable(); this.rangeChartWidgetConfigForm.get('legendLabelFont').enable(); @@ -260,6 +422,12 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent { config.enableFullscreen = buttons.includes('fullscreen'); } + private _pointLabelPreviewFn(): string { + const units: string = this.rangeChartWidgetConfigForm.get('units').value; + const decimals: number = this.rangeChartWidgetConfigForm.get('decimals').value; + return formatValue(22, decimals, units, false); + } + private _tooltipValuePreviewFn(): string { const units: string = this.rangeChartWidgetConfigForm.get('units').value; const decimals: number = this.rangeChartWidgetConfigForm.get('decimals').value; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html index 94866da201..126f9b6260 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html @@ -25,22 +25,81 @@ forceSingleDatasource formControlName="datasources"> - - +
+
+
{{ 'widgets.chart.series' | translate }}
+ + {{ 'widgets.chart.series' | translate }} + {{ 'widgets.time-series-chart.comparison.comparison' | translate }} + +
+ + +
+ + {{ 'widgets.time-series-chart.comparison.comparison' | translate }} + +
+ + + + {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} + + + {{ 'widgets.chart.time-for-comparison-days' | translate }} + + + {{ 'widgets.chart.time-for-comparison-weeks' | translate }} + + + {{ 'widgets.chart.time-for-comparison-months' | translate }} + + + {{ 'widgets.chart.time-for-comparison-years' | translate }} + + + {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} + + + + + + + + +
+
+ + +
+ + @@ -102,6 +161,9 @@ {{ 'widgets.time-series-chart.data-zoom' | translate }}
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts index 31dca75c7e..044e29614a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts @@ -23,6 +23,7 @@ import { WidgetConfigComponentData } from '@home/models/widget-component.models' import { DataKey, Datasource, + DatasourceType, legendPositions, legendPositionTranslationMap, WidgetConfig, @@ -46,8 +47,9 @@ import { } from '@home/components/widget/lib/chart/time-series-chart-widget.models'; import { EChartsTooltipTrigger } from '@home/components/widget/lib/chart/echarts-widget.models'; import { - TimeSeriesChartKeySettings, TimeSeriesChartThreshold, - TimeSeriesChartType, TimeSeriesChartYAxes, + TimeSeriesChartKeySettings, + TimeSeriesChartType, + TimeSeriesChartYAxes, TimeSeriesChartYAxisId } from '@home/components/widget/lib/chart/time-series-chart.models'; @@ -88,6 +90,8 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon chartType: TimeSeriesChartType = TimeSeriesChartType.default; + seriesMode = 'series'; + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, private $injector: Injector, @@ -104,6 +108,11 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon } } + seriesModeChange(seriesMode: string) { + this.seriesMode = seriesMode; + this.updateSeriesState(); + } + protected configForm(): UntypedFormGroup { return this.timeSeriesChartWidgetConfigForm; } @@ -130,6 +139,12 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon yAxes: [settings.yAxes, []], series: [this.getSeries(configData.config.datasources), []], + + comparisonEnabled: [settings.comparisonEnabled, []], + timeForComparison: [settings.timeForComparison, []], + comparisonCustomIntervalValue: [settings.comparisonCustomIntervalValue, [Validators.min(0)]], + comparisonXAxis: [settings.comparisonXAxis, []], + thresholds: [settings.thresholds, []], showTitle: [configData.config.showTitle, []], @@ -146,6 +161,8 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon dataZoom: [settings.dataZoom, []], stack: [settings.stack, []], + grid: [settings.grid, []], + xAxis: [settings.xAxis, []], noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []], @@ -178,6 +195,10 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon actions: [configData.config.actions || {}, []] }); + if (this.chartType === TimeSeriesChartType.state) { + this.timeSeriesChartWidgetConfigForm.addControl('states', this.fb.control(settings.states, [])); + } + this.timeSeriesChartWidgetConfigForm.get('comparisonEnabled').valueChanges.subscribe(() => this.updateSeriesState()); } protected prepareOutputConfig(config: any): WidgetConfigComponentData { @@ -197,11 +218,18 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + this.widgetConfig.config.settings.comparisonEnabled = config.comparisonEnabled; + this.widgetConfig.config.settings.timeForComparison = config.timeForComparison; + this.widgetConfig.config.settings.comparisonCustomIntervalValue = config.comparisonCustomIntervalValue; + this.widgetConfig.config.settings.comparisonXAxis = config.comparisonXAxis; + this.widgetConfig.config.settings.thresholds = config.thresholds; this.widgetConfig.config.settings.dataZoom = config.dataZoom; this.widgetConfig.config.settings.stack = config.stack; + this.widgetConfig.config.settings.grid = config.grid; + this.widgetConfig.config.settings.yAxes = config.yAxes; this.widgetConfig.config.settings.xAxis = config.xAxis; @@ -233,20 +261,35 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon this.widgetConfig.config.settings.padding = config.padding; this.widgetConfig.config.actions = config.actions; + + if (this.chartType === TimeSeriesChartType.state) { + this.widgetConfig.config.settings.states = config.states; + } return this.widgetConfig; } protected validatorTriggers(): string[] { - return ['showTitle', 'showIcon', 'showLegend', 'showTooltip', 'tooltipShowDate']; + return ['comparisonEnabled', 'showTitle', 'showIcon', 'showLegend', 'showTooltip', 'tooltipShowDate']; } protected updateValidators(emitEvent: boolean, trigger?: string) { + const comparisonEnabled: boolean = this.timeSeriesChartWidgetConfigForm.get('comparisonEnabled').value; const showTitle: boolean = this.timeSeriesChartWidgetConfigForm.get('showTitle').value; const showIcon: boolean = this.timeSeriesChartWidgetConfigForm.get('showIcon').value; const showLegend: boolean = this.timeSeriesChartWidgetConfigForm.get('showLegend').value; const showTooltip: boolean = this.timeSeriesChartWidgetConfigForm.get('showTooltip').value; const tooltipShowDate: boolean = this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').value; + if (comparisonEnabled) { + this.timeSeriesChartWidgetConfigForm.get('timeForComparison').enable(); + this.timeSeriesChartWidgetConfigForm.get('comparisonCustomIntervalValue').enable(); + this.timeSeriesChartWidgetConfigForm.get('comparisonXAxis').enable(); + } else { + this.timeSeriesChartWidgetConfigForm.get('timeForComparison').disable(); + this.timeSeriesChartWidgetConfigForm.get('comparisonCustomIntervalValue').disable(); + this.timeSeriesChartWidgetConfigForm.get('comparisonXAxis').disable(); + } + if (showTitle) { this.timeSeriesChartWidgetConfigForm.get('title').enable(); this.timeSeriesChartWidgetConfigForm.get('titleFont').enable(); @@ -328,6 +371,19 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon } } + private updateSeriesState() { + if (this.seriesMode === 'series') { + this.timeSeriesChartWidgetConfigForm.get('series').enable({emitEvent: false}); + } else { + const comparisonEnabled = this.timeSeriesChartWidgetConfigForm.get('comparisonEnabled').value; + if (comparisonEnabled) { + this.timeSeriesChartWidgetConfigForm.get('series').enable({emitEvent: false}); + } else { + this.timeSeriesChartWidgetConfigForm.get('series').disable({emitEvent: false}); + } + } + } + private removeYaxisId(series: DataKey[], yAxisId: TimeSeriesChartYAxisId): boolean { let changed = false; if (series) { @@ -364,4 +420,6 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon processor.update(Date.now()); return processor.formatted; } + + protected readonly DatasourceType = DatasourceType; } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html index f140bd7445..59a3bcb3cb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html @@ -15,8 +15,10 @@ limitations under the License. --> -
-
{{ panelTitle }}
+
+
{{ panelTitle }}
datakey.source
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts index 86367771de..67ed19b2d2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts @@ -96,6 +96,10 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC @Input() deviceId: string; + @Input() + @coerceBoolean() + hidePanel = false; + @Input() @coerceBoolean() hideDataKeyColor = false; diff --git a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts index 6cef868e66..d047abdec5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts @@ -49,6 +49,7 @@ import { TbInject } from '@shared/decorators/tb-inject'; import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; import { UserSettingsService } from '@core/http/user-settings.service'; import { ImagePipe } from '@shared/pipe/image.pipe'; +import { UtilsService } from '@core/services/utils.service'; @Directive() // eslint-disable-next-line @angular-eslint/directive-class-suffix @@ -86,6 +87,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid this.ctx.customDialog = $injector.get(CustomDialogService); this.ctx.resourceService = $injector.get(ResourceService); this.ctx.userSettingsService = $injector.get(UserSettingsService); + this.ctx.utilsService = $injector.get(UtilsService); this.ctx.telemetryWsService = $injector.get(TelemetryWebsocketService); this.ctx.date = $injector.get(DatePipe); this.ctx.imagePipe = $injector.get(ImagePipe); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html index 8d660a7884..f48b8def30 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html @@ -15,20 +15,21 @@ limitations under the License. --> -
+
-
+
-
-
{{ legendItem.label }}
+
+
{{ legendKey.label }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.scss index feb65353f6..ac7229672e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.scss @@ -19,8 +19,10 @@ position: relative; display: flex; flex-direction: column; - gap: 16px; - padding: 20px 24px 24px 24px; + gap: 8px; + &.overlay { + padding: 20px 24px 24px 24px; + } > div:not(.tb-bar-chart-overlay) { z-index: 1; } @@ -40,7 +42,7 @@ min-height: 0; display: flex; flex-direction: column; - gap: 16px; + gap: 8px; &.legend-top { flex-direction: column-reverse; } @@ -88,8 +90,9 @@ } } &.legend-right, &.legend-left { - gap: 24px; + gap: 16px; .tb-bar-chart-legend { + padding-top: 8px; flex-direction: column-reverse; justify-content: flex-end; align-items: stretch; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts index da7f4d4e40..b840f49e3b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts @@ -28,45 +28,18 @@ import { ViewEncapsulation } from '@angular/core'; import { WidgetContext } from '@home/models/widget-component.models'; -import { - backgroundStyle, - ComponentStyle, - DateFormatProcessor, - overlayStyle, - textStyle -} from '@shared/models/widget-settings.models'; -import { ResizeObserver } from '@juggle/resize-observer'; -import { formatValue } from '@core/utils'; +import { backgroundStyle, ComponentStyle, overlayStyle, textStyle } from '@shared/models/widget-settings.models'; import { Observable } from 'rxjs'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { DomSanitizer } from '@angular/platform-browser'; import { barChartWithLabelsDefaultSettings, + barChartWithLabelsTimeSeriesKeySettings, + barChartWithLabelsTimeSeriesSettings, BarChartWithLabelsWidgetSettings } from '@home/components/widget/lib/chart/bar-chart-with-labels-widget.models'; - -import * as echarts from 'echarts/core'; -import { CustomSeriesOption } from 'echarts/charts'; -import { CallbackDataParams, CustomSeriesRenderItem, LabelLayoutOptionCallback } from 'echarts/types/dist/shared'; - -import { - ECharts, - echartsModule, - EChartsOption, - EChartsSeriesItem, - echartsTooltipFormatter, - toNamedData -} from '@home/components/widget/lib/chart/echarts-widget.models'; -import { AggregationType, IntervalMath } from '@shared/models/time/time.models'; - -type BarChartDataItem = EChartsSeriesItem; - -interface BarChartLegendItem { - id: string; - color: string; - label: string; - enabled: boolean; -} +import { TbTimeSeriesChart } from '@home/components/widget/lib/chart/time-series-chart'; +import { DataKey } from '@shared/models/widget.models'; @Component({ selector: 'tb-bar-chart-with-labels-widget', @@ -92,30 +65,14 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; + overlayEnabled: boolean; + padding: string; - legendItems: BarChartLegendItem[]; + legendKeys: DataKey[]; legendLabelStyle: ComponentStyle; disabledLegendLabelStyle: ComponentStyle; - private get noAggregation(): boolean { - return this.ctx.defaultSubscription.timeWindowConfig?.aggregation?.type === AggregationType.NONE; - } - - private shapeResize$: ResizeObserver; - - private decimals = 0; - private units = ''; - - private dataItems: BarChartDataItem[] = []; - - private drawChartPending = false; - private barChart: ECharts; - private barChartOptions: EChartsOption; - - private tooltipDateFormat: DateFormatProcessor; - - private barRenderItem: CustomSeriesRenderItem; - private barLabelLayoutCallback: LabelLayoutOptionCallback; + private timeSeriesChart: TbTimeSeriesChart; constructor(private imagePipe: ImagePipe, private sanitizer: DomSanitizer, @@ -127,355 +84,64 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft this.ctx.$scope.barChartWidget = this; this.settings = {...barChartWithLabelsDefaultSettings, ...this.ctx.settings}; - this.decimals = this.ctx.decimals; - this.units = this.ctx.units; - this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.overlayStyle = overlayStyle(this.settings.background.overlay); + this.overlayEnabled = this.settings.background.overlay.enabled; + this.padding = this.overlayEnabled ? undefined : this.settings.padding; this.showLegend = this.settings.showLegend; if (this.showLegend) { - this.legendItems = []; + this.legendKeys = []; this.legendClass = `legend-${this.settings.legendPosition}`; this.legendLabelStyle = textStyle(this.settings.legendLabelFont); this.disabledLegendLabelStyle = textStyle(this.settings.legendLabelFont); this.legendLabelStyle.color = this.settings.legendLabelColor; } - let counter = 0; - if (this.ctx.datasources.length) { - for (const datasource of this.ctx.datasources) { - const dataKeys = datasource.dataKeys; - for (const dataKey of dataKeys) { - const id = counter++; - const datasourceData = this.ctx.data ? this.ctx.data.find(d => d.dataKey === dataKey) : null; - const namedData = datasourceData?.data ? toNamedData(datasourceData.data) : []; - this.dataItems.push({ - id: id+'', - dataKey, - data: namedData, - enabled: true - }); - if (this.showLegend) { - this.legendItems.push( - { - id: id+'', - label: dataKey.label, - color: dataKey.color, - enabled: true - } - ); - } + const barSettings = barChartWithLabelsTimeSeriesKeySettings(this.settings, this.ctx.decimals); + for (const datasource of this.ctx.datasources) { + const dataKeys = datasource.dataKeys; + for (const dataKey of dataKeys) { + dataKey.settings = barSettings; + if (this.showLegend) { + this.legendKeys.push(dataKey); } } } - - if (this.settings.showTooltip && this.settings.tooltipShowDate) { - this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat); - } - - const barValueStyle: ComponentStyle = textStyle(this.settings.barValueFont); - delete barValueStyle.lineHeight; - barValueStyle.fontSize = this.settings.barValueFont.size; - barValueStyle.fill = this.settings.barValueColor; - - const barLabelStyle: ComponentStyle = textStyle(this.settings.barLabelFont); - delete barLabelStyle.lineHeight; - barLabelStyle.fontSize = this.settings.barLabelFont.size; - barLabelStyle.fill = this.settings.barLabelColor; - - this.barRenderItem = (params, api) => { - - const time = api.value(0) as number; - let start = api.value(2) as number; - const end = api.value(3) as number; - let interval = end - start; - if (!start || !end || !interval) { - interval = IntervalMath.numberValue(this.ctx.timeWindow.interval); - start = time - interval / 2; - } - const enabledDataItems = this.dataItems.filter(d => d.enabled); - const barInterval = interval / (enabledDataItems.length + 1); - const intervalGap = barInterval / 2; - - const index = enabledDataItems.findIndex(d => d.id === params.seriesId); - const value = api.value(1); - const startTime = start + intervalGap + barInterval * index; - const delta = barInterval; - const lowerLeft = api.coord([startTime, value >= 0 ? value : 0]); - const height = api.size([delta, value])[1]; - const width = api.size([delta, 10])[0]; - - const coordSys: {x: number; y: number; width: number; height: number} = params.coordSys as any; - - const rectShape = echarts.graphic.clipRectByRect({ - x: lowerLeft[0], - y: lowerLeft[1], - width, - height - }, { - x: coordSys.x, - y: coordSys.y, - width: coordSys.width, - height: coordSys.height - }); - - const zeroPos = api.coord([0, 0]); - const labelParts: string[] = []; - if (this.settings.showBarValue) { - const labelValue = formatValue(value, this.decimals, '', false); - labelParts.push(`{value|${labelValue}}`); - } - if (this.settings.showBarLabel) { - labelParts.push(`{label|${params.seriesName}}`); - } - const barLabel = labelParts.join(' '); - return rectShape && { - type: 'rect', - id: time + '', - shape: rectShape, - style: { - fill: api.visual('color'), - text: barLabel, - textPosition: 'insideBottom', - textRotation: Math.PI / 2, - textDistance: 15, - textStrokeWidth: 0, - textAlign: 'left', - textVerticalAlign: 'middle', - rich: { - value: barValueStyle, - label: barLabelStyle - } - }, - focus: 'series', - transition: 'all', - enterFrom: { - style: { opacity: 0 }, - shape: { height: 0, y: zeroPos[1] } - } - }; - }; - - this.barLabelLayoutCallback = (params) => { - if (params.rect.width - params.labelRect.width < 2) { - return { - y: '100000%', - }; - } else { - return { - hideOverlap: true - }; - } - }; } ngAfterViewInit() { - if (this.drawChartPending) { - this.drawChart(); - } + const settings = barChartWithLabelsTimeSeriesSettings(this.settings); + this.timeSeriesChart = new TbTimeSeriesChart(this.ctx, settings, this.chartShape.nativeElement, this.renderer); } ngOnDestroy() { - if (this.shapeResize$) { - this.shapeResize$.disconnect(); - } - if (this.barChart) { - this.barChart.dispose(); + if (this.timeSeriesChart) { + this.timeSeriesChart.destroy(); } } public onInit() { const borderRadius = this.ctx.$widgetElement.css('borderRadius'); this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; - if (this.chartShape) { - this.drawChart(); - } else { - this.drawChartPending = true; - } this.cd.detectChanges(); } public onDataUpdated() { - for (const item of this.dataItems) { - const datasourceData = this.ctx.data ? this.ctx.data.find(d => d.dataKey === item.dataKey) : null; - item.data = datasourceData?.data ? toNamedData(datasourceData.data) : []; - } - if (this.barChart) { - (this.barChartOptions.xAxis as any).min = this.ctx.defaultSubscription.timeWindow.minTime; - (this.barChartOptions.xAxis as any).max = this.ctx.defaultSubscription.timeWindow.maxTime; - (this.barChartOptions.xAxis as any).tbTimeWindow = this.ctx.defaultSubscription.timeWindow; - this.barChartOptions.series = this.updateSeries(); - this.barChart.setOption(this.barChartOptions); + if (this.timeSeriesChart) { + this.timeSeriesChart.update(); } } - private updateSeries(): Array { - const series: Array = []; - for (const item of this.dataItems) { - if (item.enabled) { - const seriesOption: CustomSeriesOption = { - type: 'custom', - id: item.id, - name: item.dataKey.label, - color: item.dataKey.color, - data: item.data, - renderItem: this.barRenderItem, - labelLayout: this.barLabelLayoutCallback, - dimensions: [ - {name: 'intervalStart', type: 'number'}, - {name: 'intervalEnd', type: 'number'} - ], - encode: { - intervalStart: 2, - intervalEnd: 3 - } - }; - series.push(seriesOption); - } - } - return series; + public onLegendKeyEnter(key: DataKey) { + this.timeSeriesChart.keyEnter(key); } - - public onLegendItemEnter(item: BarChartLegendItem) { - this.barChart.dispatchAction({ - type: 'highlight', - seriesId: item.id - }); - } - - public onLegendItemLeave(item: BarChartLegendItem) { - this.barChart.dispatchAction({ - type: 'downplay', - seriesId: item.id - }); - } - - public toggleLegendItem(item: BarChartLegendItem) { - const enable = !item.enabled; - const dataItem = this.dataItems.find(d => d.id === item.id); - if (dataItem) { - dataItem.enabled = enable; - if (!enable) { - this.barChart.dispatchAction({ - type: 'downplay', - seriesId: item.id - }); - } - this.barChartOptions.series = this.updateSeries(); - this.barChart.setOption(this.barChartOptions, {replaceMerge: ['series']}); - item.enabled = enable; - if (enable) { - this.barChart.dispatchAction({ - type: 'highlight', - seriesId: item.id - }); - } - } + public onLegendKeyLeave(key: DataKey) { + this.timeSeriesChart.keyLeave(key); } - private drawChart() { - echartsModule.init(); - this.barChart = echarts.init(this.chartShape.nativeElement, null, { - renderer: 'canvas', - }); - this.barChartOptions = { - tooltip: { - trigger: 'axis', - confine: true, - appendTo: 'body', - axisPointer: { - type: 'shadow' - }, - formatter: (params: CallbackDataParams[]) => { - if (this.settings.showTooltip) { - const focusedSeriesIndex = this.focusedSeriesIndex(); - return echartsTooltipFormatter(this.renderer, this.tooltipDateFormat, - this.settings, params, this.decimals, this.units, focusedSeriesIndex, null, - this.noAggregation ? null : this.ctx.timeWindow.interval); - } else { - return undefined; - } - }, - padding: [8, 12], - backgroundColor: this.settings.tooltipBackgroundColor, - borderWidth: 0, - extraCssText: `line-height: 1; backdrop-filter: blur(${this.settings.tooltipBackgroundBlur}px);` - }, - grid: { - containLabel: true, - top: '30', - left: 0, - right: 0, - bottom: 0 - }, - xAxis: { - type: 'time', - scale: true, - axisTick: { - show: false - }, - axisLabel: { - hideOverlap: true, - fontSize: 10 - }, - axisLine: { - onZero: false - }, - min: this.ctx.defaultSubscription.timeWindow.minTime, - max: this.ctx.defaultSubscription.timeWindow.maxTime - }, - yAxis: { - type: 'value', - axisLabel: { - formatter: (value: any) => formatValue(value, this.decimals, this.units, false) - } - } - }; - - (this.barChartOptions.xAxis as any).tbTimeWindow = this.ctx.defaultSubscription.timeWindow; - - this.barChartOptions.series = this.updateSeries(); - - this.barChart.setOption(this.barChartOptions); - - this.shapeResize$ = new ResizeObserver(() => { - this.onResize(); - }); - this.shapeResize$.observe(this.chartShape.nativeElement); - this.onResize(); - } - - private focusedSeriesIndex(): number { - let index = - 1; - const views: any[] = (this.barChart as any)._chartsViews; - if (views) { - const hasBlurredView = !!views.find(view => { - const graphicEls: any[] = view._data._graphicEls; - return !!graphicEls.find(el => el?.currentStates.includes('blur')); - }); - if (hasBlurredView) { - const focusedView = views.find(view => { - const graphicEls: any[] = view._data._graphicEls; - return !!graphicEls.find(el => !el?.currentStates.includes('blur')); - }); - if (focusedView) { - index = !!focusedView._model ? - focusedView._model.seriesIndex : (!!focusedView.__model ? focusedView.__model.seriesIndex : -1); - } - } - } - return index; - } - - private onResize() { - const width = this.barChart.getWidth(); - const height = this.barChart.getHeight(); - const shapeWidth = this.chartShape.nativeElement.offsetWidth; - const shapeHeight = this.chartShape.nativeElement.offsetHeight; - if (width !== shapeWidth || height !== shapeHeight) { - this.barChart.resize(); - } + public toggleLegendKey(key: DataKey) { + this.timeSeriesChart.toggleKey(key); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts index 99ea088c92..53e4dc8527 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts @@ -14,25 +14,67 @@ /// limitations under the License. /// -import { BackgroundSettings, BackgroundType, customDateFormat, Font } from '@shared/models/widget-settings.models'; +import { + BackgroundSettings, + BackgroundType, + ComponentStyle, + customDateFormat, + Font, + textStyle +} from '@shared/models/widget-settings.models'; import { LegendPosition } from '@shared/models/widget.models'; import { EChartsTooltipWidgetSettings } from '@home/components/widget/lib/chart/echarts-widget.models'; +import { DeepPartial } from '@shared/models/common'; +import { + defaultTimeSeriesChartXAxisSettings, + defaultTimeSeriesChartYAxisSettings, + SeriesFillSettings, + SeriesFillType, + timeSeriesChartAnimationDefaultSettings, + TimeSeriesChartAnimationSettings, + timeSeriesChartGridDefaultSettings, + TimeSeriesChartGridSettings, + TimeSeriesChartKeySettings, + timeSeriesChartNoAggregationBarWidthDefaultSettings, + TimeSeriesChartNoAggregationBarWidthSettings, + TimeSeriesChartSeriesType, + TimeSeriesChartSettings, + TimeSeriesChartThreshold, + TimeSeriesChartXAxisSettings, + TimeSeriesChartYAxisSettings +} from '@home/components/widget/lib/chart/time-series-chart.models'; +import { CallbackDataParams, LabelLayoutOptionCallbackParams } from 'echarts/types/dist/shared'; +import { formatValue, mergeDeep } from '@core/utils'; +import { LabelLayoutOption } from 'echarts/types/src/util/types'; export interface BarChartWithLabelsWidgetSettings extends EChartsTooltipWidgetSettings { + dataZoom: boolean; showBarLabel: boolean; barLabelFont: Font; barLabelColor: string; showBarValue: boolean; barValueFont: Font; barValueColor: string; + showBarBorder: boolean; + barBorderWidth: number; + barBorderRadius: number; + barBackgroundSettings: SeriesFillSettings; + noAggregationBarWidthSettings: TimeSeriesChartNoAggregationBarWidthSettings; + grid: TimeSeriesChartGridSettings; + yAxis: TimeSeriesChartYAxisSettings; + xAxis: TimeSeriesChartXAxisSettings; + animation: TimeSeriesChartAnimationSettings; + thresholds: TimeSeriesChartThreshold[]; showLegend: boolean; legendPosition: LegendPosition; legendLabelFont: Font; legendLabelColor: string; background: BackgroundSettings; + padding: string; } export const barChartWithLabelsDefaultSettings: BarChartWithLabelsWidgetSettings = { + dataZoom: false, showBarLabel: true, barLabelFont: { family: 'Roboto', @@ -53,6 +95,30 @@ export const barChartWithLabelsDefaultSettings: BarChartWithLabelsWidgetSettings lineHeight: '12px' }, barValueColor: 'rgba(0, 0, 0, 0.76)', + showBarBorder: false, + barBorderWidth: 2, + barBorderRadius: 0, + barBackgroundSettings: { + type: SeriesFillType.none, + opacity: 0.4, + gradient: { + start: 100, + end: 0 + } + }, + noAggregationBarWidthSettings: mergeDeep({} as TimeSeriesChartNoAggregationBarWidthSettings, + timeSeriesChartNoAggregationBarWidthDefaultSettings), + grid: mergeDeep({} as TimeSeriesChartGridSettings, + timeSeriesChartGridDefaultSettings), + yAxis: mergeDeep({} as TimeSeriesChartYAxisSettings, + defaultTimeSeriesChartYAxisSettings, + { id: 'default', order: 0, showLine: false, showTicks: false } as TimeSeriesChartYAxisSettings), + xAxis: mergeDeep({} as TimeSeriesChartXAxisSettings, + defaultTimeSeriesChartXAxisSettings, + {showTicks: false, showSplitLines: false} as TimeSeriesChartXAxisSettings), + animation: mergeDeep({} as TimeSeriesChartAnimationSettings, + timeSeriesChartAnimationDefaultSettings), + thresholds: [], showLegend: true, legendPosition: LegendPosition.top, legendLabelFont: { @@ -96,5 +162,90 @@ export const barChartWithLabelsDefaultSettings: BarChartWithLabelsWidgetSettings color: 'rgba(255,255,255,0.72)', blur: 3 } - } + }, + padding: '12px' +}; + +export const barChartWithLabelsTimeSeriesSettings = (settings: BarChartWithLabelsWidgetSettings): DeepPartial => ({ + dataZoom: settings.dataZoom, + grid: settings.grid, + yAxes: { + default: settings.yAxis + }, + xAxis: settings.xAxis, + barWidthSettings: { + barGap: 0, + intervalGap: 0.5 + }, + noAggregationBarWidthSettings: settings.noAggregationBarWidthSettings, + animation: settings.animation, + thresholds: settings.thresholds, + showTooltip: settings.showTooltip, + tooltipValueFont: settings.tooltipValueFont, + tooltipValueColor: settings.tooltipValueColor, + tooltipShowDate: settings.tooltipShowDate, + tooltipDateInterval: settings.tooltipDateInterval, + tooltipDateFormat: settings.tooltipDateFormat, + tooltipDateFont: settings.tooltipDateFont, + tooltipDateColor: settings.tooltipDateColor, + tooltipBackgroundColor: settings.tooltipBackgroundColor, + tooltipBackgroundBlur: settings.tooltipBackgroundBlur, + tooltipShowFocusedSeries: true +}); + +export const barChartWithLabelsTimeSeriesKeySettings = (settings: BarChartWithLabelsWidgetSettings, + decimals: number): DeepPartial => { + const barValueStyle: ComponentStyle = textStyle(settings.barValueFont); + delete barValueStyle.lineHeight; + barValueStyle.fontSize = settings.barValueFont.size; + barValueStyle.fill = settings.barValueColor; + + const barLabelStyle: ComponentStyle = textStyle(settings.barLabelFont); + delete barLabelStyle.lineHeight; + barLabelStyle.fontSize = settings.barLabelFont.size; + barLabelStyle.fill = settings.barLabelColor; + return { + type: TimeSeriesChartSeriesType.bar, + barSettings: { + showBorder: settings.showBarBorder, + borderWidth: settings.barBorderWidth, + borderRadius: settings.barBorderRadius, + backgroundSettings: settings.barBackgroundSettings, + showLabel: settings.showBarLabel || settings.showBarValue, + labelPosition: 'insideBottom', + labelFormatter: (params: CallbackDataParams): string => { + const labelParts: string[] = []; + if (settings.showBarValue) { + const labelValue = formatValue(params.value[1], decimals, '', false); + labelParts.push(`{value|${labelValue}}`); + } + if (settings.showBarLabel) { + labelParts.push(`{label|${params.seriesName}}`); + } + return labelParts.join(' '); + }, + labelLayout: (params: LabelLayoutOptionCallbackParams): LabelLayoutOption => { + if (params.rect.width - params.labelRect.width < 2) { + return { + y: '100000%', + }; + } else { + return { + hideOverlap: true + }; + } + }, + additionalLabelOption: { + textRotation: Math.PI / 2, + textDistance: 15, + textStrokeWidth: 0, + textAlign: 'left', + textVerticalAlign: 'middle', + rich: { + value: barValueStyle, + label: barLabelStyle + } + } + } + }; }; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts index 7fdd1fbfff..35b929319b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts @@ -15,11 +15,9 @@ /// import * as echarts from 'echarts/core'; -import { Axis } from 'echarts'; import AxisModel from 'echarts/types/src/coord/cartesian/AxisModel'; import { estimateLabelUnionRect } from 'echarts/lib/coord/axisHelper'; -import { formatValue, isDefinedAndNotNull } from '@core/utils'; -import TimeScale from 'echarts/types/src/scale/Time'; +import { isDefinedAndNotNull, isFunction, isNumber } from '@core/utils'; import { DataZoomComponent, DataZoomComponentOption, @@ -44,72 +42,27 @@ import { } from 'echarts/charts'; import { LabelLayout } from 'echarts/features'; import { CanvasRenderer, SVGRenderer } from 'echarts/renderers'; -import { DataEntry, DataKey, DataSet } from '@shared/models/widget.models'; +import { DataEntry, DataKey, DataSet, Datasource, FormattedData } from '@shared/models/widget.models'; import { calculateAggIntervalWithWidgetTimeWindow, Interval, IntervalMath, WidgetTimewindow } from '@shared/models/time/time.models'; -import { CallbackDataParams } from 'echarts/types/dist/shared'; +import { CallbackDataParams, TimeAxisBandWidthCalculator } from 'echarts/types/dist/shared'; import { Renderer2 } from '@angular/core'; import { DateFormatProcessor, DateFormatSettings, Font } from '@shared/models/widget-settings.models'; import GlobalModel from 'echarts/types/src/model/Global'; import Axis2D from 'echarts/types/src/coord/cartesian/Axis2D'; import SeriesModel from 'echarts/types/src/model/Series'; +import { MarkLine2DDataItemOption } from 'echarts/types/src/component/marker/MarkLineModel'; +import { TimeAxisBaseOption } from 'echarts/types/src/coord/axisCommonTypes'; class EChartsModule { private initialized = false; init() { if (!this.initialized) { - const axisGetBandWidth = Axis.prototype.getBandWidth; - - Axis.prototype.getBandWidth = function(){ - const model: AxisModel = this.model; - const axisOption = model.option; - if (this.scale.type === 'time') { - let interval: number; - const seriesDataIndices = axisOption.axisPointer?.seriesDataIndices; - if (seriesDataIndices?.length) { - const seriesDataIndex = seriesDataIndices[0]; - const series = model.ecModel.getSeriesByIndex(seriesDataIndex.seriesIndex); - if (series) { - const values = series.getData().getValues(seriesDataIndex.dataIndex); - const start = values[2]; - const end = values[3]; - if (typeof start === 'number' && typeof end === 'number') { - interval = Math.max(end - start, 1); - } - } - } - if (!interval) { - const tbTimeWindow: WidgetTimewindow = (axisOption as any).tbTimeWindow; - if (isDefinedAndNotNull(tbTimeWindow)) { - if (axisOption.axisPointer?.value && typeof axisOption.axisPointer?.value === 'number') { - const intervalArray = calculateAggIntervalWithWidgetTimeWindow(tbTimeWindow, axisOption.axisPointer.value); - const start = intervalArray[0]; - const end = intervalArray[1]; - interval = Math.max(end - start, 1); - } else { - interval = IntervalMath.numberValue(tbTimeWindow.interval); - } - } - } - if (interval) { - const timeScale: TimeScale = this.scale; - const axisExtent: [number, number] = this._extent; - const dataExtent = timeScale.getExtent(); - const size = Math.abs(axisExtent[1] - axisExtent[0]); - return interval * (size / (dataExtent[1] - dataExtent[0])); - } else { - return axisGetBandWidth.call(this); - } - } else { - return axisGetBandWidth.call(this); - } - }; - echarts.use([ TooltipComponent, GridComponent, @@ -124,7 +77,6 @@ class EChartsModule { CanvasRenderer, SVGRenderer ]); - this.initialized = true; } } @@ -150,29 +102,108 @@ export type EChartsDataItem = [number, any, number, number]; export type NamedDataSet = {name: string; value: EChartsDataItem}[]; +export type EChartsTooltipValueFormatFunction = (value: any, latestData: FormattedData, units?: string, decimals?: number) => string; + export type EChartsSeriesItem = { id: string; + datasource: Datasource; dataKey: DataKey; data: NamedDataSet; dataSet?: DataSet; enabled: boolean; units?: string; decimals?: number; + latestData?: FormattedData; + tooltipValueFormatFunction?: EChartsTooltipValueFormatFunction; + comparisonItem?: boolean; }; -export const getXAxis = (chart: ECharts): Axis2D => { - const model: GlobalModel = (chart as any).getModel(); - const models = model.queryComponents({mainType: 'xAxis'}); - if (models?.length) { - const axisModel = models[0] as AxisModel; - return axisModel.axis; +export enum EChartsShape { + emptyCircle = 'emptyCircle', + circle = 'circle', + rect = 'rect', + roundRect = 'roundRect', + triangle = 'triangle', + diamond = 'diamond', + pin = 'pin', + arrow = 'arrow', + none = 'none' +} + +export const echartsShapes = Object.keys(EChartsShape) as EChartsShape[]; + +export const echartsShapeTranslations = new Map( + [ + [EChartsShape.emptyCircle, 'widgets.time-series-chart.shape-empty-circle'], + [EChartsShape.circle, 'widgets.time-series-chart.shape-circle'], + [EChartsShape.rect, 'widgets.time-series-chart.shape-rect'], + [EChartsShape.roundRect, 'widgets.time-series-chart.shape-round-rect'], + [EChartsShape.triangle, 'widgets.time-series-chart.shape-triangle'], + [EChartsShape.diamond, 'widgets.time-series-chart.shape-diamond'], + [EChartsShape.pin, 'widgets.time-series-chart.shape-pin'], + [EChartsShape.arrow, 'widgets.time-series-chart.shape-arrow'], + [EChartsShape.none, 'widgets.time-series-chart.shape-none'] + ] +); + +type EChartsShapeOffsetFunction = (size: number) => number; + +export const timeSeriesChartShapeOffsetFunctions = new Map( + [ + [EChartsShape.emptyCircle, size => size / 2 + 1], + [EChartsShape.circle, size => size / 2], + [EChartsShape.rect, size => size / 2], + [EChartsShape.roundRect, size => size / 2], + [EChartsShape.triangle, size => size / 2], + [EChartsShape.diamond, size => size / 2], + [EChartsShape.pin, size => size], + [EChartsShape.arrow, () => 0], + [EChartsShape.none, () => 0], + ] +); + + +export const timeAxisBandWidthCalculator: TimeAxisBandWidthCalculator = (model) => { + let interval: number; + const axisOption = model.option; + const seriesDataIndices = axisOption.axisPointer?.seriesDataIndices; + if (seriesDataIndices?.length) { + const seriesDataIndex = seriesDataIndices[0]; + const series = model.ecModel.getSeriesByIndex(seriesDataIndex.seriesIndex); + if (series) { + const values = series.getData().getValues(seriesDataIndex.dataIndex); + const start = values[2]; + const end = values[3]; + if (typeof start === 'number' && typeof end === 'number') { + interval = Math.max(end - start, 1); + } + } + } + if (!interval) { + const tbTimeWindow: WidgetTimewindow = (axisOption as any).tbTimeWindow; + if (isDefinedAndNotNull(tbTimeWindow)) { + if (axisOption.axisPointer?.value && typeof axisOption.axisPointer?.value === 'number') { + const intervalArray = calculateAggIntervalWithWidgetTimeWindow(tbTimeWindow, axisOption.axisPointer.value); + const start = intervalArray[0]; + const end = intervalArray[1]; + interval = Math.max(end - start, 1); + } else { + interval = IntervalMath.numberValue(tbTimeWindow.interval); + } + } + } + if (interval) { + const timeScale = model.axis.scale; + const axisExtent = model.axis.getExtent(); + const dataExtent = timeScale.getExtent(); + const size = Math.abs(axisExtent[1] - axisExtent[0]); + return interval * (size / (dataExtent[1] - dataExtent[0])); } - return null; }; -export const getYAxis = (chart: ECharts, axisId: string): Axis2D => { +export const getAxis = (chart: ECharts, mainType: string, axisId: string): Axis2D => { const model: GlobalModel = (chart as any).getModel(); - const models = model.queryComponents({mainType: 'yAxis', id: axisId}); + const models = model.queryComponents({mainType, id: axisId}); if (models?.length) { const axisModel = models[0] as AxisModel; return axisModel.axis; @@ -180,17 +211,20 @@ export const getYAxis = (chart: ECharts, axisId: string): Axis2D => { return null; }; -export const calculateYAxisWidth = (chart: ECharts, axisId: string): number => { - const axis = getYAxis(chart, axisId); - return calculateAxisSize(axis); +export const calculateAxisSize = (chart: ECharts, mainType: string, axisId: string): number => { + const axis = getAxis(chart, mainType, axisId); + return _calculateAxisSize(axis); }; -export const calculateXAxisHeight = (chart: ECharts): number => { - const axis = getXAxis(chart); - return calculateAxisSize(axis); +export const measureAxisNameSize = (chart: ECharts, mainType: string, axisId: string, name: string): number => { + const axis = getAxis(chart, mainType, axisId); + if (axis) { + return axis.model.getModel('nameTextStyle').getTextRect(name).height; + } + return 0; }; -const calculateAxisSize = (axis: Axis2D): number => { +const _calculateAxisSize = (axis: Axis2D): number => { let size = 0; if (axis && axis.model.option.show) { const labelUnionRect = estimateLabelUnionRect(axis); @@ -207,24 +241,24 @@ const calculateAxisSize = (axis: Axis2D): number => { return size; }; -export const measureYAxisNameWidth = (chart: ECharts, axisId: string, name: string): number => { - const axis = getYAxis(chart, axisId); - if (axis) { - return axis.model.getModel('nameTextStyle').getTextRect(name).height; - } - return 0; -}; - -export const measureXAxisNameHeight = (chart: ECharts, name: string): number => { - const axis = getXAxis(chart); - if (axis) { - return axis.model.getModel('nameTextStyle').getTextRect(name).height; +const measureSymbolOffset = (symbol: string, symbolSize: any): number => { + if (isNumber(symbolSize)) { + if (symbol) { + const offsetFunction = timeSeriesChartShapeOffsetFunctions.get(symbol as EChartsShape); + if (offsetFunction) { + return offsetFunction(symbolSize); + } else { + return symbolSize / 2; + } + } + } else { + return 0; } - return 0; }; -export const measureThresholdLabelOffset = (chart: ECharts, axisId: string, thresholdId: string, value: any): [number, number] => { - const axis = getYAxis(chart, axisId); +export const measureThresholdOffset = (chart: ECharts, axisId: string, thresholdId: string, value: any): [number, number] => { + const offset: [number, number] = [0,0]; + const axis = getAxis(chart, 'yAxis', axisId); if (axis && !axis.scale.isBlank()) { const extent = axis.scale.getExtent(); const model: GlobalModel = (chart as any).getModel(); @@ -232,6 +266,16 @@ export const measureThresholdLabelOffset = (chart: ECharts, axisId: string, thre if (models?.length) { const lineSeriesModel = models[0] as SeriesModel; const markLineModel = lineSeriesModel.getModel('markLine'); + const dataOption = markLineModel.get('data'); + for (const dataItemOption of dataOption) { + const dataItem = dataItemOption as MarkLine2DDataItemOption; + const start = dataItem[0]; + const startOffset = measureSymbolOffset(start.symbol, start.symbolSize); + offset[0] = Math.max(offset[0], startOffset); + const end = dataItem[1]; + const endOffset = measureSymbolOffset(end.symbol, end.symbolSize); + offset[1] = Math.max(offset[1], endOffset); + } const labelPosition = markLineModel.get(['label', 'position']); if (labelPosition === 'start' || labelPosition === 'end') { const labelModel = markLineModel.getModel('label'); @@ -251,34 +295,91 @@ export const measureThresholdLabelOffset = (chart: ECharts, axisId: string, thre } } if (!textWidth) { - return [0,0]; + return offset; } const distanceOpt = markLineModel.get(['label', 'distance']); let distance = 5; if (distanceOpt) { distance = typeof distanceOpt === 'number' ? distanceOpt : distanceOpt[0]; } - const offset = distance + textWidth; + const paddingOpt = markLineModel.get(['label', 'padding']); + let leftPadding = 0; + let rightPadding = 0; + if (paddingOpt) { + if (Array.isArray(paddingOpt)) { + if (paddingOpt.length === 4) { + leftPadding = paddingOpt[3]; + rightPadding = paddingOpt[1]; + } else if (paddingOpt.length === 2) { + leftPadding = rightPadding = paddingOpt[1]; + } + } else { + leftPadding = rightPadding = paddingOpt; + } + } + const textOffset = distance + textWidth + leftPadding + rightPadding; if (labelPosition === 'start') { - return [offset, 0]; + offset[0] = Math.max(offset[0], textOffset); } else { - return [0, offset]; + offset[1] = Math.max(offset[1], textOffset); } } } } - return [0,0]; + return offset; }; export const getAxisExtent = (chart: ECharts, axisId: string): [number, number] => { - const axis = getYAxis(chart, axisId); + const axis = getAxis(chart, 'yAxis', axisId); if (axis) { return axis.scale.getExtent(); } return [0,0]; }; -export const toNamedData = (data: DataSet): NamedDataSet => { +let componentBlurredKey: string; + +const isBlurred = (model: SeriesModel): boolean => { + if (!componentBlurredKey) { + const innerKeys = Object.keys(model).filter(k => k.startsWith('__ec_inner_')); + for (const k of innerKeys) { + const obj = model[k]; + if (obj.hasOwnProperty('isBlured')) { + componentBlurredKey = k; + break; + } + } + } + if (componentBlurredKey) { + const obj = model[componentBlurredKey]; + return !!obj?.isBlured; + } else { + return false; + } +}; + +export const getFocusedSeriesIndex = (chart: ECharts): number => { + const model: GlobalModel = (chart as any).getModel(); + const models = model.queryComponents({mainType: 'series'}); + if (models) { + let hasBlurred = false; + let notBlurredIndex = -1; + for (const _model of models) { + const seriesModel = _model as SeriesModel; + const blurred = isBlurred(seriesModel); + if (!blurred) { + notBlurredIndex = seriesModel.seriesIndex; + } + hasBlurred = blurred || hasBlurred; + } + if (hasBlurred) { + return notBlurredIndex; + } + } + return -1; +}; + +export const toNamedData = (data: DataSet, valueConverter?: (value: any) => any): NamedDataSet => { if (!data?.length) { return []; } else { @@ -286,14 +387,43 @@ export const toNamedData = (data: DataSet): NamedDataSet => { const ts = isDefinedAndNotNull(d[2]) ? d[2][0] : d[0]; return { name: ts + '', - value: toEChartsDataItem(d) + value: toEChartsDataItem(d, valueConverter) }; }); } }; -const toEChartsDataItem = (entry: DataEntry): EChartsDataItem => { - const item: EChartsDataItem = [entry[0], entry[1], entry[0], entry[0]]; +const minDataTs = (dataSet: NamedDataSet): number => dataSet.length ? dataSet.map(data => + Number(data.name)).reduce((a, b) => Math.min(a, b)) : undefined; + +const maxDataTs = (dataSet: NamedDataSet): number => dataSet.length ? dataSet.map(data => + Number(data.name)).reduce((a, b) => Math.max(a, b)) : undefined; + +export const adjustTimeAxisExtentToData = (timeAxisOption: TimeAxisBaseOption, + dataItems: EChartsSeriesItem[], + defaultMin: number, + defaultMax: number): void => { + let min: number; + let max: number; + for (const item of dataItems) { + if (item.enabled) { + const minTs = minDataTs(item.data); + if (typeof minTs !== 'undefined') { + min = (typeof min !== 'undefined') ? Math.min(min, minTs) : minTs; + } + const maxTs = maxDataTs(item.data); + if (typeof maxTs !== 'undefined') { + max = (typeof max !== 'undefined') ? Math.max(max, maxTs) : maxTs; + } + } + } + timeAxisOption.min = (typeof min !== 'undefined') ? min : defaultMin; + timeAxisOption.max = (typeof max !== 'undefined') ? max : defaultMax; +}; + +const toEChartsDataItem = (entry: DataEntry, valueConverter?: (value: any) => any): EChartsDataItem => { + const value = valueConverter ? valueConverter(entry[1]) : entry[1]; + const item: EChartsDataItem = [entry[0], value, entry[0], entry[0]]; if (isDefinedAndNotNull(entry[2])) { item[2] = entry[2][0]; item[3] = entry[2][1]; @@ -316,8 +446,10 @@ export const tooltipTriggerTranslationMap = new Map { + let tooltipValueFormatFunction: EChartsTooltipValueFormatFunction; + if (isFunction(tooltipValueFormatter)) { + tooltipValueFormatFunction = tooltipValueFormatter as EChartsTooltipValueFormatFunction; + } else if (typeof tooltipValueFormatter === 'string' && tooltipValueFormatter.length) { + try { + tooltipValueFormatFunction = + new Function('value', 'latestData', tooltipValueFormatter) as EChartsTooltipValueFormatFunction; + } catch (e) {} + } + return tooltipValueFormatFunction; +}; + export const echartsTooltipFormatter = (renderer: Renderer2, tooltipDateFormat: DateFormatProcessor, settings: EChartsTooltipWidgetSettings, params: CallbackDataParams[] | CallbackDataParams, - decimals: number, - units: string, + valueFormatFunction: EChartsTooltipValueFormatFunction, focusedSeriesIndex: number, series?: EChartsSeriesItem[], interval?: Interval): null | HTMLElement => { - if (!params || Array.isArray(params) && !params[0]) { - return null; - } - const firstParam = Array.isArray(params) ? params[0] : params; - if (!firstParam.value) { + + const tooltipParams = mapTooltipParams(params, series, focusedSeriesIndex); + if (!tooltipParams.items.length && !tooltipParams.comparisonItems.length) { return null; } + const tooltipElement: HTMLElement = renderer.createElement('div'); renderer.setStyle(tooltipElement, 'display', 'flex'); renderer.setStyle(tooltipElement, 'flex-direction', 'column'); renderer.setStyle(tooltipElement, 'align-items', 'flex-start'); - renderer.setStyle(tooltipElement, 'gap', '4px'); - if (settings.tooltipShowDate) { - const dateElement: HTMLElement = renderer.createElement('div'); - let dateText: string; - const startTs = firstParam.value[2]; - const endTs = firstParam.value[3]; - if (settings.tooltipDateInterval && startTs && endTs && (endTs - 1) > startTs) { - const startDateText = tooltipDateFormat.update(startTs, interval); - const endDateText = tooltipDateFormat.update(endTs - 1, interval); - if (startDateText === endDateText) { - dateText = startDateText; - } else { - dateText = startDateText + ' - ' + endDateText; - } - } else { - const ts = firstParam.value[0]; - dateText = tooltipDateFormat.update(ts, interval); + renderer.setStyle(tooltipElement, 'gap', '16px'); + + buildItemsTooltip(tooltipElement, tooltipParams.items, renderer, tooltipDateFormat, settings, valueFormatFunction, interval); + buildItemsTooltip(tooltipElement, tooltipParams.comparisonItems, renderer, tooltipDateFormat, settings, valueFormatFunction, interval); + + return tooltipElement; +}; + +interface TooltipItem { + param: CallbackDataParams; + dataItem: EChartsSeriesItem; +} + +interface TooltipParams { + items: TooltipItem[]; + comparisonItems: TooltipItem[]; +} + +const buildItemsTooltip = (tooltipElement: HTMLElement, + items: TooltipItem[], + renderer: Renderer2, + tooltipDateFormat: DateFormatProcessor, + settings: EChartsTooltipWidgetSettings, + valueFormatFunction: EChartsTooltipValueFormatFunction, + interval?: Interval) => { + if (items.length) { + const tooltipItemsElement: HTMLElement = renderer.createElement('div'); + renderer.setStyle(tooltipItemsElement, 'display', 'flex'); + renderer.setStyle(tooltipItemsElement, 'flex-direction', 'column'); + renderer.setStyle(tooltipItemsElement, 'align-items', 'flex-start'); + renderer.setStyle(tooltipItemsElement, 'gap', '4px'); + renderer.appendChild(tooltipElement, tooltipItemsElement); + if (settings.tooltipShowDate) { + renderer.appendChild(tooltipItemsElement, + constructEchartsTooltipDateElement(renderer, tooltipDateFormat, settings, items[0].param, interval)); + } + for (const item of items) { + renderer.appendChild(tooltipItemsElement, + constructEchartsTooltipSeriesElement(renderer, settings, item, valueFormatFunction)); } - renderer.appendChild(dateElement, renderer.createText(dateText)); - renderer.setStyle(dateElement, 'font-family', settings.tooltipDateFont.family); - renderer.setStyle(dateElement, 'font-size', settings.tooltipDateFont.size + settings.tooltipDateFont.sizeUnit); - renderer.setStyle(dateElement, 'font-style', settings.tooltipDateFont.style); - renderer.setStyle(dateElement, 'font-weight', settings.tooltipDateFont.weight); - renderer.setStyle(dateElement, 'line-height', settings.tooltipDateFont.lineHeight); - renderer.setStyle(dateElement, 'color', settings.tooltipDateColor); - renderer.appendChild(tooltipElement, dateElement); + } +}; + +const mapTooltipParams = (params: CallbackDataParams[] | CallbackDataParams, + series?: EChartsSeriesItem[], + focusedSeriesIndex?: number): TooltipParams => { + const result: TooltipParams = { + items: [], + comparisonItems: [] + }; + if (!params || Array.isArray(params) && !params[0]) { + return result; + } + const firstParam = Array.isArray(params) ? params[0] : params; + if (!firstParam.value) { + return result; } let seriesParams: CallbackDataParams = null; if (Array.isArray(params) && focusedSeriesIndex > -1) { @@ -381,21 +555,63 @@ export const echartsTooltipFormatter = (renderer: Renderer2, seriesParams = params; } if (seriesParams) { - renderer.appendChild(tooltipElement, constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, decimals, units, series)); + appendTooltipItem(result, seriesParams, series); } else if (Array.isArray(params)) { for (seriesParams of params) { - renderer.appendChild(tooltipElement, constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, decimals, units, series)); + appendTooltipItem(result, seriesParams, series); } } - return tooltipElement; + return result; +}; + +const appendTooltipItem = (tooltipParams: TooltipParams, seriesParams: CallbackDataParams, series?: EChartsSeriesItem[]) => { + const dataItem = series?.find(s => s.id === seriesParams.seriesId); + const tooltipItem: TooltipItem = { + param: seriesParams, + dataItem + }; + if (dataItem?.comparisonItem) { + tooltipParams.comparisonItems.push(tooltipItem); + } else { + tooltipParams.items.push(tooltipItem); + } +}; + +const constructEchartsTooltipDateElement = (renderer: Renderer2, + tooltipDateFormat: DateFormatProcessor, + settings: EChartsTooltipWidgetSettings, + param: CallbackDataParams, + interval?: Interval): HTMLElement => { + const dateElement: HTMLElement = renderer.createElement('div'); + let dateText: string; + const startTs = param.value[2]; + const endTs = param.value[3]; + if (settings.tooltipDateInterval && startTs && endTs && (endTs - 1) > startTs) { + const startDateText = tooltipDateFormat.update(startTs, interval); + const endDateText = tooltipDateFormat.update(endTs - 1, interval); + if (startDateText === endDateText) { + dateText = startDateText; + } else { + dateText = startDateText + ' - ' + endDateText; + } + } else { + const ts = param.value[0]; + dateText = tooltipDateFormat.update(ts, interval); + } + renderer.appendChild(dateElement, renderer.createText(dateText)); + renderer.setStyle(dateElement, 'font-family', settings.tooltipDateFont.family); + renderer.setStyle(dateElement, 'font-size', settings.tooltipDateFont.size + settings.tooltipDateFont.sizeUnit); + renderer.setStyle(dateElement, 'font-style', settings.tooltipDateFont.style); + renderer.setStyle(dateElement, 'font-weight', settings.tooltipDateFont.weight); + renderer.setStyle(dateElement, 'line-height', settings.tooltipDateFont.lineHeight); + renderer.setStyle(dateElement, 'color', settings.tooltipDateColor); + return dateElement; }; const constructEchartsTooltipSeriesElement = (renderer: Renderer2, settings: EChartsTooltipWidgetSettings, - seriesParams: CallbackDataParams, - decimals: number, - units: string, - series?: EChartsSeriesItem[]): HTMLElement => { + item: TooltipItem, + valueFormatFunction: EChartsTooltipValueFormatFunction): HTMLElement => { const labelValueElement: HTMLElement = renderer.createElement('div'); renderer.setStyle(labelValueElement, 'display', 'flex'); renderer.setStyle(labelValueElement, 'flex-direction', 'row'); @@ -411,10 +627,10 @@ const constructEchartsTooltipSeriesElement = (renderer: Renderer2, renderer.setStyle(circleElement, 'width', '8px'); renderer.setStyle(circleElement, 'height', '8px'); renderer.setStyle(circleElement, 'border-radius', '50%'); - renderer.setStyle(circleElement, 'background', seriesParams.color); + renderer.setStyle(circleElement, 'background', item.param.color); renderer.appendChild(labelElement, circleElement); const labelTextElement: HTMLElement = renderer.createElement('div'); - renderer.appendChild(labelTextElement, renderer.createText(seriesParams.seriesName)); + renderer.appendChild(labelTextElement, renderer.createText(item.param.seriesName)); renderer.setStyle(labelTextElement, 'font-family', 'Roboto'); renderer.setStyle(labelTextElement, 'font-size', '12px'); renderer.setStyle(labelTextElement, 'font-style', 'normal'); @@ -424,16 +640,22 @@ const constructEchartsTooltipSeriesElement = (renderer: Renderer2, renderer.setStyle(labelTextElement, 'color', 'rgba(0, 0, 0, 0.76)'); renderer.appendChild(labelElement, labelTextElement); const valueElement: HTMLElement = renderer.createElement('div'); - let formatDecimals = decimals; - let formatUnits = units; - if (series) { - const item = series.find(s => s.id === seriesParams.seriesId); - if (item) { - formatDecimals = item.decimals; - formatUnits = item.units; + let formatFunction = valueFormatFunction; + let latestData: FormattedData; + let units = ''; + let decimals = 0; + if (item.dataItem) { + if (item.dataItem.tooltipValueFormatFunction) { + formatFunction = item.dataItem.tooltipValueFormatFunction; } + latestData = item.dataItem.latestData; + units = item.dataItem.units; + decimals = item.dataItem.decimals; + } + if (!latestData) { + latestData = {} as FormattedData; } - const value = formatValue(seriesParams.value[1], formatDecimals, formatUnits, false); + const value = formatFunction(item.param.value[1], latestData, units, decimals); renderer.appendChild(valueElement, renderer.createText(value)); renderer.setStyle(valueElement, 'flex', '1'); renderer.setStyle(valueElement, 'text-align', 'end'); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html index 6d3b6d43db..63a549bb48 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html @@ -15,7 +15,8 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.scss index de2ca546fc..d956173281 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.scss @@ -19,8 +19,10 @@ position: relative; display: flex; flex-direction: column; - gap: 16px; - padding: 20px 24px 24px 24px; + gap: 8px; + &.overlay { + padding: 20px 24px 24px 24px; + } > div:not(.tb-range-chart-overlay) { z-index: 1; } @@ -40,7 +42,7 @@ min-height: 0; display: flex; flex-direction: column; - gap: 16px; + gap: 8px; &.legend-top { flex-direction: column-reverse; } @@ -88,8 +90,9 @@ } } &.legend-right, &.legend-left { - gap: 24px; + gap: 16px; .tb-range-chart-legend { + padding-top: 8px; flex-direction: column-reverse; justify-content: flex-end; align-items: stretch; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts index 1705074a1e..9d89154111 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts @@ -30,144 +30,24 @@ import { import { WidgetContext } from '@home/models/widget-component.models'; import { backgroundStyle, - ColorRange, ComponentStyle, - DateFormatProcessor, - filterIncludingColorRanges, getDataKey, overlayStyle, - sortedColorRange, textStyle } from '@shared/models/widget-settings.models'; -import { ResizeObserver } from '@juggle/resize-observer'; -import * as echarts from 'echarts/core'; -import { formatValue, isDefinedAndNotNull, isNumber } from '@core/utils'; -import { rangeChartDefaultSettings, RangeChartWidgetSettings } from './range-chart-widget.models'; +import { isDefinedAndNotNull } from '@core/utils'; +import { + rangeChartDefaultSettings, + rangeChartTimeSeriesKeySettings, + rangeChartTimeSeriesSettings, + RangeChartWidgetSettings, + RangeItem, + toRangeItems +} from './range-chart-widget.models'; import { Observable } from 'rxjs'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { DomSanitizer } from '@angular/platform-browser'; -import { - ECharts, - echartsModule, - EChartsOption, - echartsTooltipFormatter, - toNamedData -} from '@home/components/widget/lib/chart/echarts-widget.models'; -import { CallbackDataParams } from 'echarts/types/dist/shared'; -import { AggregationType } from '@shared/models/time/time.models'; - -interface VisualPiece { - lt?: number; - gt?: number; - lte?: number; - gte?: number; - value?: number; - color?: string; -} - -interface RangeItem { - index: number; - from?: number; - to?: number; - piece: VisualPiece; - color: string; - label: string; - visible: boolean; - enabled: boolean; -} - -const rangeItemLabel = (from?: number, to?: number): string => { - if (isNumber(from) && isNumber(to)) { - if (from === to) { - return `${from}`; - } else { - return `${from} - ${to}`; - } - } else if (isNumber(from)) { - return `≥ ${from}`; - } else if (isNumber(to)) { - return `< ${to}`; - } else { - return null; - } -}; - -const toVisualPiece = (color: string, from?: number, to?: number): VisualPiece => { - const piece: VisualPiece = { - color - }; - if (isNumber(from) && isNumber(to)) { - if (from === to) { - piece.value = from; - } else { - piece.gte = from; - piece.lt = to; - } - } else if (isNumber(from)) { - piece.gte = from; - } else if (isNumber(to)) { - piece.lt = to; - } - return piece; -}; - -const toRangeItems = (colorRanges: Array): RangeItem[] => { - const rangeItems: RangeItem[] = []; - let counter = 0; - const ranges = sortedColorRange(filterIncludingColorRanges(colorRanges)).filter(r => isNumber(r.from) || isNumber(r.to)); - for (let i = 0; i < ranges.length; i++) { - const range = ranges[i]; - let from = range.from; - const to = range.to; - if (i > 0) { - const prevRange = ranges[i - 1]; - if (isNumber(prevRange.to) && isNumber(from) && from < prevRange.to) { - from = prevRange.to; - } - } - rangeItems.push( - { - index: counter++, - color: range.color, - enabled: true, - visible: true, - from, - to, - label: rangeItemLabel(from, to), - piece: toVisualPiece(range.color, from, to) - } - ); - if (!isNumber(from) || !isNumber(to)) { - const value = !isNumber(from) ? to : from; - rangeItems.push( - { - index: counter++, - color: 'transparent', - enabled: true, - visible: false, - label: '', - piece: { gt: value - 0.000000001, lt: value + 0.000000001, color: 'transparent'} - } - ); - } - } - return rangeItems; -}; - -const getMarkPoints = (ranges: Array): number[] => { - const points = new Set(); - for (const range of ranges) { - if (range.visible) { - if (isNumber(range.from)) { - points.add(range.from); - } - if (isNumber(range.to)) { - points.add(range.to); - } - } - } - return Array.from(points).sort(); -}; +import { TbTimeSeriesChart } from '@home/components/widget/lib/chart/time-series-chart'; @Component({ selector: 'tb-range-chart-widget', @@ -193,28 +73,19 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; + overlayEnabled: boolean; + padding: string; legendLabelStyle: ComponentStyle; disabledLegendLabelStyle: ComponentStyle; visibleRangeItems: RangeItem[]; - private get noAggregation(): boolean { - return this.ctx.defaultSubscription.timeWindowConfig?.aggregation?.type === AggregationType.NONE; - } - - private rangeItems: RangeItem[]; - - private shapeResize$: ResizeObserver; - private decimals = 0; private units = ''; - private drawChartPending = false; - private rangeChart: ECharts; - private rangeChartOptions: EChartsOption; - private selectedRanges: {[key: number]: boolean} = {}; + private rangeItems: RangeItem[]; - private tooltipDateFormat: DateFormatProcessor; + private timeSeriesChart: TbTimeSeriesChart; constructor(private imagePipe: ImagePipe, private sanitizer: DomSanitizer, @@ -235,16 +106,17 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn if (dataKey?.units) { this.units = dataKey.units; } - + if (dataKey) { + dataKey.settings = rangeChartTimeSeriesKeySettings(this.settings); + } this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.overlayStyle = overlayStyle(this.settings.background.overlay); + this.overlayEnabled = this.settings.background.overlay.enabled; + this.padding = this.overlayEnabled ? undefined : this.settings.padding; this.rangeItems = toRangeItems(this.settings.rangeColors); this.visibleRangeItems = this.rangeItems.filter(item => item.visible); - for (const range of this.rangeItems) { - this.selectedRanges[range.index] = true; - } this.showLegend = this.settings.showLegend && !!this.rangeItems.length; @@ -254,192 +126,33 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn this.disabledLegendLabelStyle = textStyle(this.settings.legendLabelFont); this.legendLabelStyle.color = this.settings.legendLabelColor; } - - if (this.settings.showTooltip && this.settings.tooltipShowDate) { - this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat); - } } ngAfterViewInit() { - if (this.drawChartPending) { - this.drawChart(); - } + const settings = rangeChartTimeSeriesSettings(this.settings, this.rangeItems, this.decimals, this.units); + this.timeSeriesChart = new TbTimeSeriesChart(this.ctx, settings, this.chartShape.nativeElement, this.renderer); } ngOnDestroy() { - if (this.shapeResize$) { - this.shapeResize$.disconnect(); - } - if (this.rangeChart) { - this.rangeChart.dispose(); + if (this.timeSeriesChart) { + this.timeSeriesChart.destroy(); } } public onInit() { const borderRadius = this.ctx.$widgetElement.css('borderRadius'); this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; - if (this.chartShape) { - this.drawChart(); - } else { - this.drawChartPending = true; - } this.cd.detectChanges(); } public onDataUpdated() { - if (this.rangeChart) { - this.rangeChart.setOption({ - xAxis: { - min: this.ctx.defaultSubscription.timeWindow.minTime, - max: this.ctx.defaultSubscription.timeWindow.maxTime, - tbTimeWindow: this.ctx.defaultSubscription.timeWindow - }, - series: [ - {data: this.ctx.data?.length ? toNamedData(this.ctx.data[0].data) : []} - ], - visualMap: { - selected: this.selectedRanges - } - }); + if (this.timeSeriesChart) { + this.timeSeriesChart.update(); } } public toggleRangeItem(item: RangeItem) { item.enabled = !item.enabled; - this.selectedRanges[item.index] = item.enabled; - this.rangeChart.dispatchAction({ - type: 'selectDataRange', - selected: this.selectedRanges - }); - } - - private drawChart() { - echartsModule.init(); - const dataKey = getDataKey(this.ctx.datasources); - this.rangeChart = echarts.init(this.chartShape.nativeElement, null, { - renderer: 'canvas', - }); - this.rangeChartOptions = { - tooltip: { - trigger: 'axis', - confine: true, - appendTo: 'body', - axisPointer: { - type: 'shadow' - }, - formatter: (params: CallbackDataParams[]) => - this.settings.showTooltip ? echartsTooltipFormatter(this.renderer, this.tooltipDateFormat, - this.settings, params, this.decimals, this.units, 0, null, - this.noAggregation ? null : this.ctx.timeWindow.interval) : undefined, - padding: [8, 12], - backgroundColor: this.settings.tooltipBackgroundColor, - borderWidth: 0, - extraCssText: `line-height: 1; backdrop-filter: blur(${this.settings.tooltipBackgroundBlur}px);` - }, - grid: { - containLabel: true, - top: '30', - left: 0, - right: 0, - bottom: this.settings.dataZoom ? 60 : 0 - }, - xAxis: { - type: 'time', - axisTick: { - show: true - }, - axisLabel: { - hideOverlap: true, - fontSize: 10 - }, - axisLine: { - onZero: false - }, - min: this.ctx.defaultSubscription.timeWindow.minTime, - max: this.ctx.defaultSubscription.timeWindow.maxTime - }, - yAxis: { - type: 'value', - axisLabel: { - formatter: (value: any) => formatValue(value, this.decimals, this.units, false) - } - }, - series: [{ - type: 'line', - name: dataKey?.label, - smooth: false, - showSymbol: false, - animation: true, - areaStyle: this.settings.fillArea ? {} : undefined, - data: this.ctx.data?.length ? toNamedData(this.ctx.data[0].data) : [], - markLine: this.rangeItems.length ? { - animation: true, - symbol: ['circle', 'arrow'], - symbolSize: [5, 7], - lineStyle: { - width: 1, - type: [3, 3], - color: '#37383b' - }, - label: { - position: 'insideEndTop', - color: '#37383b', - backgroundColor: 'rgba(255,255,255,0.56)', - padding: [4, 5], - borderRadius: 4, - formatter: params => formatValue(params.value, this.decimals, this.units, false) - }, - emphasis: { - disabled: true - }, - data: getMarkPoints(this.rangeItems).map(point => ({ yAxis: point })) - } : undefined - }], - dataZoom: [ - { - type: 'inside', - disabled: !this.settings.dataZoom - }, - { - type: 'slider', - show: this.settings.dataZoom, - showDetail: false, - right: 10 - } - ], - visualMap: { - show: false, - type: 'piecewise', - selected: this.selectedRanges, - dimension: 1, - pieces: this.rangeItems.map(item => item.piece), - outOfRange: { - color: this.settings.outOfRangeColor - }, - inRange: !this.rangeItems.length ? { - color: this.settings.outOfRangeColor - } : undefined - } - }; - - (this.rangeChartOptions.xAxis as any).tbTimeWindow = this.ctx.defaultSubscription.timeWindow; - - this.rangeChart.setOption(this.rangeChartOptions); - - this.shapeResize$ = new ResizeObserver(() => { - this.onResize(); - }); - this.shapeResize$.observe(this.chartShape.nativeElement); - this.onResize(); - } - - private onResize() { - const width = this.rangeChart.getWidth(); - const height = this.rangeChart.getHeight(); - const shapeWidth = this.chartShape.nativeElement.offsetWidth; - const shapeHeight = this.chartShape.nativeElement.offsetHeight; - if (width !== shapeWidth || height !== shapeHeight) { - this.rangeChart.resize(); - } + this.timeSeriesChart.toggleVisualMapRange(item.index); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts index 31905e70ba..aa9c290231 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts @@ -18,22 +18,80 @@ import { BackgroundSettings, BackgroundType, ColorRange, + filterIncludingColorRanges, Font, - simpleDateFormat + simpleDateFormat, + sortedColorRange } from '@shared/models/widget-settings.models'; import { LegendPosition } from '@shared/models/widget.models'; -import { EChartsTooltipWidgetSettings } from '@home/components/widget/lib/chart/echarts-widget.models'; +import { EChartsShape, EChartsTooltipWidgetSettings } from '@home/components/widget/lib/chart/echarts-widget.models'; +import { + createTimeSeriesChartVisualMapPiece, + defaultTimeSeriesChartXAxisSettings, + defaultTimeSeriesChartYAxisSettings, + LineSeriesStepType, + SeriesFillType, + SeriesLabelPosition, ThresholdLabelPosition, timeSeriesChartAnimationDefaultSettings, + TimeSeriesChartAnimationSettings, + timeSeriesChartColorScheme, timeSeriesChartGridDefaultSettings, TimeSeriesChartGridSettings, + TimeSeriesChartKeySettings, + TimeSeriesChartLineType, + TimeSeriesChartSeriesType, + TimeSeriesChartSettings, + TimeSeriesChartThreshold, timeSeriesChartThresholdDefaultSettings, + TimeSeriesChartThresholdType, + TimeSeriesChartVisualMapPiece, + TimeSeriesChartXAxisSettings, + TimeSeriesChartYAxisSettings +} from '@home/components/widget/lib/chart/time-series-chart.models'; +import { isNumber, mergeDeep } from '@core/utils'; +import { DeepPartial } from '@shared/models/common'; + +export interface RangeItem { + index: number; + from?: number; + to?: number; + color: string; + label: string; + visible: boolean; + enabled: boolean; + piece: TimeSeriesChartVisualMapPiece; +} export interface RangeChartWidgetSettings extends EChartsTooltipWidgetSettings { dataZoom: boolean; rangeColors: Array; outOfRangeColor: string; + showRangeThresholds: boolean; + rangeThreshold: Partial; fillArea: boolean; + fillAreaOpacity: number; + showLine: boolean; + step: boolean; + stepType: LineSeriesStepType; + smooth: boolean; + lineType: TimeSeriesChartLineType; + lineWidth: number; + showPoints: boolean; + showPointLabel: boolean; + pointLabelPosition: SeriesLabelPosition; + pointLabelFont: Font; + pointLabelColor: string; + enablePointLabelBackground: boolean; + pointLabelBackground: string; + pointShape: EChartsShape; + pointSize: number; + grid: TimeSeriesChartGridSettings; + yAxis: TimeSeriesChartYAxisSettings; + xAxis: TimeSeriesChartXAxisSettings; + animation: TimeSeriesChartAnimationSettings; + thresholds: TimeSeriesChartThreshold[]; showLegend: boolean; legendPosition: LegendPosition; legendLabelFont: Font; legendLabelColor: string; background: BackgroundSettings; + padding: string; } export const rangeChartDefaultSettings: RangeChartWidgetSettings = { @@ -48,7 +106,53 @@ export const rangeChartDefaultSettings: RangeChartWidgetSettings = { {from: 40, color: '#D81838'} ], outOfRangeColor: '#ccc', + showRangeThresholds: true, + rangeThreshold: mergeDeep({} as Partial, + timeSeriesChartThresholdDefaultSettings, + { lineColor: '#37383b', + lineType: TimeSeriesChartLineType.dashed, + startSymbol: EChartsShape.circle, + startSymbolSize: 5, + endSymbol: EChartsShape.arrow, + endSymbolSize: 7, + labelPosition: ThresholdLabelPosition.insideEndTop, + labelColor: '#37383b', + enableLabelBackground: true}), fillArea: true, + fillAreaOpacity: 0.7, + showLine: true, + step: false, + stepType: LineSeriesStepType.start, + smooth: false, + lineType: TimeSeriesChartLineType.solid, + lineWidth: 2, + showPoints: false, + showPointLabel: false, + pointLabelPosition: SeriesLabelPosition.top, + pointLabelFont: { + family: 'Roboto', + size: 11, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '1' + }, + pointLabelColor: timeSeriesChartColorScheme['series.label'].light, + enablePointLabelBackground: false, + pointLabelBackground: 'rgba(255,255,255,0.56)', + pointShape: EChartsShape.emptyCircle, + pointSize: 4, + grid: mergeDeep({} as TimeSeriesChartGridSettings, + timeSeriesChartGridDefaultSettings), + yAxis: mergeDeep({} as TimeSeriesChartYAxisSettings, + defaultTimeSeriesChartYAxisSettings, + { id: 'default', order: 0, showLine: false, showTicks: false } as TimeSeriesChartYAxisSettings), + xAxis: mergeDeep({} as TimeSeriesChartXAxisSettings, + defaultTimeSeriesChartXAxisSettings, + {showSplitLines: false} as TimeSeriesChartXAxisSettings), + animation: mergeDeep({} as TimeSeriesChartAnimationSettings, + timeSeriesChartAnimationDefaultSettings), + thresholds: [], showLegend: true, legendPosition: LegendPosition.top, legendLabelFont: { @@ -92,5 +196,150 @@ export const rangeChartDefaultSettings: RangeChartWidgetSettings = { color: 'rgba(255,255,255,0.72)', blur: 3 } + }, + padding: '12px' +}; + +export const rangeChartTimeSeriesSettings = (settings: RangeChartWidgetSettings, rangeItems: RangeItem[], + decimals: number, units: string): DeepPartial => { + let thresholds: DeepPartial[] = settings.showRangeThresholds ? getMarkPoints(rangeItems).map(item => ({ + ...{type: TimeSeriesChartThresholdType.constant, + yAxisId: 'default', + units, + decimals, + value: item}, + ...settings.rangeThreshold + } as DeepPartial)) : []; + if (settings.thresholds?.length) { + thresholds = thresholds.concat(settings.thresholds); + } + return { + dataZoom: settings.dataZoom, + thresholds, + grid: settings.grid, + yAxes: { + default: { + ...settings.yAxis, + ...{ + decimals, + units + } + } + }, + xAxis: settings.xAxis, + animation: settings.animation, + visualMapSettings: { + outOfRangeColor: settings.outOfRangeColor, + pieces: rangeItems.map(item => item.piece) + }, + showTooltip: settings.showTooltip, + tooltipValueFont: settings.tooltipValueFont, + tooltipValueColor: settings.tooltipValueColor, + tooltipShowDate: settings.tooltipShowDate, + tooltipDateInterval: settings.tooltipDateInterval, + tooltipDateFormat: settings.tooltipDateFormat, + tooltipDateFont: settings.tooltipDateFont, + tooltipDateColor: settings.tooltipDateColor, + tooltipBackgroundColor: settings.tooltipBackgroundColor, + tooltipBackgroundBlur: settings.tooltipBackgroundBlur, + }; +}; + +export const rangeChartTimeSeriesKeySettings = (settings: RangeChartWidgetSettings): DeepPartial => ({ + type: TimeSeriesChartSeriesType.line, + lineSettings: { + showLine: settings.showLine, + step: settings.step, + stepType: settings.stepType, + smooth: settings.smooth, + lineType: settings.lineType, + lineWidth: settings.lineWidth, + showPoints: settings.showPoints, + showPointLabel: settings.showPointLabel, + pointLabelPosition: settings.pointLabelPosition, + pointLabelFont: settings.pointLabelFont, + pointLabelColor: settings.pointLabelColor, + enablePointLabelBackground: settings.enablePointLabelBackground, + pointLabelBackground: settings.pointLabelBackground, + pointShape: settings.pointShape, + pointSize: settings.pointSize, + fillAreaSettings: { + type: settings.fillArea ? SeriesFillType.opacity : SeriesFillType.none, + opacity: settings.fillAreaOpacity + } + } + }); + +export const toRangeItems = (colorRanges: Array): RangeItem[] => { + const rangeItems: RangeItem[] = []; + let counter = 0; + const ranges = sortedColorRange(filterIncludingColorRanges(colorRanges)).filter(r => isNumber(r.from) || isNumber(r.to)); + for (let i = 0; i < ranges.length; i++) { + const range = ranges[i]; + let from = range.from; + const to = range.to; + if (i > 0) { + const prevRange = ranges[i - 1]; + if (isNumber(prevRange.to) && isNumber(from) && from < prevRange.to) { + from = prevRange.to; + } + } + rangeItems.push( + { + index: counter++, + color: range.color, + enabled: true, + visible: true, + from, + to, + label: rangeItemLabel(from, to), + piece: createTimeSeriesChartVisualMapPiece(range.color, from, to) + } + ); + if (!isNumber(from) || !isNumber(to)) { + const value = !isNumber(from) ? to : from; + rangeItems.push( + { + index: counter++, + color: 'transparent', + enabled: true, + visible: false, + label: '', + piece: { gt: value - 0.000000001, lt: value + 0.000000001, color: 'transparent'} + } + ); + } + } + return rangeItems; +}; + +const rangeItemLabel = (from?: number, to?: number): string => { + if (isNumber(from) && isNumber(to)) { + if (from === to) { + return `${from}`; + } else { + return `${from} - ${to}`; + } + } else if (isNumber(from)) { + return `≥ ${from}`; + } else if (isNumber(to)) { + return `< ${to}`; + } else { + return null; + } +}; + +const getMarkPoints = (ranges: Array): number[] => { + const points = new Set(); + for (const range of ranges) { + if (range.visible) { + if (isNumber(range.from)) { + points.add(range.from); + } + if (isNumber(range.to)) { + points.add(range.to); + } + } } + return Array.from(points).sort(); }; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-bar.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-bar.models.ts index c00a078b2f..ee3c703a17 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-bar.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-bar.models.ts @@ -22,7 +22,7 @@ import { TimeSeriesChartNoAggregationBarWidthStrategy } from '@home/components/widget/lib/chart/time-series-chart.models'; import { CustomSeriesRenderItemParams } from 'echarts'; -import { CustomSeriesRenderItemAPI, CustomSeriesRenderItemReturn } from 'echarts/types/dist/shared'; +import { CallbackDataParams, CustomSeriesRenderItemAPI, CustomSeriesRenderItemReturn } from 'echarts/types/dist/shared'; import { isNumeric } from '@core/utils'; import * as echarts from 'echarts/core'; @@ -34,6 +34,8 @@ export interface BarVisualSettings { } export interface BarRenderSharedContext { + barGap: number; + intervalGap: number; timeInterval: Interval; noAggregationBarWidthStrategy: TimeSeriesChartNoAggregationBarWidthStrategy; noAggregationWidthRelative: boolean; @@ -47,6 +49,7 @@ export interface BarRenderContext { noAggregation?: boolean; visualSettings?: BarVisualSettings; labelOption?: SeriesLabelOption; + additionalLabelOption?: {[key: string]: any}; barStackIndex?: number; currentStackItems?: TimeSeriesChartDataItem[]; } @@ -77,10 +80,13 @@ export const renderTimeSeriesBar = (params: CustomSeriesRenderItemParams, api: C start = time - interval / 2; } - const gap = 0.3; - const barInterval = separateBar ? interval : interval / (renderCtx.barsCount + gap * (renderCtx.barsCount + 3)); - const intervalGap = barInterval * gap * 2; - const barGap = barInterval * gap; + const barGapRatio = renderCtx.shared.barGap; + const intervalGapRatio = renderCtx.shared.intervalGap; + const barInterval = separateBar + ? interval + : interval / (renderCtx.barsCount + barGapRatio * (renderCtx.barsCount - 1) + intervalGapRatio * 2); + const intervalGap = barInterval * intervalGapRatio; + const barGap = barInterval * barGapRatio; const value = api.value(1); const startTime = separateBar ? start : start + intervalGap + (barInterval + barGap) * renderCtx.barIndex; const delta = barInterval; @@ -124,7 +130,7 @@ export const renderTimeSeriesBar = (params: CustomSeriesRenderItemParams, api: C const zeroPos = api.coord([0, offset]); - const style: any = { + let style: any = { fill: renderCtx.visualSettings.color, stroke: renderCtx.visualSettings.borderColor, lineWidth: renderCtx.visualSettings.borderWidth @@ -139,10 +145,20 @@ export const renderTimeSeriesBar = (params: CustomSeriesRenderItemParams, api: C position = 'top'; } } - style.text = (renderCtx.labelOption.formatter as LabelFormatterCallback)({value: [null, value]} as any); + style.text = (renderCtx.labelOption.formatter as LabelFormatterCallback)( + { + seriesName: params.seriesName, + value: [null, value] + } as CallbackDataParams); style.textDistance = 5; style.textPosition = position; + style.textBackgroundColor = renderCtx.labelOption.backgroundColor; + style.textPadding = renderCtx.labelOption.padding; + style.textBorderRadius = renderCtx.labelOption.borderRadius; style.rich = renderCtx.labelOption.rich; + if (renderCtx.additionalLabelOption) { + style = {...style, ...renderCtx.additionalLabelOption}; + } } let borderRadius: number[]; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-state.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-state.models.ts new file mode 100644 index 0000000000..9a72361632 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-state.models.ts @@ -0,0 +1,109 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + TimeSeriesChartStateSettings, + TimeSeriesChartStateSourceType, + TimeSeriesChartTicksFormatter, + TimeSeriesChartTicksGenerator +} from '@home/components/widget/lib/chart/time-series-chart.models'; +import { UtilsService } from '@core/services/utils.service'; +import { EChartsTooltipValueFormatFunction } from '@home/components/widget/lib/chart/echarts-widget.models'; +import { FormattedData } from '@shared/models/widget.models'; +import { formatValue, isDefinedAndNotNull, isNumber, isNumeric } from '@core/utils'; +import { LabelFormatterCallback } from 'echarts'; + +export class TimeSeriesChartStateValueConverter { + + private readonly constantsMap = new Map(); + private readonly rangeStates: TimeSeriesChartStateSettings[] = []; + private readonly ticks: {value: number}[] = []; + private readonly labelsMap = new Map(); + + public readonly ticksGenerator: TimeSeriesChartTicksGenerator; + public readonly ticksFormatter: TimeSeriesChartTicksFormatter; + public readonly tooltipFormatter: EChartsTooltipValueFormatFunction; + public readonly labelFormatter: LabelFormatterCallback; + public readonly valueConverter: (value: any) => any; + + constructor(utils: UtilsService, + states: TimeSeriesChartStateSettings[]) { + const ticks: number[] = []; + for (const state of states) { + if (state.sourceType === TimeSeriesChartStateSourceType.constant) { + this.constantsMap.set(state.sourceValue, state.value); + } else { + this.rangeStates.push(state); + } + if (!ticks.includes(state.value)) { + ticks.push(state.value); + const label = utils.customTranslation(state.label, state.label); + this.labelsMap.set(state.value, label); + } + } + this.ticks = ticks.map(val => ({value: val})); + this.ticksGenerator = () => this.ticks; + this.ticksFormatter = (value: any) => { + const result = this.labelsMap.get(value); + return result || ''; + }; + this.tooltipFormatter = (value: any, latestData: FormattedData, units?: string, decimals?: number) => { + const result = this.labelsMap.get(value); + if (typeof result === 'string') { + return result; + } else { + return formatValue(value, decimals, units, false); + } + }; + this.labelFormatter = (params) => { + const value = params.value[1]; + const result = this.labelsMap.get(value); + if (typeof result === 'string') { + return `{value|${result}}`; + } else { + return undefined; + } + }; + this.valueConverter = (value: any) => { + let key = value; + if (key === 'true') { + key = true; + } else if (key === 'false') { + key = false; + } + const result = this.constantsMap.get(key); + if (typeof result === 'number') { + return result; + } else if (this.rangeStates.length && isDefinedAndNotNull(value) && isNumeric(value)) { + for (const state of this.rangeStates) { + const num = Number(value); + if (TimeSeriesChartStateValueConverter.constantRange(state) && state.sourceRangeFrom === num) { + return state.value; + } else if ((!isNumber(state.sourceRangeFrom) || num >= state.sourceRangeFrom) && + (!isNumber(state.sourceRangeTo) || num < state.sourceRangeTo)) { + return state.value; + } + } + } + return value; + }; + } + + static constantRange(state: TimeSeriesChartStateSettings): boolean { + return isNumber(state.sourceRangeFrom) && isNumber(state.sourceRangeTo) && state.sourceRangeFrom === state.sourceRangeTo; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index 6f985466c4..bfe65a0d7c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -18,9 +18,12 @@ import { ECharts, EChartsOption, EChartsSeriesItem, + EChartsShape, EChartsTooltipTrigger, + EChartsTooltipValueFormatFunction, EChartsTooltipWidgetSettings, - measureThresholdLabelOffset + measureThresholdOffset, + timeAxisBandWidthCalculator } from '@home/components/widget/lib/chart/echarts-widget.models'; import { autoDateFormat, @@ -30,12 +33,18 @@ import { textStyle, tsToFormatTimeUnit } from '@shared/models/widget-settings.models'; -import { XAXisOption, YAXisOption } from 'echarts/types/dist/shared'; +import { + LabelLayoutOptionCallback, + VisualMapComponentOption, + XAXisOption, + YAXisOption +} from 'echarts/types/dist/shared'; import { CustomSeriesOption, LineSeriesOption } from 'echarts/charts'; import { formatValue, isDefinedAndNotNull, isFunction, + isNumber, isNumeric, isUndefined, isUndefinedOrNull, @@ -45,25 +54,31 @@ import { import { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient'; import tinycolor from 'tinycolor2'; import { ValueAxisBaseOption } from 'echarts/types/src/coord/axisCommonTypes'; -import { SeriesLabelOption } from 'echarts/types/src/util/types'; +import { LabelLayoutOption, SeriesLabelOption } from 'echarts/types/src/util/types'; +import { LabelFormatterCallback } from 'echarts'; import { BarRenderContext, BarRenderSharedContext, BarVisualSettings, renderTimeSeriesBar } from '@home/components/widget/lib/chart/time-series-chart-bar.models'; -import { DataKey } from '@shared/models/widget.models'; +import { DataKey, DataKeySettingsWithComparison, WidgetComparisonSettings } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { TbColorScheme } from '@shared/models/color.models'; import { AbstractControl, ValidationErrors } from '@angular/forms'; import { MarkLine2DDataItemOption } from 'echarts/types/src/component/marker/MarkLineModel'; import { DatePipe } from '@angular/common'; +import { BuiltinTextPosition } from 'zrender/src/core/types'; +import { CartesianAxisOption } from 'echarts/types/src/coord/cartesian/AxisModel'; +import { WidgetTimewindow } from '@shared/models/time/time.models'; +import { UtilsService } from '@core/services/utils.service'; export enum TimeSeriesChartType { default = 'default', line = 'line', bar = 'bar', - point = 'point' + point = 'point', + state = 'state' } export const timeSeriesChartTypeTranslations = new Map( @@ -74,7 +89,7 @@ export const timeSeriesChartTypeTranslations = new Map( ] ); -export enum TimeSeriesChartShape { - emptyCircle = 'emptyCircle', - circle = 'circle', - rect = 'rect', - roundRect = 'roundRect', - triangle = 'triangle', - diamond = 'diamond', - pin = 'pin', - arrow = 'arrow', - none = 'none' -} - -export const timeSeriesChartShapes = Object.keys(TimeSeriesChartShape) as TimeSeriesChartShape[]; - -export const timeSeriesChartShapeTranslations = new Map( - [ - [TimeSeriesChartShape.emptyCircle, 'widgets.time-series-chart.shape-empty-circle'], - [TimeSeriesChartShape.circle, 'widgets.time-series-chart.shape-circle'], - [TimeSeriesChartShape.rect, 'widgets.time-series-chart.shape-rect'], - [TimeSeriesChartShape.roundRect, 'widgets.time-series-chart.shape-round-rect'], - [TimeSeriesChartShape.triangle, 'widgets.time-series-chart.shape-triangle'], - [TimeSeriesChartShape.diamond, 'widgets.time-series-chart.shape-diamond'], - [TimeSeriesChartShape.pin, 'widgets.time-series-chart.shape-pin'], - [TimeSeriesChartShape.arrow, 'widgets.time-series-chart.shape-arrow'], - [TimeSeriesChartShape.none, 'widgets.time-series-chart.shape-none'] - ] -); - export enum TimeSeriesChartLineType { solid = 'solid', dashed = 'dashed', @@ -221,6 +208,21 @@ export const timeSeriesThresholdTypeTranslations = new Map( + [ + [TimeSeriesChartStateSourceType.constant, 'widgets.time-series-chart.state.type-constant'], + [TimeSeriesChartStateSourceType.range, 'widgets.time-series-chart.state.type-range'] + ] +); + export enum SeriesFillType { none = 'none', opacity = 'opacity', @@ -401,6 +403,38 @@ export const defaultTimeSeriesChartYAxisSettings: TimeSeriesChartYAxisSettings = splitLinesColor: timeSeriesChartColorScheme['axis.splitLine'].light }; +export const defaultTimeSeriesChartXAxisSettings: TimeSeriesChartXAxisSettings = { + show: true, + label: '', + labelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '600', + lineHeight: '1' + }, + labelColor: timeSeriesChartColorScheme['axis.label'].light, + position: AxisPosition.bottom, + showTickLabels: true, + tickLabelFont: { + family: 'Roboto', + size: 10, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '1' + }, + tickLabelColor: timeSeriesChartColorScheme['axis.tickLabel'].light, + ticksFormat: {}, + showTicks: true, + ticksColor: timeSeriesChartColorScheme['axis.ticks'].light, + showLine: true, + lineColor: timeSeriesChartColorScheme['axis.line'].light, + showSplitLines: true, + splitLinesColor: timeSeriesChartColorScheme['axis.splitLine'].light +}; + export type TimeSeriesChartYAxes = {[id: TimeSeriesChartYAxisId]: TimeSeriesChartYAxisSettings}; export interface TimeSeriesChartThreshold { @@ -415,16 +449,19 @@ export interface TimeSeriesChartThreshold { units?: string; decimals?: number; lineColor: string; - lineType: TimeSeriesChartLineType; + lineType: TimeSeriesChartLineType | number | number[]; lineWidth: number; - startSymbol: TimeSeriesChartShape; + startSymbol: EChartsShape; startSymbolSize: number; - endSymbol: TimeSeriesChartShape; + endSymbol: EChartsShape; endSymbolSize: number; showLabel: boolean; labelPosition: ThresholdLabelPosition; labelFont: Font; labelColor: string; + enableLabelBackground: boolean; + labelBackground: string; + additionalLabelOption?: {[key: string]: any}; } export const timeSeriesChartThresholdValid = (threshold: TimeSeriesChartThreshold): boolean => { @@ -469,9 +506,9 @@ export const timeSeriesChartThresholdDefaultSettings: TimeSeriesChartThreshold = lineColor: timeSeriesChartColorScheme['threshold.line'].light, lineType: TimeSeriesChartLineType.solid, lineWidth: 1, - startSymbol: TimeSeriesChartShape.none, + startSymbol: EChartsShape.none, startSymbolSize: 5, - endSymbol: TimeSeriesChartShape.arrow, + endSymbol: EChartsShape.arrow, endSymbolSize: 5, showLabel: true, labelPosition: ThresholdLabelPosition.end, @@ -483,7 +520,9 @@ export const timeSeriesChartThresholdDefaultSettings: TimeSeriesChartThreshold = weight: '400', lineHeight: '1' }, - labelColor: timeSeriesChartColorScheme['threshold.label'].light + labelColor: timeSeriesChartColorScheme['threshold.label'].light, + enableLabelBackground: false, + labelBackground: 'rgba(255,255,255,0.56)' }; export enum TimeSeriesChartNoAggregationBarWidthStrategy { @@ -513,6 +552,25 @@ export interface TimeSeriesChartNoAggregationBarWidthSettings { barWidth?: TimeSeriesChartBarWidth; } +export const timeSeriesChartNoAggregationBarWidthDefaultSettings: TimeSeriesChartNoAggregationBarWidthSettings = { + strategy: TimeSeriesChartNoAggregationBarWidthStrategy.group, + groupWidth: { + relative: true, + relativeWidth: 2, + absoluteWidth: 1000 + }, + barWidth: { + relative: true, + relativeWidth: 2, + absoluteWidth: 1000 + } +}; + +export interface TimeSeriesChartBarWidthSettings { + barGap: number; + intervalGap: number; +} + export enum TimeSeriesChartAnimationEasing { linear = 'linear', quadraticIn = 'quadraticIn', @@ -560,15 +618,114 @@ export interface TimeSeriesChartAnimationSettings { animationDelayUpdate: number; } -export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings { +export const timeSeriesChartAnimationDefaultSettings: TimeSeriesChartAnimationSettings = { + animation: true, + animationThreshold: 2000, + animationDuration: 500, + animationEasing: TimeSeriesChartAnimationEasing.cubicOut, + animationDelay: 0, + animationDurationUpdate: 300, + animationEasingUpdate: TimeSeriesChartAnimationEasing.cubicOut, + animationDelayUpdate: 0 +}; + +export interface TimeSeriesChartVisualMapPiece { + lt?: number; + gt?: number; + lte?: number; + gte?: number; + value?: number; + color?: string; +} + +export const createTimeSeriesChartVisualMapPiece = (color: string, from?: number, to?: number): TimeSeriesChartVisualMapPiece => { + const piece: TimeSeriesChartVisualMapPiece = { + color + }; + if (isNumber(from) && isNumber(to)) { + if (from === to) { + piece.value = from; + } else { + piece.gte = from; + piece.lt = to; + } + } else if (isNumber(from)) { + piece.gte = from; + } else if (isNumber(to)) { + piece.lt = to; + } + return piece; +}; + +export interface TimeSeriesChartVisualMapSettings { + outOfRangeColor: string; + pieces: TimeSeriesChartVisualMapPiece[]; +} + +export interface TimeSeriesChartStateSettings { + label: string; + value: number; + sourceType: TimeSeriesChartStateSourceType; + sourceValue?: any; + sourceRangeFrom?: number; + sourceRangeTo?: number; +} + +export const timeSeriesChartStateValid = (state: TimeSeriesChartStateSettings): boolean => { + if (isUndefinedOrNull(state.value) || !state.sourceType) { + return false; + } + switch (state.sourceType) { + case TimeSeriesChartStateSourceType.constant: + if (isUndefinedOrNull(state.sourceValue)) { + return false; + } + break; + } + return true; +}; + +export const timeSeriesChartStateValidator = (control: AbstractControl): ValidationErrors | null => { + const state: TimeSeriesChartStateSettings = control.value; + if (!timeSeriesChartStateValid(state)) { + return { + state: true + }; + } + return null; +}; + +export interface TimeSeriesChartComparisonSettings extends WidgetComparisonSettings { + comparisonXAxis?: TimeSeriesChartXAxisSettings; +} + +export interface TimeSeriesChartGridSettings { + show: boolean; + backgroundColor: string; + borderWidth: number; + borderColor: string; +} + +export const timeSeriesChartGridDefaultSettings: TimeSeriesChartGridSettings = { + show: false, + backgroundColor: null, + borderWidth: 1, + borderColor: '#ccc' +}; + +export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings, TimeSeriesChartComparisonSettings { thresholds: TimeSeriesChartThreshold[]; darkMode: boolean; dataZoom: boolean; stack: boolean; + grid: TimeSeriesChartGridSettings; yAxes: TimeSeriesChartYAxes; xAxis: TimeSeriesChartXAxisSettings; animation: TimeSeriesChartAnimationSettings; + barWidthSettings: TimeSeriesChartBarWidthSettings; noAggregationBarWidthSettings: TimeSeriesChartNoAggregationBarWidthSettings; + visualMapSettings?: TimeSeriesChartVisualMapSettings; + states?: TimeSeriesChartStateSettings[]; } export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { @@ -576,65 +733,23 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { darkMode: false, dataZoom: true, stack: false, + grid: mergeDeep({} as TimeSeriesChartGridSettings, + timeSeriesChartGridDefaultSettings), yAxes: { default: mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, { id: 'default', order: 0 } as TimeSeriesChartYAxisSettings) }, - xAxis: { - show: true, - label: '', - labelFont: { - family: 'Roboto', - size: 12, - sizeUnit: 'px', - style: 'normal', - weight: '600', - lineHeight: '1' - }, - labelColor: timeSeriesChartColorScheme['axis.label'].light, - position: AxisPosition.bottom, - showTickLabels: true, - tickLabelFont: { - family: 'Roboto', - size: 10, - sizeUnit: 'px', - style: 'normal', - weight: '400', - lineHeight: '1' - }, - tickLabelColor: timeSeriesChartColorScheme['axis.tickLabel'].light, - ticksFormat: {}, - showTicks: true, - ticksColor: timeSeriesChartColorScheme['axis.ticks'].light, - showLine: true, - lineColor: timeSeriesChartColorScheme['axis.line'].light, - showSplitLines: true, - splitLinesColor: timeSeriesChartColorScheme['axis.splitLine'].light - }, - animation: { - animation: true, - animationThreshold: 2000, - animationDuration: 500, - animationEasing: TimeSeriesChartAnimationEasing.cubicOut, - animationDelay: 0, - animationDurationUpdate: 300, - animationEasingUpdate: TimeSeriesChartAnimationEasing.cubicOut, - animationDelayUpdate: 0 - }, - noAggregationBarWidthSettings: { - strategy: TimeSeriesChartNoAggregationBarWidthStrategy.group, - groupWidth: { - relative: true, - relativeWidth: 2, - absoluteWidth: 1000 - }, - barWidth: { - relative: true, - relativeWidth: 2, - absoluteWidth: 1000 - } + xAxis: mergeDeep({} as TimeSeriesChartXAxisSettings, + defaultTimeSeriesChartXAxisSettings), + animation: mergeDeep({} as TimeSeriesChartAnimationSettings, + timeSeriesChartAnimationDefaultSettings), + barWidthSettings: { + barGap: 0.3, + intervalGap: 0.6 }, + noAggregationBarWidthSettings: mergeDeep({} as TimeSeriesChartNoAggregationBarWidthSettings, + timeSeriesChartNoAggregationBarWidthDefaultSettings), showTooltip: true, tooltipTrigger: EChartsTooltipTrigger.axis, tooltipValueFont: { @@ -659,7 +774,13 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { tooltipDateColor: 'rgba(0, 0, 0, 0.76)', tooltipDateInterval: true, tooltipBackgroundColor: 'rgba(255, 255, 255, 0.76)', - tooltipBackgroundBlur: 4 + tooltipBackgroundBlur: 4, + comparisonEnabled: false, + timeForComparison: 'previousInterval', + comparisonCustomIntervalValue: 7200000, + comparisonXAxis: mergeDeep({} as TimeSeriesChartXAxisSettings, + defaultTimeSeriesChartXAxisSettings, + { position: AxisPosition.top } as TimeSeriesChartXAxisSettings) }; export interface SeriesFillSettings { @@ -683,7 +804,10 @@ export interface LineSeriesSettings { pointLabelPosition: SeriesLabelPosition; pointLabelFont: Font; pointLabelColor: string; - pointShape: TimeSeriesChartShape; + enablePointLabelBackground: boolean; + pointLabelBackground: string; + pointLabelFormatter?: string | LabelFormatterCallback; + pointShape: EChartsShape; pointSize: number; fillAreaSettings: SeriesFillSettings; } @@ -693,19 +817,25 @@ export interface BarSeriesSettings { borderWidth: number; borderRadius: number; showLabel: boolean; - labelPosition: SeriesLabelPosition; + labelPosition: SeriesLabelPosition | BuiltinTextPosition; labelFont: Font; labelColor: string; + enableLabelBackground: boolean; + labelBackground: string; + labelFormatter?: string | LabelFormatterCallback; + labelLayout?: LabelLayoutOption | LabelLayoutOptionCallback; + additionalLabelOption?: {[key: string]: any}; backgroundSettings: SeriesFillSettings; } -export interface TimeSeriesChartKeySettings { +export interface TimeSeriesChartKeySettings extends DataKeySettingsWithComparison { yAxisId: TimeSeriesChartYAxisId; showInLegend: boolean; dataHiddenByDefault: boolean; type: TimeSeriesChartSeriesType; lineSettings: LineSeriesSettings; barSettings: BarSeriesSettings; + tooltipValueFormatter?: string | EChartsTooltipValueFormatFunction; } export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = { @@ -732,7 +862,9 @@ export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = { lineHeight: '1' }, pointLabelColor: timeSeriesChartColorScheme['series.label'].light, - pointShape: TimeSeriesChartShape.emptyCircle, + enablePointLabelBackground: false, + pointLabelBackground: 'rgba(255,255,255,0.56)', + pointShape: EChartsShape.emptyCircle, pointSize: 4, fillAreaSettings: { type: SeriesFillType.none, @@ -758,6 +890,8 @@ export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = { lineHeight: '1' }, labelColor: timeSeriesChartColorScheme['series.label'].light, + enableLabelBackground: false, + labelBackground: 'rgba(255,255,255,0.56)', backgroundSettings: { type: SeriesFillType.none, opacity: 0.4, @@ -766,10 +900,16 @@ export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = { end: 0 } } + }, + comparisonSettings: { + showValuesForComparison: false, + comparisonValuesLabel: '', + color: '' } }; export interface TimeSeriesChartDataItem extends EChartsSeriesItem { + xAxisIndex: number; yAxisId: TimeSeriesChartYAxisId; yAxisIndex: number; option?: LineSeriesOption | CustomSeriesOption; @@ -790,16 +930,27 @@ export interface TimeSeriesChartThresholdItem { option?: LineSeriesOption; } -export interface TimeSeriesChartYAxis { +export interface TimeSeriesChartAxis { id: string; + settings: TimeSeriesChartAxisSettings; + option: CartesianAxisOption; +} + +export interface TimeSeriesChartYAxis extends TimeSeriesChartAxis { decimals: number; settings: TimeSeriesChartYAxisSettings; option: YAXisOption & ValueAxisBaseOption; } +export interface TimeSeriesChartXAxis extends TimeSeriesChartAxis { + settings: TimeSeriesChartXAxisSettings; + option: XAXisOption; +} + export const createTimeSeriesYAxis = (units: string, decimals: number, settings: TimeSeriesChartYAxisSettings, + utils: UtilsService, darkMode: boolean): TimeSeriesChartYAxis => { const yAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont, settings.tickLabelColor, darkMode, 'axis.tickLabel'); @@ -840,6 +991,7 @@ export const createTimeSeriesYAxis = (units: string, decimals, settings, option: { + mainType: 'yAxis', show: settings.show, type: 'value', position: settings.position, @@ -853,7 +1005,7 @@ export const createTimeSeriesYAxis = (units: string, splitNumber, interval, ticksGenerator, - name: settings.label, + name: utils.customTranslation(settings.label, settings.label), nameLocation: 'middle', nameRotate: settings.position === AxisPosition.left ? 90 : -90, nameTextStyle: { @@ -907,72 +1059,113 @@ export const createTimeSeriesYAxis = (units: string, }; }; -export const createTimeSeriesXAxisOption = (settings: TimeSeriesChartXAxisSettings, - min: number, max: number, - datePipe: DatePipe, - darkMode: boolean): XAXisOption => { +export const createTimeSeriesXAxis = (id: string, + settings: TimeSeriesChartXAxisSettings, + min: number, max: number, + datePipe: DatePipe, + utils: UtilsService, + darkMode: boolean): TimeSeriesChartXAxis => { const xAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont, settings.tickLabelColor, darkMode, 'axis.tickLabel'); const xAxisNameStyle = createChartTextStyle(settings.labelFont, settings.labelColor, darkMode, 'axis.label'); const ticksFormat = mergeDeep({}, defaultXAxisTicksFormat, settings.ticksFormat); return { - show: settings.show, - type: 'time', - scale: true, - position: settings.position, - name: settings.label, - nameLocation: 'middle', - nameTextStyle: { - color: xAxisNameStyle.color, - fontStyle: xAxisNameStyle.fontStyle, - fontWeight: xAxisNameStyle.fontWeight, - fontFamily: xAxisNameStyle.fontFamily, - fontSize: xAxisNameStyle.fontSize - }, - axisTick: { - show: settings.showTicks, - lineStyle: { - color: prepareChartThemeColor(settings.ticksColor, darkMode, 'axis.ticks') - } - }, - axisLabel: { - show: settings.showTickLabels, - color: xAxisTickLabelStyle.color, - fontStyle: xAxisTickLabelStyle.fontStyle, - fontWeight: xAxisTickLabelStyle.fontWeight, - fontFamily: xAxisTickLabelStyle.fontFamily, - fontSize: xAxisTickLabelStyle.fontSize, - hideOverlap: true, - formatter: (value: number, index: number, extra: {level: number}) => { - const unit = tsToFormatTimeUnit(value); - const format = ticksFormat[unit]; - const formatted = datePipe.transform(value, format); - if (extra.level > 0) { - return `{primary|${formatted}}`; - } else { - return formatted; + id, + settings, + option: { + mainType: 'xAxis', + show: settings.show, + type: 'time', + scale: true, + position: settings.position, + id, + name: utils.customTranslation(settings.label, settings.label), + nameLocation: 'middle', + nameTextStyle: { + color: xAxisNameStyle.color, + fontStyle: xAxisNameStyle.fontStyle, + fontWeight: xAxisNameStyle.fontWeight, + fontFamily: xAxisNameStyle.fontFamily, + fontSize: xAxisNameStyle.fontSize + }, + axisPointer: { + shadowStyle: { + color: id === 'main' ? 'rgba(210,219,238,0.2)' : 'rgba(150,150,150,0.1)' } - } - }, - axisLine: { - show: settings.showLine, - onZero: false, - lineStyle: { - color: prepareChartThemeColor(settings.lineColor, darkMode, 'axis.line') - } - }, - splitLine: { - show: settings.showSplitLines, - lineStyle: { - color: prepareChartThemeColor(settings.splitLinesColor, darkMode, 'axis.splitLine') - } - }, - min, - max + }, + axisTick: { + show: settings.showTicks, + lineStyle: { + color: prepareChartThemeColor(settings.ticksColor, darkMode, 'axis.ticks') + } + }, + axisLabel: { + show: settings.showTickLabels, + color: xAxisTickLabelStyle.color, + fontStyle: xAxisTickLabelStyle.fontStyle, + fontWeight: xAxisTickLabelStyle.fontWeight, + fontFamily: xAxisTickLabelStyle.fontFamily, + fontSize: xAxisTickLabelStyle.fontSize, + hideOverlap: true, + /** Min/Max time label always visible **/ + /* alignMinLabel: 'left', + alignMaxLabel: 'right', + showMinLabel: true, + showMaxLabel: true, */ + formatter: (value: number, _index: number, extra: {level: number}) => { + const unit = tsToFormatTimeUnit(value); + const format = ticksFormat[unit]; + const formatted = datePipe.transform(value, format); + if (extra.level > 0) { + return `{primary|${formatted}}`; + } else { + return formatted; + } + } + }, + axisLine: { + show: settings.showLine, + onZero: false, + lineStyle: { + color: prepareChartThemeColor(settings.lineColor, darkMode, 'axis.line') + } + }, + splitLine: { + show: settings.showSplitLines, + lineStyle: { + color: prepareChartThemeColor(settings.splitLinesColor, darkMode, 'axis.splitLine') + } + }, + min, + max, + bandWidthCalculator: timeAxisBandWidthCalculator + } }; }; +export const createTimeSeriesVisualMapOption = (settings: TimeSeriesChartVisualMapSettings, + selectedRanges: {[key: number]: boolean}): VisualMapComponentOption => ({ + show: false, + type: 'piecewise', + selected: selectedRanges, + dimension: 1, + pieces: settings.pieces, + outOfRange: { + color: settings.outOfRangeColor +}, + inRange: !settings.pieces.length ? { + color: settings.outOfRangeColor + } : undefined +}); + +export const updateXAxisTimeWindow = (option: XAXisOption, + timeWindow: WidgetTimewindow) => { + option.min = timeWindow.minTime; + option.max = timeWindow.maxTime; + (option as any).tbTimeWindow = timeWindow; +}; + export const generateChartData = (dataItems: TimeSeriesChartDataItem[], thresholdItems: TimeSeriesChartThresholdItem[], stack: boolean, @@ -982,7 +1175,7 @@ export const generateChartData = (dataItems: TimeSeriesChartDataItem[], let series = generateChartSeries(dataItems, stack, noAggregation, barRenderSharedContext, darkMode); if (thresholdItems.length) { - const thresholds = generateChartThresholds(thresholdItems, darkMode); + const thresholds = generateChartThresholds(thresholdItems); series = series.concat(thresholds); } return series; @@ -994,7 +1187,7 @@ export const calculateThresholdsOffset = (chart: ECharts, const result: [number, number] = [0, 0]; for (const item of thresholdItems) { const yAxis = yAxisList[item.yAxisIndex]; - const offset = measureThresholdLabelOffset(chart, yAxis.id, item.id, item.value); + const offset = measureThresholdOffset(chart, yAxis.id, item.id, item.value); result[0] = Math.max(result[0], offset[0]); result[1] = Math.max(result[1], offset[1]); } @@ -1016,7 +1209,7 @@ export const parseThresholdData = (value: any): TimeSeriesChartThresholdValue => return thresholdValue; }; -const generateChartThresholds = (thresholdItems: TimeSeriesChartThresholdItem[], darkMode: boolean): Array => { +const generateChartThresholds = (thresholdItems: TimeSeriesChartThresholdItem[]): Array => { const series: Array = []; for (const item of thresholdItems) { if (isDefinedAndNotNull(item.value)) { @@ -1055,6 +1248,14 @@ const generateChartThresholds = (thresholdItems: TimeSeriesChartThresholdItem[], } } }; + if (item.settings.enableLabelBackground) { + seriesOption.markLine.label.backgroundColor = item.settings.labelBackground; + seriesOption.markLine.label.padding = [4, 5]; + seriesOption.markLine.label.borderRadius = 4; + } + if (item.settings.additionalLabelOption) { + seriesOption.markLine.label = {...seriesOption.markLine.label, ...item.settings.additionalLabelOption}; + } item.option = seriesOption; } seriesOption.markLine.data = []; @@ -1129,8 +1330,9 @@ const generateChartSeries = (dataItems: TimeSeriesChartDataItem[], }; export const updateDarkMode = (options: EChartsOption, settings: TimeSeriesChartSettings, + xAxisList: TimeSeriesChartXAxis[], yAxisList: TimeSeriesChartYAxis[], - dataItems: TimeSeriesChartDataItem[], thresholdDataItems: TimeSeriesChartThresholdItem[], + dataItems: TimeSeriesChartDataItem[], darkMode: boolean): EChartsOption => { options.darkMode = darkMode; if (Array.isArray(options.yAxis)) { @@ -1145,12 +1347,14 @@ export const updateDarkMode = (options: EChartsOption, settings: TimeSeriesChart } } if (Array.isArray(options.xAxis)) { - for (const xAxis of options.xAxis) { - xAxis.nameTextStyle.color = prepareChartThemeColor(settings.xAxis.labelColor, darkMode, 'axis.label'); - xAxis.axisLabel.color = prepareChartThemeColor(settings.xAxis.tickLabelColor, darkMode, 'axis.tickLabel'); - xAxis.axisLine.lineStyle.color = prepareChartThemeColor(settings.xAxis.lineColor, darkMode, 'axis.line'); - xAxis.axisTick.lineStyle.color = prepareChartThemeColor(settings.xAxis.ticksColor, darkMode, 'axis.ticks'); - xAxis.splitLine.lineStyle.color = prepareChartThemeColor(settings.xAxis.splitLinesColor, darkMode, 'axis.splitLine'); + for (let i = 0; i < options.xAxis.length; i++) { + const xAxis = options.xAxis[i]; + const xAxisSettings = xAxisList[i].settings; + xAxis.nameTextStyle.color = prepareChartThemeColor(xAxisSettings.labelColor, darkMode, 'axis.label'); + xAxis.axisLabel.color = prepareChartThemeColor(xAxisSettings.tickLabelColor, darkMode, 'axis.tickLabel'); + xAxis.axisLine.lineStyle.color = prepareChartThemeColor(xAxisSettings.lineColor, darkMode, 'axis.line'); + xAxis.axisTick.lineStyle.color = prepareChartThemeColor(xAxisSettings.ticksColor, darkMode, 'axis.ticks'); + xAxis.splitLine.lineStyle.color = prepareChartThemeColor(xAxisSettings.splitLinesColor, darkMode, 'axis.splitLine'); } } for (const item of dataItems) { @@ -1170,7 +1374,7 @@ export const updateDarkMode = (options: EChartsOption, settings: TimeSeriesChart } else { if (item.barRenderContext?.labelOption?.show) { const barSettings = item.dataKey.settings as BarSeriesSettings; - item.barRenderContext.labelOption.rich.value.color = prepareChartThemeColor(barSettings.labelColor, + (item.barRenderContext.labelOption.rich.value as any).fill = prepareChartThemeColor(barSettings.labelColor, darkMode, 'series.label'); } } @@ -1189,6 +1393,7 @@ const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem, seriesOption = { id: item.id, dataGroupId: item.id, + xAxisIndex: item.xAxisIndex, yAxisIndex: item.yAxisIndex, name: item.dataKey.label, color: seriesColor, @@ -1214,11 +1419,14 @@ const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem, const lineSeriesOption = seriesOption as LineSeriesOption; lineSeriesOption.type = 'line'; lineSeriesOption.label = createSeriesLabelOption(item, lineSettings.showPointLabel, - lineSettings.pointLabelFont, lineSettings.pointLabelColor, lineSettings.pointLabelPosition, darkMode); + lineSettings.pointLabelFont, lineSettings.pointLabelColor, + lineSettings.enablePointLabelBackground, lineSettings.pointLabelBackground, + lineSettings.pointLabelPosition, + lineSettings.pointLabelFormatter, false, darkMode); lineSeriesOption.step = lineSettings.step ? lineSettings.stepType : false; - lineSeriesOption.smooth = lineSettings.smooth; + lineSeriesOption.smooth = lineSettings.smooth ? 0.25 : false; if (lineSettings.smooth) { - lineSeriesOption.smoothMonotone = 'x'; + lineSeriesOption.smoothMonotone = 'none'; } lineSeriesOption.lineStyle = { width: lineSettings.showLine ? lineSettings.lineWidth : 0, @@ -1228,7 +1436,7 @@ const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem, lineSeriesOption.areaStyle = {}; if (lineSettings.fillAreaSettings.type === SeriesFillType.opacity) { lineSeriesOption.areaStyle.opacity = lineSettings.fillAreaSettings.opacity; - } else { + } else if (lineSettings.fillAreaSettings.type === SeriesFillType.gradient) { lineSeriesOption.areaStyle.opacity = 1; lineSeriesOption.areaStyle.color = createLinearOpacityGradient(seriesColor, lineSettings.fillAreaSettings.gradient); } @@ -1255,9 +1463,12 @@ const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem, } item.barRenderContext.visualSettings = barVisualSettings; item.barRenderContext.labelOption = createSeriesLabelOption(item, barSettings.showLabel, - barSettings.labelFont, barSettings.labelColor, barSettings.labelPosition, darkMode); + barSettings.labelFont, barSettings.labelColor, barSettings.enableLabelBackground, barSettings.labelBackground, + barSettings.labelPosition, barSettings.labelFormatter, true, darkMode); + item.barRenderContext.additionalLabelOption = barSettings.additionalLabelOption; barSeriesOption.renderItem = (params, api) => renderTimeSeriesBar(params, api, item.barRenderContext); + barSeriesOption.labelLayout = barSettings.labelLayout; } } seriesOption.data = item.data; @@ -1265,30 +1476,63 @@ const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem, }; const createSeriesLabelOption = (item: TimeSeriesChartDataItem, show: boolean, - labelFont: Font, labelColor: string, position: SeriesLabelPosition, + labelFont: Font, labelColor: string, + enableBackground: boolean, labelBackground: string, + position: SeriesLabelPosition | BuiltinTextPosition, + labelFormatter: string | LabelFormatterCallback, + labelColorFill: boolean, darkMode: boolean): SeriesLabelOption => { let labelStyle: ComponentStyle = {}; if (show) { - labelStyle = createChartTextStyle(labelFont, labelColor, darkMode, 'series.label'); + labelStyle = createChartTextStyle(labelFont, labelColor, darkMode, 'series.label', labelColorFill); } - return { + let formatFunction: (...args: any[]) => any; + if (typeof labelFormatter === 'string' && labelFormatter.length) { + formatFunction = parseFunction(labelFormatter, ['value']); + } + const formatter: LabelFormatterCallback = (params): string => { + let result: string; + if (typeof labelFormatter === 'string') { + if (formatFunction) { + try { + result = formatFunction(params.value[1]); + } catch (_e) { + } + } + } else if (isFunction(labelFormatter)) { + result = labelFormatter(params); + } + if (isUndefined(result)) { + result = formatValue(params.value[1], item.decimals, item.units, false); + result = `{value|${result}}`; + } + return result; + }; + const labelOption: SeriesLabelOption = { show, position, - formatter: (params): string => { - const value = formatValue(params.value[1], item.decimals, item.units, false); - return `{value|${value}}`; - }, + formatter, rich: { value: labelStyle } }; + if (enableBackground) { + labelOption.backgroundColor = labelBackground; + labelOption.padding = [4, 5]; + labelOption.borderRadius = 4; + } + return labelOption; }; -const createChartTextStyle = (font: Font, color: string, darkMode: boolean, colorKey?: string): ComponentStyle => { +const createChartTextStyle = (font: Font, color: string, darkMode: boolean, colorKey?: string, fill = false): ComponentStyle => { const style = textStyle(font); delete style.lineHeight; style.fontSize = font.size; - style.color = prepareChartThemeColor(color, darkMode, colorKey); + if (fill) { + style.fill = prepareChartThemeColor(color, darkMode, colorKey); + } else { + style.color = prepareChartThemeColor(color, darkMode, colorKey); + } return style; }; 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 65a323e4b6..2f987f14fe 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 @@ -16,14 +16,16 @@ import { WidgetContext } from '@home/models/widget-component.models'; import { - AxisPosition, calculateThresholdsOffset, - createTimeSeriesXAxisOption, + createTimeSeriesVisualMapOption, + createTimeSeriesXAxis, createTimeSeriesYAxis, defaultTimeSeriesChartYAxisSettings, generateChartData, + LineSeriesStepType, parseThresholdData, SeriesLabelPosition, + TimeSeriesChartAxis, TimeSeriesChartDataItem, timeSeriesChartDefaultSettings, timeSeriesChartKeyDefaultSettings, @@ -31,38 +33,41 @@ import { TimeSeriesChartNoAggregationBarWidthStrategy, TimeSeriesChartSeriesType, TimeSeriesChartSettings, - TimeSeriesChartShape, TimeSeriesChartThreshold, timeSeriesChartThresholdDefaultSettings, TimeSeriesChartThresholdItem, TimeSeriesChartThresholdType, TimeSeriesChartType, + TimeSeriesChartXAxis, TimeSeriesChartYAxis, TimeSeriesChartYAxisId, TimeSeriesChartYAxisSettings, - updateDarkMode + updateDarkMode, + updateXAxisTimeWindow } from '@home/components/widget/lib/chart/time-series-chart.models'; import { ResizeObserver } from '@juggle/resize-observer'; import { - calculateXAxisHeight, - calculateYAxisWidth, + adjustTimeAxisExtentToData, + calculateAxisSize, + createTooltipValueFormatFunction, ECharts, echartsModule, EChartsOption, + EChartsShape, echartsTooltipFormatter, EChartsTooltipTrigger, + EChartsTooltipValueFormatFunction, getAxisExtent, - measureXAxisNameHeight, - measureYAxisNameWidth, + getFocusedSeriesIndex, + measureAxisNameSize, toNamedData } from '@home/components/widget/lib/chart/echarts-widget.models'; import { DateFormatProcessor } from '@shared/models/widget-settings.models'; -import { isDefinedAndNotNull, isEqual, mergeDeep } from '@core/utils'; -import { DataKey, Datasource, DatasourceType, widgetType } from '@shared/models/widget.models'; +import { formattedDataFormDatasourceData, formatValue, isDefinedAndNotNull, isEqual, mergeDeep } from '@core/utils'; +import { DataKey, Datasource, DatasourceType, FormattedData, widgetType } from '@shared/models/widget.models'; import * as echarts from 'echarts/core'; -import { CallbackDataParams } from 'echarts/types/dist/shared'; +import { CallbackDataParams, PiecewiseVisualMapOption } from 'echarts/types/dist/shared'; import { Renderer2 } from '@angular/core'; -import { CustomSeriesOption, LineSeriesOption } from 'echarts/charts'; import { BehaviorSubject } from 'rxjs'; import { AggregationType } from '@shared/models/time/time.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; @@ -70,6 +75,7 @@ import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { DeepPartial } from '@shared/models/common'; import { BarRenderSharedContext } from '@home/components/widget/lib/chart/time-series-chart-bar.models'; +import { TimeSeriesChartStateValueConverter } from '@home/components/widget/lib/chart/time-series-chart-state.models'; export class TbTimeSeriesChart { @@ -86,8 +92,15 @@ export class TbTimeSeriesChart { settings.type = TimeSeriesChartSeriesType.line; settings.lineSettings.showLine = false; settings.lineSettings.showPoints = true; - settings.lineSettings.pointShape = TimeSeriesChartShape.circle; + settings.lineSettings.pointShape = EChartsShape.circle; settings.lineSettings.pointSize = 8; + } else if (type === TimeSeriesChartType.state) { + settings.type = TimeSeriesChartSeriesType.line; + settings.lineSettings.showLine = true; + settings.lineSettings.step = true; + settings.lineSettings.stepType = LineSeriesStepType.end; + settings.lineSettings.pointShape = EChartsShape.circle; + settings.lineSettings.pointSize = 12; } return settings; } @@ -96,21 +109,34 @@ export class TbTimeSeriesChart { } private get noAggregation(): boolean { - return this.ctx.defaultSubscription.timeWindowConfig?.aggregation?.type === AggregationType.NONE; + return this.ctx.defaultSubscription.subscriptionTimewindow?.aggregation?.type === AggregationType.NONE; + } + + private get stateData(): boolean { + return this.ctx.defaultSubscription.subscriptionTimewindow?.aggregation?.stateData === true; } private readonly shapeResize$: ResizeObserver; private readonly settings: TimeSeriesChartSettings; + private readonly comparisonEnabled: boolean; + private readonly stackMode: boolean; + + private xAxisList: TimeSeriesChartXAxis[] = []; private yAxisList: TimeSeriesChartYAxis[] = []; private dataItems: TimeSeriesChartDataItem[] = []; private thresholdItems: TimeSeriesChartThresholdItem[] = []; + private hasVisualMap = false; + private visualMapSelectedRanges: {[key: number]: boolean}; + private timeSeriesChart: ECharts; private timeSeriesChartOptions: EChartsOption; private readonly tooltipDateFormat: DateFormatProcessor; + private readonly tooltipValueFormatFunction: EChartsTooltipValueFormatFunction; + private readonly stateValueConverter: TimeSeriesChartStateValueConverter; private yMinSubject = new BehaviorSubject(-1); private yMaxSubject = new BehaviorSubject(1); @@ -127,6 +153,8 @@ export class TbTimeSeriesChart { private barRenderSharedContext: BarRenderSharedContext; + private latestData: FormattedData[] = []; + yMin$ = this.yMinSubject.asObservable(); yMax$ = this.yMaxSubject.asObservable(); @@ -139,14 +167,31 @@ export class TbTimeSeriesChart { this.settings = mergeDeep({} as TimeSeriesChartSettings, timeSeriesChartDefaultSettings, this.inputSettings as TimeSeriesChartSettings); + this.comparisonEnabled = !!this.ctx.defaultSubscription.comparisonEnabled; + this.stackMode = !this.comparisonEnabled && this.settings.stack; + if (this.settings.states && this.settings.states.length) { + this.stateValueConverter = new TimeSeriesChartStateValueConverter(this.ctx.utilsService, this.settings.states); + this.tooltipValueFormatFunction = this.stateValueConverter.tooltipFormatter; + } const $dashboardPageElement = this.ctx.$containerParent.parents('.tb-dashboard-page'); - const dashboardPageElement = $($dashboardPageElement[$dashboardPageElement.length-1]); - this.darkMode = this.settings.darkMode || dashboardPageElement.hasClass('dark'); + const dashboardPageElement = $dashboardPageElement.length ? $($dashboardPageElement[$dashboardPageElement.length-1]) : null; + this.darkMode = this.settings.darkMode || dashboardPageElement?.hasClass('dark'); + this.setupXAxes(); this.setupYAxes(); this.setupData(); this.setupThresholds(); - if (this.settings.showTooltip && this.settings.tooltipShowDate) { - this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat); + this.setupVisualMap(); + if (this.settings.showTooltip) { + if (this.settings.tooltipShowDate) { + this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat); + } + if (!this.tooltipValueFormatFunction) { + this.tooltipValueFormatFunction = + createTooltipValueFormatFunction(this.settings.tooltipValueFormatter); + if (!this.tooltipValueFormatFunction) { + this.tooltipValueFormatFunction = (value, latestData, units, decimals) => formatValue(value, decimals, units, false); + } + } } this.onResize(); if (this.autoResize) { @@ -155,15 +200,17 @@ export class TbTimeSeriesChart { }); this.shapeResize$.observe(this.chartElement); } - this.darkModeObserver = new MutationObserver(mutations => { - for(let mutation of mutations) { - if (mutation.type === 'attributes' && mutation.attributeName === 'class') { - const darkMode = dashboardPageElement.hasClass('dark'); - this.setDarkMode(darkMode); + if (dashboardPageElement) { + this.darkModeObserver = new MutationObserver(mutations => { + for (const mutation of mutations) { + if (mutation.type === 'attributes' && mutation.attributeName === 'class') { + const darkMode = dashboardPageElement.hasClass('dark'); + this.setDarkMode(darkMode); + } } - } - }); - this.darkModeObserver.observe(dashboardPageElement[0], { attributes: true }); + }); + this.darkModeObserver.observe(dashboardPageElement[0], {attributes: true}); + } } public update(): void { @@ -171,19 +218,24 @@ export class TbTimeSeriesChart { const datasourceData = this.ctx.data ? this.ctx.data.find(d => d.dataKey === item.dataKey) : null; if (!isEqual(item.dataSet, datasourceData?.data)) { item.dataSet = datasourceData?.data; - item.data = datasourceData?.data ? toNamedData(datasourceData.data) : []; + item.data = datasourceData?.data ? toNamedData(datasourceData.data, this.stateValueConverter?.valueConverter) : []; } } this.onResize(); if (this.timeSeriesChart) { - this.timeSeriesChartOptions.xAxis[0].min = this.ctx.defaultSubscription.timeWindow.minTime; - this.timeSeriesChartOptions.xAxis[0].max = this.ctx.defaultSubscription.timeWindow.maxTime; - this.timeSeriesChartOptions.xAxis[0].tbTimeWindow = this.ctx.defaultSubscription.timeWindow; + updateXAxisTimeWindow(this.xAxisList[0].option, this.ctx.defaultSubscription.timeWindow); if (this.noAggregation) { this.timeSeriesChartOptions.tooltip[0].axisPointer.type = 'line'; } else { this.timeSeriesChartOptions.tooltip[0].axisPointer.type = 'shadow'; } + if (this.comparisonEnabled) { + updateXAxisTimeWindow(this.xAxisList[1].option, this.ctx.defaultSubscription.comparisonTimeWindow); + } + this.timeSeriesChartOptions.xAxis = this.xAxisList.map(axis => axis.option); + if (this.hasVisualMap) { + (this.timeSeriesChartOptions.visualMap as PiecewiseVisualMapOption).selected = this.visualMapSelectedRanges; + } this.barRenderSharedContext.timeInterval = this.ctx.timeWindow.interval; this.updateSeriesData(true); if (this.highlightedDataKey) { @@ -195,6 +247,14 @@ export class TbTimeSeriesChart { public latestUpdated() { let update = false; if (this.ctx.latestData) { + this.latestData = formattedDataFormDatasourceData(this.ctx.latestData); + for (const item of this.dataItems) { + let latestData = this.latestData.find(data => data.$datasource === item.datasource); + if (!latestData) { + latestData = {} as FormattedData; + } + item.latestData = latestData; + } for (const item of this.thresholdItems) { if (item.settings.type === TimeSeriesChartThresholdType.latestKey && item.latestDataKey) { const data = this.ctx.latestData.find(d => d.dataKey === item.latestDataKey); @@ -243,13 +303,13 @@ export class TbTimeSeriesChart { seriesId: dataItem.id }); } - this.timeSeriesChartOptions.series = this.updateSeries(); + this.updateSeries(); const mergeList = ['series']; if (this.updateYAxisScale(this.yAxisList)) { this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); mergeList.push('yAxis'); } - this.timeSeriesChart.setOption(this.timeSeriesChartOptions, this.settings.stack ? {notMerge: true} : {replaceMerge: mergeList}); + this.timeSeriesChart.setOption(this.timeSeriesChartOptions, this.stackMode ? {notMerge: true} : {replaceMerge: mergeList}); this.updateAxes(); dataKey.hidden = !enable; if (enable) { @@ -261,6 +321,16 @@ export class TbTimeSeriesChart { } } + public toggleVisualMapRange(index: number): void { + if (this.hasVisualMap) { + this.visualMapSelectedRanges[index] = !this.visualMapSelectedRanges[index]; + this.timeSeriesChart.dispatchAction({ + type: 'selectDataRange', + selected: this.visualMapSelectedRanges + }); + } + } + public destroy(): void { if (this.shapeResize$) { this.shapeResize$.disconnect(); @@ -270,7 +340,7 @@ export class TbTimeSeriesChart { } this.yMinSubject.complete(); this.yMaxSubject.complete(); - this.darkModeObserver.disconnect(); + this.darkModeObserver?.disconnect(); } public resize(): void { @@ -282,8 +352,7 @@ export class TbTimeSeriesChart { this.darkMode = darkMode; if (this.timeSeriesChart) { this.timeSeriesChartOptions = updateDarkMode(this.timeSeriesChartOptions, - this.settings, this.yAxisList, this.dataItems, - this.thresholdItems, darkMode); + this.settings, this.xAxisList, this.yAxisList, this.dataItems, darkMode); this.timeSeriesChart.setOption(this.timeSeriesChartOptions); } } @@ -298,6 +367,8 @@ export class TbTimeSeriesChart { const targetBarWidth = noAggregationBarWidthSettings.strategy === TimeSeriesChartNoAggregationBarWidthStrategy.group ? noAggregationBarWidthSettings.groupWidth : noAggregationBarWidthSettings.barWidth; this.barRenderSharedContext = { + barGap: this.settings.barWidthSettings.barGap, + intervalGap: this.settings.barWidthSettings.intervalGap, timeInterval: this.ctx.timeWindow?.interval, noAggregationBarWidthStrategy: noAggregationBarWidthSettings.strategy, noAggregationWidthRelative: targetBarWidth.relative, @@ -311,12 +382,18 @@ export class TbTimeSeriesChart { timeSeriesChartKeyDefaultSettings, dataKey.settings); if ((keySettings.type === TimeSeriesChartSeriesType.line && keySettings.lineSettings.showPointLabel && keySettings.lineSettings.pointLabelPosition === SeriesLabelPosition.top) || - (keySettings.type === TimeSeriesChartSeriesType.bar && keySettings.barSettings.showLabel)) { + (keySettings.type === TimeSeriesChartSeriesType.bar && + keySettings.barSettings.showLabel && + [SeriesLabelPosition.top, SeriesLabelPosition.bottom] + .includes(keySettings.barSettings.labelPosition as SeriesLabelPosition))) { this.topPointLabels = true; } + if (this.stateValueConverter && keySettings.type === TimeSeriesChartSeriesType.line) { + keySettings.lineSettings.pointLabelFormatter = this.stateValueConverter.labelFormatter; + } dataKey.settings = keySettings; const datasourceData = this.ctx.data ? this.ctx.data.find(d => d.dataKey === dataKey) : null; - const namedData = datasourceData?.data ? toNamedData(datasourceData.data) : []; + const namedData = datasourceData?.data ? toNamedData(datasourceData.data, this.stateValueConverter?.valueConverter) : []; const units = dataKey.units && dataKey.units.length ? dataKey.units : this.ctx.units; const decimals = isDefinedAndNotNull(dataKey.decimals) ? dataKey.decimals : (isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2); @@ -324,15 +401,21 @@ export class TbTimeSeriesChart { if (!Object.keys(this.settings.yAxes).includes(yAxisId)) { yAxisId = 'default'; } + const comparisonItem = this.comparisonEnabled && dataKey.isAdditional; + const xAxisIndex = comparisonItem ? 1 : 0; this.dataItems.push({ id: this.nextComponentId(), units, decimals, + xAxisIndex, yAxisId, yAxisIndex: this.getYAxisIndex(yAxisId), + comparisonItem, + datasource, dataKey, data: namedData, - enabled: !keySettings.dataHiddenByDefault + enabled: !keySettings.dataHiddenByDefault, + tooltipValueFormatFunction: createTooltipValueFormatFunction(keySettings.tooltipValueFormatter) }); } } @@ -344,6 +427,11 @@ export class TbTimeSeriesChart { for (const thresholdSettings of this.settings.thresholds) { const threshold = mergeDeep({} as TimeSeriesChartThreshold, timeSeriesChartThresholdDefaultSettings, thresholdSettings); + if (!this.topPointLabels) { + if (threshold.showLabel && !threshold.labelPosition.endsWith('Bottom')) { + this.topPointLabels = true; + } + } let latestDataKey: DataKey = null; let entityDataKey: DataKey = null; let value = null; @@ -413,6 +501,18 @@ export class TbTimeSeriesChart { this.subscribeForEntityThresholds(thresholdDatasources); } + private setupXAxes(): void { + const mainXAxis = createTimeSeriesXAxis('main', this.settings.xAxis, this.ctx.defaultSubscription.timeWindow.minTime, + this.ctx.defaultSubscription.timeWindow.maxTime, this.ctx.date, this.ctx.utilsService, this.darkMode); + this.xAxisList.push(mainXAxis); + if (this.comparisonEnabled) { + const comparisonXAxis = createTimeSeriesXAxis('comparison', this.settings.comparisonXAxis, + this.ctx.defaultSubscription.comparisonTimeWindow.minTime, this.ctx.defaultSubscription.comparisonTimeWindow.maxTime, + this.ctx.date, this.ctx.utilsService, this.darkMode); + this.xAxisList.push(comparisonXAxis); + } + } + private setupYAxes(): void { const yAxisSettingsList = Object.values(this.settings.yAxes); yAxisSettingsList.sort((a1, a2) => a1.order - a2.order); @@ -422,11 +522,24 @@ export class TbTimeSeriesChart { const units = axisSettings.units && axisSettings.units.length ? axisSettings.units : this.ctx.units; const decimals = isDefinedAndNotNull(axisSettings.decimals) ? axisSettings.decimals : (isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2); - const yAxis = createTimeSeriesYAxis(units, decimals, axisSettings, this.darkMode); + if (this.stateValueConverter) { + axisSettings.ticksGenerator = this.stateValueConverter.ticksGenerator; + axisSettings.ticksFormatter = this.stateValueConverter.ticksFormatter; + } + const yAxis = createTimeSeriesYAxis(units, decimals, axisSettings, this.ctx.utilsService, this.darkMode); this.yAxisList.push(yAxis); } } + private setupVisualMap(): void { + if (this.settings.visualMapSettings?.pieces && this.settings.visualMapSettings?.pieces.length) { + this.hasVisualMap = true; + this.visualMapSelectedRanges = {}; + this.settings.visualMapSettings.pieces.forEach((_val, index) => { + this.visualMapSelectedRanges[index] = true; + }); + } + } private nextComponentId(): string { return (this.componentIndexCounter++) + ''; @@ -490,35 +603,39 @@ export class TbTimeSeriesChart { }, formatter: (params: CallbackDataParams[]) => this.settings.showTooltip ? echartsTooltipFormatter(this.renderer, this.tooltipDateFormat, - this.settings, params, 0, '', -1, this.dataItems, - this.noAggregation ? null : this.ctx.timeWindow.interval) : undefined, + this.settings, params, this.tooltipValueFormatFunction, + this.settings.tooltipShowFocusedSeries ? getFocusedSeriesIndex(this.timeSeriesChart) : -1, + this.dataItems, this.noAggregation ? null : this.ctx.timeWindow.interval) : undefined, padding: [8, 12], backgroundColor: this.settings.tooltipBackgroundColor, borderWidth: 0, extraCssText: `line-height: 1; backdrop-filter: blur(${this.settings.tooltipBackgroundBlur}px);` }], grid: [{ + show: this.settings.grid.show, + backgroundColor: this.settings.grid.backgroundColor, + borderWidth: this.settings.grid.borderWidth, + borderColor: this.settings.grid.borderColor, top: this.minTopOffset(), left: this.settings.dataZoom ? 5 : 0, right: this.settings.dataZoom ? 5 : 0, bottom: this.minBottomOffset() }], - xAxis: [ - createTimeSeriesXAxisOption(this.settings.xAxis, this.ctx.defaultSubscription.timeWindow.minTime, - this.ctx.defaultSubscription.timeWindow.maxTime, this.ctx.date, this.darkMode) - ], + xAxis: this.xAxisList.map(axis => axis.option), yAxis: this.yAxisList.map(axis => axis.option), dataZoom: [ { type: 'inside', disabled: !this.settings.dataZoom, - realtime: true + realtime: true, + filterMode: this.stateData ? 'none' : 'weakFilter' }, { type: 'slider', show: this.settings.dataZoom, showDetail: false, realtime: true, + filterMode: this.stateData ? 'none' : 'weakFilter', bottom: 10 } ], @@ -531,16 +648,20 @@ export class TbTimeSeriesChart { animationEasingUpdate: this.settings.animation.animationEasingUpdate, animationDelayUpdate: this.settings.animation.animationDelayUpdate }; + if (this.hasVisualMap) { + this.timeSeriesChartOptions.visualMap = + createTimeSeriesVisualMapOption(this.settings.visualMapSettings, this.visualMapSelectedRanges); + } this.timeSeriesChartOptions.xAxis[0].tbTimeWindow = this.ctx.defaultSubscription.timeWindow; - this.timeSeriesChartOptions.series = this.updateSeries(); + this.updateSeries(); if (this.updateYAxisScale(this.yAxisList)) { this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); } this.timeSeriesChart.setOption(this.timeSeriesChartOptions); - this.updateAxes(); + this.updateAxes(false); if (this.settings.dataZoom) { this.timeSeriesChart.on('datazoom', () => { @@ -550,7 +671,7 @@ export class TbTimeSeriesChart { } private updateSeriesData(updateScale = false): void { - this.timeSeriesChartOptions.series = this.updateSeries(); + this.updateSeries(); if (updateScale && this.updateYAxisScale(this.yAxisList)) { this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); } @@ -558,46 +679,40 @@ export class TbTimeSeriesChart { this.updateAxes(); } - private updateSeries(): Array { - return generateChartData(this.dataItems, this.thresholdItems, - this.settings.stack, + private updateSeries(): void { + this.timeSeriesChartOptions.series = generateChartData(this.dataItems, this.thresholdItems, + this.stackMode, this.noAggregation, this.barRenderSharedContext, this.darkMode); + if (this.stateData && !this.comparisonEnabled) { + adjustTimeAxisExtentToData(this.timeSeriesChartOptions.xAxis[0], this.dataItems, + this.ctx.defaultSubscription.timeWindow.minTime, + this.ctx.defaultSubscription.timeWindow.maxTime); + } } - private updateAxes() { + private updateAxes(lazy = true) { const leftAxisList = this.yAxisList.filter(axis => axis.option.position === 'left'); - let res = this.updateYAxisOffset(leftAxisList); + let res = this.updateAxisOffset(leftAxisList); let leftOffset = res.offset + (!res.offset && this.settings.dataZoom ? 5 : 0); let changed = res.changed; const rightAxisList = this.yAxisList.filter(axis => axis.option.position === 'right'); - res = this.updateYAxisOffset(rightAxisList); + res = this.updateAxisOffset(rightAxisList); let rightOffset = res.offset + (!res.offset && this.settings.dataZoom ? 5 : 0); changed = changed || res.changed; + let bottomOffset = this.minBottomOffset(); const minTopOffset = this.minTopOffset(); - let topOffset = minTopOffset; - if (this.timeSeriesChartOptions.xAxis[0].show) { - const xAxisHeight = calculateXAxisHeight(this.timeSeriesChart); - if (this.timeSeriesChartOptions.xAxis[0].position === AxisPosition.bottom) { - bottomOffset += xAxisHeight; - } else { - topOffset = Math.max(minTopOffset, xAxisHeight); - } - if (this.settings.xAxis.label) { - const nameHeight = measureXAxisNameHeight(this.timeSeriesChart, this.timeSeriesChartOptions.xAxis[0].name); - if (this.timeSeriesChartOptions.xAxis[0].position === AxisPosition.bottom) { - bottomOffset += nameHeight; - } else { - topOffset = Math.max(minTopOffset, xAxisHeight + nameHeight); - } - const nameGap = xAxisHeight; - if (this.timeSeriesChartOptions.xAxis[0].nameGap !== nameGap) { - this.timeSeriesChartOptions.xAxis[0].nameGap = nameGap; - changed = true; - } - } - } + + const topAxisList = this.xAxisList.filter(axis => axis.option.position === 'top'); + res = this.updateAxisOffset(topAxisList); + const topOffset = Math.max(res.offset, minTopOffset); + changed = changed || res.changed; + + const bottomAxisList = this.xAxisList.filter(axis => axis.option.position === 'bottom'); + res = this.updateAxisOffset(bottomAxisList); + bottomOffset += res.offset; + changed = changed || res.changed; const thresholdsOffset = calculateThresholdsOffset(this.timeSeriesChart, this.thresholdItems, this.yAxisList); leftOffset = Math.max(leftOffset, thresholdsOffset[0]); @@ -615,7 +730,8 @@ export class TbTimeSeriesChart { } if (changed) { this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); - this.timeSeriesChart.setOption(this.timeSeriesChartOptions, {replaceMerge: ['yAxis', 'xAxis', 'grid'], lazyUpdate: true}); + this.timeSeriesChartOptions.xAxis = this.xAxisList.map(axis => axis.option); + this.timeSeriesChart.setOption(this.timeSeriesChartOptions, {replaceMerge: ['yAxis', 'xAxis', 'grid'], lazyUpdate: lazy}); } if (this.yAxisList.length) { const extent = getAxisExtent(this.timeSeriesChart, this.yAxisList[0].id); @@ -630,45 +746,45 @@ export class TbTimeSeriesChart { } } - private updateYAxisOffset(axisList: TimeSeriesChartYAxis[]): {offset: number; changed: boolean} { + private updateAxisOffset(axisList: TimeSeriesChartAxis[]): {offset: number; changed: boolean} { const result = {offset: 0, changed: false}; - let width = 0; - for (const yAxis of axisList) { - const newWidth = calculateYAxisWidth(this.timeSeriesChart, yAxis.id); - if (width && newWidth) { + let size = 0; + for (const axis of axisList) { + const newSize = calculateAxisSize(this.timeSeriesChart, axis.option.mainType, axis.id); + if (size && newSize) { result.offset += 5; } - width = newWidth; - const showLine = !!width && yAxis.settings.showLine; - if (yAxis.option.axisLine.show !== showLine) { - yAxis.option.axisLine.show = showLine; + size = newSize; + const showLine = !!size && axis.settings.showLine; + if (axis.option.axisLine.show !== showLine) { + axis.option.axisLine.show = showLine; result.changed = true; } - if (yAxis.option.offset !== result.offset) { - yAxis.option.offset = result.offset; + if (axis.option.offset !== result.offset) { + axis.option.offset = result.offset; result.changed = true; } - if (yAxis.settings.label) { - if (!width) { - if (yAxis.option.name) { - yAxis.option.name = null; + if (axis.settings.label) { + if (!size) { + if (axis.option.name) { + axis.option.name = null; result.changed = true; } } else { - if (!yAxis.option.name) { - yAxis.option.name = yAxis.settings.label; + if (!axis.option.name) { + axis.option.name = axis.settings.label; result.changed = true; } - const nameGap = width; - if (yAxis.option.nameGap !== nameGap) { - yAxis.option.nameGap = nameGap; + const nameGap = size; + if (axis.option.nameGap !== nameGap) { + axis.option.nameGap = nameGap; result.changed = true; } - const nameWidth = measureYAxisNameWidth(this.timeSeriesChart, yAxis.id, yAxis.settings.label); - result.offset += nameWidth; + const nameSize = measureAxisNameSize(this.timeSeriesChart, axis.option.mainType, axis.id, axis.settings.label); + result.offset += nameSize; } } - result.offset += width; + result.offset += size; } return result; } @@ -694,7 +810,7 @@ export class TbTimeSeriesChart { private minTopOffset(): number { const showTickLabels = !!this.yAxisList.find(yAxis => yAxis.settings.show && yAxis.settings.showTickLabels); - return (this.topPointLabels) ? 20 : + return (this.topPointLabels) ? 25 : (showTickLabels ? 10 : 5); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts index 14596b56a8..cc51bde975 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts @@ -18,11 +18,10 @@ /// import { - DataKey, + DataKey, DataKeySettingsWithComparison, Datasource, DatasourceData, FormattedData, - JsonSettingsSchema, LegendConfig } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; @@ -219,13 +218,7 @@ export interface TbFlotKeyThreshold { color: string; } -export interface TbFlotKeyComparisonSettings { - showValuesForComparison: boolean; - comparisonValuesLabel: string; - color: string; -} - -export interface TbFlotKeySettings { +export interface TbFlotKeySettings extends DataKeySettingsWithComparison { excludeFromStacking: boolean; hideDataByDefault: boolean; disableDataHiding: boolean; @@ -249,7 +242,6 @@ export interface TbFlotKeySettings { axisPosition: TbFlotYAxisPosition; axisTicksFormatter: string; thresholds: TbFlotKeyThreshold[]; - comparisonSettings: TbFlotKeyComparisonSettings; } export interface TbFlotLatestKeySettings { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.scss index a85d7d694b..4bef541ad2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.scss @@ -65,6 +65,8 @@ .tb-battery-level-box { display: flex; align-items: center; + min-height: 0; + max-height: 100%; .tb-battery-level-rectangle { width: 100%; height: 100%; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts index 5251a0c602..0cc6d266b0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts @@ -307,7 +307,7 @@ export class BatteryLevelWidgetComponent implements OnInit, OnDestroy, AfterView const newWidth = height * horizontalBatteryDimensions.shapeAspectRatio; this.renderer.setStyle(this.batteryLevelRectangle.nativeElement, 'width', newWidth + 'px'); } else { - this.renderer.setStyle(this.batteryLevelRectangle.nativeElement, 'width', null); + this.renderer.setStyle(this.batteryLevelRectangle.nativeElement, 'width', width + 'px'); } if (this.batteryLevelValue) { const ratios = horizontalBatteryDimensions.heightRatio; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html index b20b941bd1..4123df9c9f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html @@ -17,39 +17,102 @@ -->
-
widgets.bar-chart.bar-chart-card-style
-
- - {{ 'widgets.bar-chart.label-on-bar' | translate }} +
widgets.bar-chart.bar-chart-style
+
+ + {{ 'widgets.time-series-chart.data-zoom' | translate }} -
- - - - -
-
- - {{ 'widgets.bar-chart.value-on-bar' | translate }} - -
- - - - + + +
+
widgets.bar-chart.bar-appearance
+
+ + {{ 'widgets.bar-chart.label-on-bar' | translate }} + +
+ + + + +
+
+
+ + {{ 'widgets.bar-chart.value-on-bar' | translate }} + +
+ + + + +
+
+
+ + {{ 'widgets.time-series-chart.series.bar.show-border' | translate }} + +
+
+
widgets.time-series-chart.series.bar.border-width
+ + +
+
+
widgets.time-series-chart.series.bar.border-radius
+ + + +
+ + + + +
+
+
widgets.time-series-chart.axis.y-axis
+ + +
+
+
widgets.time-series-chart.axis.x-axis
+ +
+ +
@@ -149,10 +212,22 @@
-
-
{{ 'widgets.background.background' | translate }}
- - + + +
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
{{ 'widget-config.card-padding' | translate }}
+ + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts index b69b023d34..3c3703642c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts @@ -16,6 +16,7 @@ import { Component, Injector } from '@angular/core'; import { + Datasource, legendPositions, legendPositionTranslationMap, WidgetSettings, @@ -24,10 +25,10 @@ import { import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { formatValue } from '@core/utils'; +import { formatValue, mergeDeep } from '@core/utils'; import { DateFormatProcessor, DateFormatSettings } from '@shared/models/widget-settings.models'; import { - barChartWithLabelsDefaultSettings + barChartWithLabelsDefaultSettings, BarChartWithLabelsWidgetSettings } from '@home/components/widget/lib/chart/bar-chart-with-labels-widget.models'; @Component({ @@ -37,6 +38,15 @@ import { }) export class BarChartWithLabelsWidgetSettingsComponent extends WidgetSettingsComponent { + public get datasource(): Datasource { + const datasources: Datasource[] = this.widgetConfig.config.datasources; + if (datasources && datasources.length) { + return datasources[0]; + } else { + return null; + } + } + legendPositions = legendPositions; legendPositionTranslationMap = legendPositionTranslationMap; @@ -58,18 +68,34 @@ export class BarChartWithLabelsWidgetSettingsComponent extends WidgetSettingsCom } protected defaultSettings(): WidgetSettings { - return {...barChartWithLabelsDefaultSettings}; + return mergeDeep({} as BarChartWithLabelsWidgetSettings, barChartWithLabelsDefaultSettings); } protected onSettingsSet(settings: WidgetSettings) { this.barChartWidgetSettingsForm = this.fb.group({ + dataZoom: [settings.dataZoom, []], + showBarLabel: [settings.showBarLabel, []], barLabelFont: [settings.barLabelFont, []], barLabelColor: [settings.barLabelColor, []], showBarValue: [settings.showBarValue, []], barValueFont: [settings.barValueFont, []], barValueColor: [settings.barValueColor, []], + showBarBorder: [settings.showBarBorder, []], + barBorderWidth: [settings.barBorderWidth, []], + barBorderRadius: [settings.barBorderRadius, []], + barBackgroundSettings: [settings.barBackgroundSettings, []], + noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []], + + grid: [settings.grid, []], + + yAxis: [settings.yAxis, []], + xAxis: [settings.xAxis, []], + + thresholds: [settings.thresholds, []], + + animation: [settings.animation, []], showLegend: [settings.showLegend, []], legendPosition: [settings.legendPosition, []], @@ -88,17 +114,19 @@ export class BarChartWithLabelsWidgetSettingsComponent extends WidgetSettingsCom tooltipBackgroundColor: [settings.tooltipBackgroundColor, []], tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []], - background: [settings.background, []] + background: [settings.background, []], + padding: [settings.padding, []] }); } protected validatorTriggers(): string[] { - return ['showBarLabel', 'showBarValue', 'showLegend', 'showTooltip', 'tooltipShowDate']; + return ['showBarLabel', 'showBarValue', 'showBarBorder', 'showLegend', 'showTooltip', 'tooltipShowDate']; } protected updateValidators(emitEvent: boolean) { const showBarLabel: boolean = this.barChartWidgetSettingsForm.get('showBarLabel').value; const showBarValue: boolean = this.barChartWidgetSettingsForm.get('showBarValue').value; + const showBarBorder: boolean = this.barChartWidgetSettingsForm.get('showBarBorder').value; const showLegend: boolean = this.barChartWidgetSettingsForm.get('showLegend').value; const showTooltip: boolean = this.barChartWidgetSettingsForm.get('showTooltip').value; const tooltipShowDate: boolean = this.barChartWidgetSettingsForm.get('tooltipShowDate').value; @@ -119,6 +147,12 @@ export class BarChartWithLabelsWidgetSettingsComponent extends WidgetSettingsCom this.barChartWidgetSettingsForm.get('barValueColor').disable(); } + if (showBarBorder) { + this.barChartWidgetSettingsForm.get('barBorderWidth').enable(); + } else { + this.barChartWidgetSettingsForm.get('barBorderWidth').disable(); + } + if (showLegend) { this.barChartWidgetSettingsForm.get('legendPosition').enable(); this.barChartWidgetSettingsForm.get('legendLabelFont').enable(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html index cf5bbcfdd5..c5de2a86a7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html @@ -17,29 +17,178 @@ -->
-
widgets.range-chart.range-chart-card-style
+
widgets.range-chart.range-chart-style
{{ 'widgets.range-chart.data-zoom' | translate }}
-
-
{{ 'widgets.range-chart.range-colors' | translate }}
- - + + +
+
widgets.range-chart.range-chart-appearance
+
+
{{ 'widgets.range-chart.range-colors' | translate }}
+ + +
+
+
{{ 'widgets.range-chart.out-of-range-color' | translate }}
+ + +
+
+ + {{ 'widgets.range-chart.show-range-thresholds' | translate }} + + + +
+
+ + {{ 'widgets.range-chart.fill-area' | translate }} + +
+
+
widgets.range-chart.fill-area-opacity
+ + + +
+
+
widgets.time-series-chart.series.line.line
+
+ + {{ 'widgets.time-series-chart.series.line.show-line' | translate }} + +
+
+ + {{ 'widgets.time-series-chart.series.line.step-line' | translate }} + + + + + {{ lineSeriesStepTypeTranslations.get(stepType) | translate }} + + + +
+
+ + {{ 'widgets.time-series-chart.series.line.smooth-line' | translate }} + +
+
+
widgets.time-series-chart.line-type
+ + + + {{ timeSeriesLineTypeTranslations.get(lineType) | translate }} + + + +
+
+
widgets.time-series-chart.line-width
+ + + +
+
+
+
widgets.time-series-chart.series.point.points
+
+ + {{ 'widgets.time-series-chart.series.point.show-points' | translate }} + +
+
+ +
+ {{ 'widgets.time-series-chart.series.point.point-label' | translate }} +
+
+
+ + + + {{ seriesLabelPositionTranslations.get(position) | translate }} + + + + + + + +
+
+
+ + {{ 'widgets.time-series-chart.series.point.point-label-background' | translate }} + + + +
+
+
widgets.time-series-chart.series.point.point-shape
+ + + + {{ echartsShapeTranslations.get(shape) | translate }} + + + +
+
+
widgets.time-series-chart.series.point.point-size
+ + + +
+
-
-
{{ 'widgets.range-chart.out-of-range-color' | translate }}
- - +
+
widgets.time-series-chart.axis.y-axis
+ +
-
- - {{ 'widgets.range-chart.fill-area' | translate }} - +
+
widgets.time-series-chart.axis.x-axis
+ +
+ +
@@ -139,10 +288,22 @@
-
-
{{ 'widgets.background.background' | translate }}
- - + + +
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
{{ 'widget-config.card-padding' | translate }}
+ + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts index f0870dbd80..7e568ca7ab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts @@ -16,17 +16,30 @@ import { Component, Injector } from '@angular/core'; import { + Datasource, legendPositions, legendPositionTranslationMap, WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -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 { formatValue } from '@core/utils'; -import { rangeChartDefaultSettings } from '@home/components/widget/lib/chart/range-chart-widget.models'; +import { formatValue, mergeDeep } from '@core/utils'; +import { + rangeChartDefaultSettings, + RangeChartWidgetSettings +} from '@home/components/widget/lib/chart/range-chart-widget.models'; import { DateFormatProcessor, DateFormatSettings } from '@shared/models/widget-settings.models'; +import { + lineSeriesStepTypes, + lineSeriesStepTypeTranslations, + seriesLabelPositions, + seriesLabelPositionTranslations, + timeSeriesLineTypes, + timeSeriesLineTypeTranslations +} from '@home/components/widget/lib/chart/time-series-chart.models'; +import { echartsShapes, echartsShapeTranslations } from '@home/components/widget/lib/chart/echarts-widget.models'; @Component({ selector: 'tb-range-chart-widget-settings', @@ -35,12 +48,39 @@ import { DateFormatProcessor, DateFormatSettings } from '@shared/models/widget-s }) export class RangeChartWidgetSettingsComponent extends WidgetSettingsComponent { + public get datasource(): Datasource { + const datasources: Datasource[] = this.widgetConfig.config.datasources; + if (datasources && datasources.length) { + return datasources[0]; + } else { + return null; + } + } + + lineSeriesStepTypes = lineSeriesStepTypes; + + lineSeriesStepTypeTranslations = lineSeriesStepTypeTranslations; + + timeSeriesLineTypes = timeSeriesLineTypes; + + timeSeriesLineTypeTranslations = timeSeriesLineTypeTranslations; + + seriesLabelPositions = seriesLabelPositions; + + seriesLabelPositionTranslations = seriesLabelPositionTranslations; + + echartsShapes = echartsShapes; + + echartsShapeTranslations = echartsShapeTranslations; + legendPositions = legendPositions; legendPositionTranslationMap = legendPositionTranslationMap; rangeChartWidgetSettingsForm: UntypedFormGroup; + pointLabelPreviewFn = this._pointLabelPreviewFn.bind(this); + tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this); tooltipDatePreviewFn = this._tooltipDatePreviewFn.bind(this); @@ -56,7 +96,7 @@ export class RangeChartWidgetSettingsComponent extends WidgetSettingsComponent { } protected defaultSettings(): WidgetSettings { - return {...rangeChartDefaultSettings}; + return mergeDeep({} as RangeChartWidgetSettings, rangeChartDefaultSettings); } protected onSettingsSet(settings: WidgetSettings) { @@ -64,7 +104,36 @@ export class RangeChartWidgetSettingsComponent extends WidgetSettingsComponent { dataZoom: [settings.dataZoom, []], rangeColors: [settings.rangeColors, []], outOfRangeColor: [settings.outOfRangeColor, []], + showRangeThresholds: [settings.showRangeThresholds, []], + rangeThreshold: [settings.rangeThreshold, []], fillArea: [settings.fillArea, []], + fillAreaOpacity: [settings.fillAreaOpacity, [Validators.min(0), Validators.max(1)]], + + showLine: [settings.showLine, []], + step: [settings.step, []], + stepType: [settings.stepType, []], + smooth: [settings.smooth, []], + lineType: [settings.lineType, []], + lineWidth: [settings.lineWidth, [Validators.min(0)]], + + showPoints: [settings.showPoints, []], + showPointLabel: [settings.showPointLabel, []], + pointLabelPosition: [settings.pointLabelPosition, []], + pointLabelFont: [settings.pointLabelFont, []], + pointLabelColor: [settings.pointLabelColor, []], + enablePointLabelBackground: [settings.enablePointLabelBackground, []], + pointLabelBackground: [settings.pointLabelBackground, []], + pointShape: [settings.pointShape, []], + pointSize: [settings.pointSize, [Validators.min(0)]], + + grid: [settings.grid, []], + + yAxis: [settings.yAxis, []], + xAxis: [settings.xAxis, []], + + thresholds: [settings.thresholds, []], + + animation: [settings.animation, []], showLegend: [settings.showLegend, []], legendPosition: [settings.legendPosition, []], @@ -83,19 +152,75 @@ export class RangeChartWidgetSettingsComponent extends WidgetSettingsComponent { tooltipBackgroundColor: [settings.tooltipBackgroundColor, []], tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []], - background: [settings.background, []] + background: [settings.background, []], + padding: [settings.padding, []] }); } protected validatorTriggers(): string[] { - return ['showLegend', 'showTooltip', 'tooltipShowDate']; + return ['showRangeThresholds', 'fillArea', 'showLine', 'step', 'showPointLabel', 'enablePointLabelBackground', + 'showLegend', 'showTooltip', 'tooltipShowDate']; } protected updateValidators(emitEvent: boolean) { + const showRangeThresholds: boolean = this.rangeChartWidgetSettingsForm.get('showRangeThresholds').value; + const fillArea: boolean = this.rangeChartWidgetSettingsForm.get('fillArea').value; + const showLine: boolean = this.rangeChartWidgetSettingsForm.get('showLine').value; + const step: boolean = this.rangeChartWidgetSettingsForm.get('step').value; + const showPointLabel: boolean = this.rangeChartWidgetSettingsForm.get('showPointLabel').value; + const enablePointLabelBackground: boolean = this.rangeChartWidgetSettingsForm.get('enablePointLabelBackground').value; const showLegend: boolean = this.rangeChartWidgetSettingsForm.get('showLegend').value; const showTooltip: boolean = this.rangeChartWidgetSettingsForm.get('showTooltip').value; const tooltipShowDate: boolean = this.rangeChartWidgetSettingsForm.get('tooltipShowDate').value; + if (showRangeThresholds) { + this.rangeChartWidgetSettingsForm.get('rangeThreshold').enable(); + } else { + this.rangeChartWidgetSettingsForm.get('rangeThreshold').disable(); + } + + if (fillArea) { + this.rangeChartWidgetSettingsForm.get('fillAreaOpacity').enable(); + } else { + this.rangeChartWidgetSettingsForm.get('fillAreaOpacity').disable(); + } + + if (showLine) { + this.rangeChartWidgetSettingsForm.get('step').enable({emitEvent: false}); + if (step) { + this.rangeChartWidgetSettingsForm.get('stepType').enable(); + this.rangeChartWidgetSettingsForm.get('smooth').disable(); + } else { + this.rangeChartWidgetSettingsForm.get('stepType').disable(); + this.rangeChartWidgetSettingsForm.get('smooth').enable(); + } + this.rangeChartWidgetSettingsForm.get('lineType').enable(); + this.rangeChartWidgetSettingsForm.get('lineWidth').enable(); + } else { + this.rangeChartWidgetSettingsForm.get('step').disable({emitEvent: false}); + this.rangeChartWidgetSettingsForm.get('stepType').disable(); + this.rangeChartWidgetSettingsForm.get('smooth').disable(); + this.rangeChartWidgetSettingsForm.get('lineType').disable(); + this.rangeChartWidgetSettingsForm.get('lineWidth').disable(); + } + if (showPointLabel) { + this.rangeChartWidgetSettingsForm.get('pointLabelPosition').enable(); + this.rangeChartWidgetSettingsForm.get('pointLabelFont').enable(); + this.rangeChartWidgetSettingsForm.get('pointLabelColor').enable(); + this.rangeChartWidgetSettingsForm.get('enablePointLabelBackground').enable({emitEvent: false}); + if (enablePointLabelBackground) { + this.rangeChartWidgetSettingsForm.get('pointLabelBackground').enable(); + } else { + this.rangeChartWidgetSettingsForm.get('pointLabelBackground').disable(); + } + } else { + this.rangeChartWidgetSettingsForm.get('pointLabelPosition').disable(); + this.rangeChartWidgetSettingsForm.get('pointLabelFont').disable(); + this.rangeChartWidgetSettingsForm.get('pointLabelColor').disable(); + this.rangeChartWidgetSettingsForm.get('enablePointLabelBackground').disable({emitEvent: false}); + this.rangeChartWidgetSettingsForm.get('pointLabelBackground').disable(); + } + if (showLegend) { this.rangeChartWidgetSettingsForm.get('legendPosition').enable(); this.rangeChartWidgetSettingsForm.get('legendLabelFont').enable(); @@ -136,6 +261,12 @@ export class RangeChartWidgetSettingsComponent extends WidgetSettingsComponent { } } + private _pointLabelPreviewFn(): string { + const units: string = this.widgetConfig.config.units; + const decimals: number = this.widgetConfig.config.decimals; + return formatValue(22, decimals, units, false); + } + private _tooltipValuePreviewFn(): string { const units: string = this.widgetConfig.config.units; const decimals: number = this.widgetConfig.config.decimals; @@ -148,5 +279,4 @@ export class RangeChartWidgetSettingsComponent extends WidgetSettingsComponent { processor.update(Date.now()); return processor.formatted; } - } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-bar-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-bar-settings.component.html index 447784091e..ddbeaf2480 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-bar-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-bar-settings.component.html @@ -59,6 +59,15 @@
+
+ + {{ 'widgets.time-series-chart.series.bar.label-background' | translate }} + + + +
{ this.updateModel(); }); merge(this.barSettingsFormGroup.get('showBorder').valueChanges, - this.barSettingsFormGroup.get('showLabel').valueChanges) + this.barSettingsFormGroup.get('showLabel').valueChanges, + this.barSettingsFormGroup.get('enableLabelBackground').valueChanges) .subscribe(() => { this.updateValidators(); }); @@ -116,6 +119,7 @@ export class TimeSeriesChartBarSettingsComponent implements OnInit, ControlValue private updateValidators() { const showBorder: boolean = this.barSettingsFormGroup.get('showBorder').value; const showLabel: boolean = this.barSettingsFormGroup.get('showLabel').value; + const enableLabelBackground: boolean = this.barSettingsFormGroup.get('enableLabelBackground').value; if (showBorder) { this.barSettingsFormGroup.get('borderWidth').enable({emitEvent: false}); } else { @@ -125,10 +129,18 @@ export class TimeSeriesChartBarSettingsComponent implements OnInit, ControlValue this.barSettingsFormGroup.get('labelPosition').enable({emitEvent: false}); this.barSettingsFormGroup.get('labelFont').enable({emitEvent: false}); this.barSettingsFormGroup.get('labelColor').enable({emitEvent: false}); + this.barSettingsFormGroup.get('enableLabelBackground').enable({emitEvent: false}); + if (enableLabelBackground) { + this.barSettingsFormGroup.get('labelBackground').enable({emitEvent: false}); + } else { + this.barSettingsFormGroup.get('labelBackground').disable({emitEvent: false}); + } } else { this.barSettingsFormGroup.get('labelPosition').disable({emitEvent: false}); this.barSettingsFormGroup.get('labelFont').disable({emitEvent: false}); this.barSettingsFormGroup.get('labelColor').disable({emitEvent: false}); + this.barSettingsFormGroup.get('enableLabelBackground').disable({emitEvent: false}); + this.barSettingsFormGroup.get('labelBackground').disable({emitEvent: false}); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html index 5f0fbb9e37..a265a1b431 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html @@ -56,6 +56,45 @@ formControlName="barSettings">
+
+
widgets.chart.tooltip-settings
+ + +
+
+
widgets.time-series-chart.comparison.settings
+ + + + + {{ 'widgets.time-series-chart.comparison.show-values-for-comparison' | translate }} + + + + +
+
widgets.time-series-chart.comparison.comparison-values-label
+ + + +
+
+
{{ 'widgets.time-series-chart.comparison.comparison-data-color' | translate }}
+ + +
+
+
+
{{ timeSeriesChartTypeTranslations.get(chartType) | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts index d1ac428396..229607d7f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts @@ -29,6 +29,7 @@ import { } from '@home/components/widget/lib/chart/time-series-chart.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { TimeSeriesChartWidgetSettings } from '@home/components/widget/lib/chart/time-series-chart-widget.models'; +import { WidgetService } from '@core/http/widget.service'; @Component({ selector: 'tb-time-series-chart-key-settings', @@ -53,7 +54,12 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent yAxisIds: TimeSeriesChartYAxisId[]; + comparisonEnabled: boolean; + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + constructor(protected store: Store, + private widgetService: WidgetService, private fb: UntypedFormBuilder) { super(store); } @@ -69,6 +75,7 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent } const widgetSettings = (widgetConfig.config?.settings || {}) as TimeSeriesChartWidgetSettings; this.yAxisIds = widgetSettings.yAxes ? Object.keys(widgetSettings.yAxes) : ['default']; + this.comparisonEnabled = !!widgetSettings.comparisonEnabled; } protected defaultSettings(): WidgetSettings { @@ -88,17 +95,25 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent dataHiddenByDefault: [seriesSettings.dataHiddenByDefault, []], type: [seriesSettings.type, []], lineSettings: [seriesSettings.lineSettings, []], - barSettings: [seriesSettings.barSettings, []] + barSettings: [seriesSettings.barSettings, []], + tooltipValueFormatter: [seriesSettings.tooltipValueFormatter, []], + comparisonSettings: this.fb.group({ + showValuesForComparison: [seriesSettings.comparisonSettings?.showValuesForComparison, []], + comparisonValuesLabel: [seriesSettings.comparisonSettings?.comparisonValuesLabel, []], + color: [seriesSettings.comparisonSettings?.color, []] + }) }); } protected validatorTriggers(): string[] { - return ['showInLegend', 'type']; + return ['showInLegend', 'type', 'comparisonSettings.showValuesForComparison']; } protected updateValidators(_emitEvent: boolean) { const showInLegend: boolean = this.timeSeriesChartKeySettingsForm.get('showInLegend').value; const type: TimeSeriesChartSeriesType = this.timeSeriesChartKeySettingsForm.get('type').value; + const showValuesForComparison: boolean = + this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('showValuesForComparison').value; if (showInLegend) { this.timeSeriesChartKeySettingsForm.get('dataHiddenByDefault').enable(); } else { @@ -112,5 +127,17 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent this.timeSeriesChartKeySettingsForm.get('lineSettings').disable(); this.timeSeriesChartKeySettingsForm.get('barSettings').enable(); } + if (this.comparisonEnabled) { + this.timeSeriesChartKeySettingsForm.get('comparisonSettings').enable({emitEvent: false}); + if (showValuesForComparison) { + this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('comparisonValuesLabel').enable(); + this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('color').enable(); + } else { + this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('comparisonValuesLabel').disable(); + this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('color').disable(); + } + } else { + this.timeSeriesChartKeySettingsForm.get('comparisonSettings').disable({emitEvent: false}); + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.html index 78f1129727..f20f1323b3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.html @@ -18,7 +18,7 @@
widgets.time-series-chart.series.line.line
-
+
{{ 'widgets.time-series-chart.series.line.show-line' | translate }} @@ -35,7 +35,7 @@
-
+
{{ 'widgets.time-series-chart.series.line.smooth-line' | translate }} @@ -97,12 +97,21 @@
+
+ + {{ 'widgets.time-series-chart.series.point.point-label-background' | translate }} + + + +
widgets.time-series-chart.series.point.point-shape
- - {{ timeSeriesChartShapeTranslations.get(shape) | translate }} + + {{ echartsShapeTranslations.get(shape) | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.ts index 84533b26e4..1d57220b4e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.ts @@ -28,11 +28,11 @@ import { lineSeriesStepTypeTranslations, seriesLabelPositions, seriesLabelPositionTranslations, - timeSeriesChartShapes, - timeSeriesChartShapeTranslations, TimeSeriesChartType, + TimeSeriesChartType, timeSeriesLineTypes, timeSeriesLineTypeTranslations } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { echartsShapes, echartsShapeTranslations } from '@home/components/widget/lib/chart/echarts-widget.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { merge } from 'rxjs'; @@ -67,9 +67,9 @@ export class TimeSeriesChartLineSettingsComponent implements OnInit, ControlValu seriesLabelPositionTranslations = seriesLabelPositionTranslations; - timeSeriesChartShapes = timeSeriesChartShapes; + echartsShapes = echartsShapes; - timeSeriesChartShapeTranslations = timeSeriesChartShapeTranslations; + echartsShapeTranslations = echartsShapeTranslations; pointLabelPreviewFn = this._pointLabelPreviewFn.bind(this); @@ -103,6 +103,8 @@ export class TimeSeriesChartLineSettingsComponent implements OnInit, ControlValu pointLabelPosition: [null, []], pointLabelFont: [null, []], pointLabelColor: [null, []], + enablePointLabelBackground: [null, []], + pointLabelBackground: [null, []], pointShape: [null, []], pointSize: [null, [Validators.min(0)]], fillAreaSettings: [null, []] @@ -112,7 +114,8 @@ export class TimeSeriesChartLineSettingsComponent implements OnInit, ControlValu }); merge(this.lineSettingsFormGroup.get('showLine').valueChanges, this.lineSettingsFormGroup.get('step').valueChanges, - this.lineSettingsFormGroup.get('showPointLabel').valueChanges) + this.lineSettingsFormGroup.get('showPointLabel').valueChanges, + this.lineSettingsFormGroup.get('enablePointLabelBackground').valueChanges) .subscribe(() => { this.updateValidators(); }); @@ -144,11 +147,22 @@ export class TimeSeriesChartLineSettingsComponent implements OnInit, ControlValu } private updateValidators() { + const state = this.chartType === TimeSeriesChartType.state; const showLine: boolean = this.lineSettingsFormGroup.get('showLine').value; const step: boolean = this.lineSettingsFormGroup.get('step').value; const showPointLabel: boolean = this.lineSettingsFormGroup.get('showPointLabel').value; + const enablePointLabelBackground: boolean = this.lineSettingsFormGroup.get('enablePointLabelBackground').value; + if (state) { + this.lineSettingsFormGroup.get('showLine').disable({emitEvent: false}); + } else { + this.lineSettingsFormGroup.get('showLine').enable({emitEvent: false}); + } if (showLine) { - this.lineSettingsFormGroup.get('step').enable({emitEvent: false}); + if (state) { + this.lineSettingsFormGroup.get('step').disable({emitEvent: false}); + } else { + this.lineSettingsFormGroup.get('step').enable({emitEvent: false}); + } if (step) { this.lineSettingsFormGroup.get('stepType').enable({emitEvent: false}); this.lineSettingsFormGroup.get('smooth').disable({emitEvent: false}); @@ -169,10 +183,18 @@ export class TimeSeriesChartLineSettingsComponent implements OnInit, ControlValu this.lineSettingsFormGroup.get('pointLabelPosition').enable({emitEvent: false}); this.lineSettingsFormGroup.get('pointLabelFont').enable({emitEvent: false}); this.lineSettingsFormGroup.get('pointLabelColor').enable({emitEvent: false}); + this.lineSettingsFormGroup.get('enablePointLabelBackground').enable({emitEvent: false}); + if (enablePointLabelBackground) { + this.lineSettingsFormGroup.get('pointLabelBackground').enable({emitEvent: false}); + } else { + this.lineSettingsFormGroup.get('pointLabelBackground').disable({emitEvent: false}); + } } else { this.lineSettingsFormGroup.get('pointLabelPosition').disable({emitEvent: false}); this.lineSettingsFormGroup.get('pointLabelFont').disable({emitEvent: false}); this.lineSettingsFormGroup.get('pointLabelColor').disable({emitEvent: false}); + this.lineSettingsFormGroup.get('enablePointLabelBackground').disable({emitEvent: false}); + this.lineSettingsFormGroup.get('pointLabelBackground').disable({emitEvent: false}); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html index fbd960be18..6134f3dcae 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html @@ -16,6 +16,50 @@ --> +
+
+ + {{ 'widgets.time-series-chart.comparison.comparison' | translate }} + +
+ + + + {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} + + + {{ 'widgets.chart.time-for-comparison-days' | translate }} + + + {{ 'widgets.chart.time-for-comparison-weeks' | translate }} + + + {{ 'widgets.chart.time-for-comparison-months' | translate }} + + + {{ 'widgets.chart.time-for-comparison-years' | translate }} + + + {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} + + + + + + + + +
+
+
+ +
+ +
@@ -118,6 +165,13 @@
+ +
{{ 'tooltip.date' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts index 6edab7ecf2..2b95d57b7d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts @@ -23,7 +23,7 @@ import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -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 { formatValue, isDefinedAndNotNull, mergeDeep } from '@core/utils'; @@ -35,10 +35,12 @@ import { } from '@home/components/widget/lib/chart/time-series-chart-widget.models'; import { TimeSeriesChartKeySettings, - TimeSeriesChartType, TimeSeriesChartYAxes, + TimeSeriesChartType, + TimeSeriesChartYAxes, TimeSeriesChartYAxisId } from '@home/components/widget/lib/chart/time-series-chart.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { WidgetService } from '@core/http/widget.service'; @Component({ selector: 'tb-time-series-chart-widget-settings', @@ -77,8 +79,11 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon chartType: TimeSeriesChartType = TimeSeriesChartType.default; + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + constructor(protected store: Store, private $injector: Injector, + private widgetService: WidgetService, private fb: UntypedFormBuilder) { super(store); } @@ -104,18 +109,25 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon } protected defaultSettings(): WidgetSettings { - return mergeDeep({} as TimeSeriesChartWidgetSettings, timeSeriesChartWidgetDefaultSettings); + return mergeDeep({} as TimeSeriesChartWidgetSettings, timeSeriesChartWidgetDefaultSettings); } protected onSettingsSet(settings: WidgetSettings) { this.timeSeriesChartWidgetSettingsForm = this.fb.group({ + comparisonEnabled: [settings.comparisonEnabled, []], + timeForComparison: [settings.timeForComparison, []], + comparisonCustomIntervalValue: [settings.comparisonCustomIntervalValue, [Validators.min(0)]], + comparisonXAxis: [settings.comparisonXAxis, []], + yAxes: [settings.yAxes, []], thresholds: [settings.thresholds, []], dataZoom: [settings.dataZoom, []], stack: [settings.stack, []], + grid: [settings.grid, []], + xAxis: [settings.xAxis, []], noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []], @@ -129,6 +141,7 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon tooltipTrigger: [settings.tooltipTrigger, []], tooltipValueFont: [settings.tooltipValueFont, []], tooltipValueColor: [settings.tooltipValueColor, []], + tooltipValueFormatter: [settings.tooltipValueFormatter, []], tooltipShowDate: [settings.tooltipShowDate, []], tooltipDateFormat: [settings.tooltipDateFormat, []], tooltipDateFont: [settings.tooltipDateFont, []], @@ -143,17 +156,31 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon background: [settings.background, []], padding: [settings.padding, []] }); + if (this.chartType === TimeSeriesChartType.state) { + this.timeSeriesChartWidgetSettingsForm.addControl('states', this.fb.control(settings.states, [])); + } } protected validatorTriggers(): string[] { - return ['showLegend', 'showTooltip', 'tooltipShowDate']; + return ['comparisonEnabled', 'showLegend', 'showTooltip', 'tooltipShowDate']; } protected updateValidators(emitEvent: boolean) { + const comparisonEnabled: boolean = this.timeSeriesChartWidgetSettingsForm.get('comparisonEnabled').value; const showLegend: boolean = this.timeSeriesChartWidgetSettingsForm.get('showLegend').value; const showTooltip: boolean = this.timeSeriesChartWidgetSettingsForm.get('showTooltip').value; const tooltipShowDate: boolean = this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').value; + if (comparisonEnabled) { + this.timeSeriesChartWidgetSettingsForm.get('timeForComparison').enable(); + this.timeSeriesChartWidgetSettingsForm.get('comparisonCustomIntervalValue').enable(); + this.timeSeriesChartWidgetSettingsForm.get('comparisonXAxis').enable(); + } else { + this.timeSeriesChartWidgetSettingsForm.get('timeForComparison').disable(); + this.timeSeriesChartWidgetSettingsForm.get('comparisonCustomIntervalValue').disable(); + this.timeSeriesChartWidgetSettingsForm.get('comparisonXAxis').disable(); + } + if (showLegend) { this.timeSeriesChartWidgetSettingsForm.get('legendLabelFont').enable(); this.timeSeriesChartWidgetSettingsForm.get('legendLabelColor').enable(); @@ -168,6 +195,7 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon this.timeSeriesChartWidgetSettingsForm.get('tooltipTrigger').enable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipValueFont').enable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipValueColor').enable(); + this.timeSeriesChartWidgetSettingsForm.get('tooltipValueFormatter').enable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').enable({emitEvent: false}); this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundColor').enable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundBlur').enable(); @@ -185,6 +213,7 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon } else { this.timeSeriesChartWidgetSettingsForm.get('tooltipValueFont').disable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipValueColor').disable(); + this.timeSeriesChartWidgetSettingsForm.get('tooltipValueFormatter').disable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').disable({emitEvent: false}); this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFormat').disable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFont').disable(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.html new file mode 100644 index 0000000000..cd7a26441a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.html @@ -0,0 +1,28 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.ts new file mode 100644 index 0000000000..7072d7051f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.ts @@ -0,0 +1,108 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { TimeSeriesChartAxisSettings } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { + TimeSeriesChartAxisSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component'; + +@Component({ + selector: 'tb-time-series-chart-axis-settings-button', + templateUrl: './time-series-chart-axis-settings-button.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartAxisSettingsButtonComponent), + multi: true + } + ] +}) +export class TimeSeriesChartAxisSettingsButtonComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + @Input() + axisType: 'xAxis' | 'yAxis' = 'xAxis'; + + @Input() + panelTitle: string; + + @Input() + @coerceBoolean() + advanced = false; + + private modelValue: TimeSeriesChartAxisSettings; + + private propagateChange = null; + + constructor(private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef) {} + + ngOnInit(): void { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: TimeSeriesChartAxisSettings): void { + this.modelValue = value; + } + + openAxisSettingsPopup($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + axisSettings: this.modelValue, + axisType: this.axisType, + panelTitle: this.panelTitle, + advanced: this.advanced + }; + const axisSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, TimeSeriesChartAxisSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, + ctx, + {}, + {}, {}, true); + axisSettingsPanelPopover.tbComponentRef.instance.popover = axisSettingsPanelPopover; + axisSettingsPanelPopover.tbComponentRef.instance.axisSettingsApplied.subscribe((axisSettings) => { + axisSettingsPanelPopover.hide(); + this.modelValue = axisSettings; + this.propagateChange(this.modelValue); + }); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html similarity index 69% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html index 759074b4be..dbc74d9ed1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html @@ -15,17 +15,17 @@ limitations under the License. --> -
-
{{ 'widgets.time-series-chart.axis.y-axis-settings' | translate }}
-
+
+
{{ panelTitle }}
+
+ [axisType]="axisType">
-
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.scss similarity index 89% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.scss index 095c38b7f7..853057c24e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.scss @@ -15,7 +15,7 @@ */ @import '../../../../../../../../../scss/constants'; -.tb-y-axis-settings-panel { +.tb-axis-settings-panel { width: 530px; display: flex; flex-direction: column; @@ -23,7 +23,7 @@ @media #{$mat-lt-md} { width: 90vw; } - .tb-y-axis-settings-panel-content { + .tb-axis-settings-panel-content { display: flex; flex-direction: column; gap: 16px; @@ -31,14 +31,14 @@ margin: -10px; padding: 10px; } - .tb-y-axis-settings-title { + .tb-axis-settings-title { font-size: 16px; font-weight: 500; line-height: 24px; letter-spacing: 0.25px; color: rgba(0, 0, 0, 0.87); } - .tb-y-axis-settings-panel-buttons { + .tb-axis-settings-panel-buttons { height: 40px; display: flex; flex-direction: row; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.ts similarity index 56% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.ts index 8046aafd29..ebfbcf84a1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.ts @@ -17,40 +17,49 @@ import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { TimeSeriesChartYAxisSettings } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { + TimeSeriesChartAxisSettings, + TimeSeriesChartYAxisSettings +} from '@home/components/widget/lib/chart/time-series-chart.models'; import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ - selector: 'tb-time-series-chart-y-axis-settings-panel', - templateUrl: './time-series-chart-y-axis-settings-panel.component.html', + selector: 'tb-time-series-chart-axis-settings-panel', + templateUrl: './time-series-chart-axis-settings-panel.component.html', providers: [], - styleUrls: ['./time-series-chart-y-axis-settings-panel.component.scss'], + styleUrls: ['./time-series-chart-axis-settings-panel.component.scss'], encapsulation: ViewEncapsulation.None }) -export class TimeSeriesChartYAxisSettingsPanelComponent implements OnInit { +export class TimeSeriesChartAxisSettingsPanelComponent implements OnInit { @Input() - yAxisSettings: TimeSeriesChartYAxisSettings; + axisType: 'xAxis' | 'yAxis' = 'xAxis'; + + @Input() + panelTitle: string; + + @Input() + axisSettings: TimeSeriesChartAxisSettings; @Input() @coerceBoolean() advanced = false; @Input() - popover: TbPopoverComponent; + popover: TbPopoverComponent; @Output() - yAxisSettingsApplied = new EventEmitter(); + axisSettingsApplied = new EventEmitter(); - yAxisSettingsFormGroup: UntypedFormGroup; + axisSettingsFormGroup: UntypedFormGroup; constructor(private fb: UntypedFormBuilder) { } ngOnInit(): void { - this.yAxisSettingsFormGroup = this.fb.group( + this.axisSettingsFormGroup = this.fb.group( { - yAxis: [this.yAxisSettings, []] + axis: [this.axisSettings, []] } ); } @@ -59,8 +68,8 @@ export class TimeSeriesChartYAxisSettingsPanelComponent implements OnInit { this.popover?.hide(); } - applyYAxisSettings() { - const yAxisSettings = this.yAxisSettingsFormGroup.get('yAxis').getRawValue(); - this.yAxisSettingsApplied.emit(yAxisSettings); + applyAxisSettings() { + const axisSettings = this.axisSettingsFormGroup.get('axis').getRawValue(); + this.axisSettingsApplied.emit(axisSettings); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html index 1d4bb95f5c..cd240ba8e4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html @@ -56,18 +56,27 @@
-
+
widget-config.units-short
-
+
widget-config.decimals-short
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts index 8dafc7050f..91fe61caa3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts @@ -70,6 +70,14 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu @coerceBoolean() advanced = false; + @Input() + @coerceBoolean() + hideUnits = false; + + @Input() + @coerceBoolean() + hideDecimals = false; + private modelValue: TimeSeriesChartXAxisSettings | TimeSeriesChartYAxisSettings; private propagateChange = null; @@ -77,7 +85,7 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu public axisSettingsFormGroup: UntypedFormGroup; constructor(private fb: UntypedFormBuilder, - private widgetService: WidgetService,) { + private widgetService: WidgetService) { } ngOnInit(): void { @@ -105,6 +113,7 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu this.axisSettingsFormGroup.addControl('units', this.fb.control(null, [])); this.axisSettingsFormGroup.addControl('decimals', this.fb.control(null, [Validators.min(0)])); this.axisSettingsFormGroup.addControl('ticksFormatter', this.fb.control(null, [])); + this.axisSettingsFormGroup.addControl('ticksGenerator', this.fb.control(null, [])); this.axisSettingsFormGroup.addControl('interval', this.fb.control(null, [Validators.min(0)])); this.axisSettingsFormGroup.addControl('splitNumber', this.fb.control(null, [Validators.min(1)])); this.axisSettingsFormGroup.addControl('min', this.fb.control(null, [])); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-fill-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-fill-settings.component.html similarity index 96% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-fill-settings.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-fill-settings.component.html index 934f985483..99e3c1be8f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-fill-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-fill-settings.component.html @@ -27,8 +27,8 @@
widgets.time-series-chart.series.opacity
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-fill-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-fill-settings.component.ts similarity index 97% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-fill-settings.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-fill-settings.component.ts index 44cc870f5b..dd7f4dd7f6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-fill-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-fill-settings.component.ts @@ -34,7 +34,7 @@ import { AppState } from '@core/core.state'; @Component({ selector: 'tb-time-series-chart-fill-settings', templateUrl: './time-series-chart-fill-settings.component.html', - styleUrls: ['./../widget-settings.scss'], + styleUrls: ['./../../widget-settings.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, @@ -73,7 +73,7 @@ export class TimeSeriesChartFillSettingsComponent implements OnInit, ControlValu ngOnInit(): void { this.fillSettingsFormGroup = this.fb.group({ type: [null, []], - opacity: [null, [Validators.min(0), Validators.max(100)]], + opacity: [null, [Validators.min(0), Validators.max(1)]], gradient: this.fb.group({ start: [null, [Validators.min(0), Validators.max(100)]], end: [null, [Validators.min(0), Validators.max(100)]] diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component.html new file mode 100644 index 0000000000..1b439e5bea --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component.html @@ -0,0 +1,54 @@ + + +
+ + + + + {{ 'widgets.time-series-chart.grid.grid' | translate }} + + + + +
+
{{ 'widgets.time-series-chart.grid.background-color' | translate }}
+ + +
+
+
{{ 'widgets.time-series-chart.grid.border' | translate }}
+
+ + + px + + + +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component.ts new file mode 100644 index 0000000000..fd88ce2a8f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component.ts @@ -0,0 +1,115 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { TimeSeriesChartGridSettings } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { WidgetService } from '@core/http/widget.service'; + +@Component({ + selector: 'tb-time-series-chart-grid-settings', + templateUrl: './time-series-chart-grid-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartGridSettingsComponent), + multi: true + } + ] +}) +export class TimeSeriesChartGridSettingsComponent implements OnInit, ControlValueAccessor { + + settingsExpanded = false; + + @Input() + disabled: boolean; + + private modelValue: TimeSeriesChartGridSettings; + + private propagateChange = null; + + public gridSettingsFormGroup: UntypedFormGroup; + + constructor(private fb: UntypedFormBuilder, + private widgetService: WidgetService,) { + } + + ngOnInit(): void { + this.gridSettingsFormGroup = this.fb.group({ + show: [null, []], + backgroundColor: [null, []], + borderWidth: [null, [Validators.min(0)]], + borderColor: [null, []], + }); + this.gridSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.gridSettingsFormGroup.get('show').valueChanges + .subscribe(() => { + this.updateValidators(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.gridSettingsFormGroup.disable({emitEvent: false}); + } else { + this.gridSettingsFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: TimeSeriesChartGridSettings): void { + this.modelValue = value; + this.gridSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(); + this.gridSettingsFormGroup.get('show').valueChanges.subscribe((show) => { + this.settingsExpanded = show; + }); + } + + private updateValidators() { + const show: boolean = this.gridSettingsFormGroup.get('show').value; + if (show) { + this.gridSettingsFormGroup.enable({emitEvent: false}); + } else { + this.gridSettingsFormGroup.disable({emitEvent: false}); + this.gridSettingsFormGroup.get('show').enable({emitEvent: false}); + } + } + + private updateModel() { + this.modelValue = this.gridSettingsFormGroup.getRawValue(); + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.html new file mode 100644 index 0000000000..a2e11c3a8e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.html @@ -0,0 +1,62 @@ + +
+ + + + + + + + + + {{ timeSeriesStateSourceTypeTranslations.get(type) | translate }} + + + +
+ + + + + + + + +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.scss new file mode 100644 index 0000000000..247eb5d28f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.scss @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import '../../../../../../../../../scss/constants'; + +.tb-form-table-row.tb-time-series-state-row { + @media #{$mat-lt-md} { + align-items: flex-start; + } + .tb-state-label-field { + flex: 1; + min-width: 70px; + } + .tb-state-value-field { + width: 80px; + min-width: 80px; + } + .tb-state-source-field { + width: 100px; + min-width: 100px; + } + .tb-state-source-value-field { + flex: 1; + min-width: 200px; + display: flex; + flex-direction: column; + gap: 8px; + @media #{$mat-gt-sm} { + min-width: 358px; + flex-direction: row; + align-items: center; + gap: 12px; + } + .tb-inline-field { + flex: 1; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.ts new file mode 100644 index 0000000000..97c87432c2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.ts @@ -0,0 +1,138 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { + TimeSeriesChartStateSettings, + TimeSeriesChartStateSourceType, + timeSeriesStateSourceTypes, + timeSeriesStateSourceTypeTranslations +} from '@home/components/widget/lib/chart/time-series-chart.models'; + +@Component({ + selector: 'tb-time-series-chart-state-row', + templateUrl: './time-series-chart-state-row.component.html', + styleUrls: ['./time-series-chart-state-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartStateRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class TimeSeriesChartStateRowComponent implements ControlValueAccessor, OnInit { + + TimeSeriesChartStateSourceType = TimeSeriesChartStateSourceType; + + timeSeriesStateSourceTypes = timeSeriesStateSourceTypes; + + timeSeriesStateSourceTypeTranslations = timeSeriesStateSourceTypeTranslations; + + @Input() + disabled: boolean; + + @Output() + stateRemoved = new EventEmitter(); + + stateFormGroup: UntypedFormGroup; + + modelValue: TimeSeriesChartStateSettings; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef) { + } + + ngOnInit() { + this.stateFormGroup = this.fb.group({ + label: [null, []], + value: [null, [Validators.required]], + sourceType: [null, [Validators.required]], + sourceValue: [null, [Validators.required]], + sourceRangeFrom: [null, []], + sourceRangeTo: [null, []] + }); + this.stateFormGroup.valueChanges.subscribe( + () => this.updateModel() + ); + this.stateFormGroup.get('sourceType').valueChanges.subscribe(() => { + this.updateValidators(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.stateFormGroup.disable({emitEvent: false}); + } else { + this.stateFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: TimeSeriesChartStateSettings): void { + this.modelValue = value; + this.stateFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(); + this.cd.markForCheck(); + } + + private updateValidators() { + const sourceType: TimeSeriesChartStateSourceType = this.stateFormGroup.get('sourceType').value; + if (sourceType === TimeSeriesChartStateSourceType.constant) { + this.stateFormGroup.get('sourceValue').enable({emitEvent: false}); + this.stateFormGroup.get('sourceRangeFrom').disable({emitEvent: false}); + this.stateFormGroup.get('sourceRangeTo').disable({emitEvent: false}); + } else if (sourceType === TimeSeriesChartStateSourceType.range) { + this.stateFormGroup.get('sourceValue').disable({emitEvent: false}); + this.stateFormGroup.get('sourceRangeFrom').enable({emitEvent: false}); + this.stateFormGroup.get('sourceRangeTo').enable({emitEvent: false}); + } + } + + private updateModel() { + this.modelValue = this.stateFormGroup.value; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.html new file mode 100644 index 0000000000..5db76d1493 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.html @@ -0,0 +1,47 @@ + +
+
{{ 'widgets.time-series-chart.state.states' | translate }}
+
+
+
widgets.time-series-chart.state.label
+
widgets.time-series-chart.state.ticks-value
+
widgets.time-series-chart.state.source
+
widgets.time-series-chart.state.value-range
+
+
+
+
+ + + +
+
+
+
+ +
+
+ + {{ 'widgets.time-series-chart.state.no-states' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.scss new file mode 100644 index 0000000000..3177b8cdc1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.scss @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import '../../../../../../../../../scss/constants'; + +.tb-time-series-states-panel { + .tb-form-table { + overflow-x: auto; + } + .tb-form-table-header-cell { + &.tb-state-label-header { + flex: 1; + min-width: 80px; + } + &.tb-state-value-header { + width: 80px; + min-width: 80px; + } + &.tb-state-source-header { + width: 100px; + min-width: 100px; + } + &.tb-state-source-value-header { + flex: 1; + min-width: 200px; + @media #{$mat-gt-sm} { + min-width: 358px; + } + } + &.tb-actions-header { + width: 40px; + min-width: 40px; + } + } + .tb-form-table-header { + min-width: fit-content; + } + .tb-form-table-body { + min-width: fit-content; + .mat-divider { + margin-top: 8px; + @media #{$mat-gt-sm} { + display: none; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.ts new file mode 100644 index 0000000000..8c30582425 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.ts @@ -0,0 +1,144 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { + TimeSeriesChartStateSettings, + TimeSeriesChartStateSourceType, + timeSeriesChartStateValid, + timeSeriesChartStateValidator +} from '@home/components/widget/lib/chart/time-series-chart.models'; + +@Component({ + selector: 'tb-time-series-chart-states-panel', + templateUrl: './time-series-chart-states-panel.component.html', + styleUrls: ['./time-series-chart-states-panel.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartStatesPanelComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TimeSeriesChartStatesPanelComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class TimeSeriesChartStatesPanelComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + disabled: boolean; + + statesFormGroup: UntypedFormGroup; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder) { + } + + ngOnInit() { + this.statesFormGroup = this.fb.group({ + states: [this.fb.array([]), []] + }); + this.statesFormGroup.valueChanges.subscribe( + () => { + let states: TimeSeriesChartStateSettings[] = this.statesFormGroup.get('states').value; + if (states) { + states = states.filter(s => timeSeriesChartStateValid(s)); + } + this.propagateChange(states); + } + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.statesFormGroup.disable({emitEvent: false}); + } else { + this.statesFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: TimeSeriesChartStateSettings[] | undefined): void { + const states = value || []; + this.statesFormGroup.setControl('states', this.prepareStatesFormArray(states), {emitEvent: false}); + } + + public validate(c: UntypedFormControl) { + const valid = this.statesFormGroup.valid; + return valid ? null : { + states: { + valid: false, + }, + }; + } + + statesFormArray(): UntypedFormArray { + return this.statesFormGroup.get('states') as UntypedFormArray; + } + + trackByState(index: number, stateControl: AbstractControl): any { + return stateControl; + } + + removeState(index: number) { + (this.statesFormGroup.get('states') as UntypedFormArray).removeAt(index); + } + + addState() { + const state: TimeSeriesChartStateSettings = { + label: '', + value: 0, + sourceType: TimeSeriesChartStateSourceType.constant + }; + const statesArray = this.statesFormGroup.get('states') as UntypedFormArray; + const stateControl = this.fb.control(state, [timeSeriesChartStateValidator]); + statesArray.push(stateControl); + } + + private prepareStatesFormArray(states: TimeSeriesChartStateSettings[] | undefined): UntypedFormArray { + const statesControls: Array = []; + if (states) { + states.forEach((state) => { + statesControls.push(this.fb.control(state, [timeSeriesChartStateValidator])); + }); + } + return this.fb.array(statesControls); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html index 7e2c9f74c2..d0ea8f0ef3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html @@ -73,7 +73,7 @@ [formControl]="entityKeyFormControl">
-
+
@@ -98,14 +98,13 @@
- + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings.component.ts new file mode 100644 index 0000000000..6908b086af --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings.component.ts @@ -0,0 +1,124 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { deepClone } from '@core/utils'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { WidgetConfig } from '@shared/models/widget.models'; +import { + TimeSeriesChartThreshold, + TimeSeriesChartYAxisId +} from '@home/components/widget/lib/chart/time-series-chart.models'; +import { + TimeSeriesChartThresholdSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component'; + +@Component({ + selector: 'tb-time-series-chart-threshold-settings', + templateUrl: './time-series-chart-threshold-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartThresholdSettingsComponent), + multi: true + } + ] +}) +export class TimeSeriesChartThresholdSettingsComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + @Input() + widgetConfig: WidgetConfig; + + @Input() + yAxisIds: TimeSeriesChartYAxisId[]; + + @Input() + @coerceBoolean() + hideYAxis = false; + + @Input() + @coerceBoolean() + boxButton = false; + + @Input() + icon = 'settings'; + + @Input() + title = 'widgets.time-series-chart.threshold.threshold-settings'; + + private modelValue: Partial; + + private propagateChange = null; + + constructor(private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef) {} + + ngOnInit(): void { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: Partial): void { + this.modelValue = value; + } + + openThresholdSettingsPopup($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + thresholdSettings: deepClone(this.modelValue), + panelTitle: this.title, + widgetConfig: this.widgetConfig, + hideYAxis: this.hideYAxis, + yAxisIds: this.yAxisIds + }; + const thresholdSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, TimeSeriesChartThresholdSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, + ctx, + {}, + {}, {}, true); + thresholdSettingsPanelPopover.tbComponentRef.instance.popover = thresholdSettingsPanelPopover; + thresholdSettingsPanelPopover.tbComponentRef.instance.thresholdSettingsApplied.subscribe((thresholdSettings) => { + thresholdSettingsPanelPopover.hide(); + this.modelValue = thresholdSettings; + this.propagateChange(this.modelValue); + }); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.html index e975eb4653..ba0325da93 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.html @@ -21,7 +21,7 @@
widgets.time-series-chart.threshold.source
widgets.time-series-chart.threshold.key-value
-
widgets.time-series-chart.axis.y-axis
+
widgets.time-series-chart.axis.y-axis
widgets.color.color
widget-config.units-short
widget-config.decimals-short
@@ -31,6 +31,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.ts index 9cb5e5ced2..95e3b242c3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.ts @@ -38,6 +38,7 @@ import { IAliasController } from '@core/api/widget-api.models'; import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; import { DataKey, Datasource, WidgetConfig } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-time-series-chart-thresholds-panel', @@ -77,6 +78,10 @@ export class TimeSeriesChartThresholdsPanelComponent implements ControlValueAcce @Input() yAxisIds: TimeSeriesChartYAxisId[]; + @Input() + @coerceBoolean() + hideYAxis = false; + thresholdsFormGroup: UntypedFormGroup; private propagateChange = (_val: any) => {}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts index a4824fd51e..3fcde90cef 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts @@ -36,9 +36,10 @@ import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; import { coerceBoolean } from '@shared/decorators/coercion'; import { - TimeSeriesChartYAxisSettingsPanelComponent -} from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component'; + TimeSeriesChartAxisSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component'; import { deepClone } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'tb-time-series-chart-y-axis-row', @@ -76,6 +77,7 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O private propagateChange = (_val: any) => {}; constructor(private fb: UntypedFormBuilder, + private translate: TranslateService, private popoverService: TbPopoverService, private renderer: Renderer2, private viewContainerRef: ViewContainerRef, @@ -143,16 +145,18 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O this.popoverService.hidePopover(trigger); } else { const ctx: any = { - yAxisSettings: deepClone(this.modelValue), + axisType: 'yAxis', + panelTitle: this.translate.instant('widgets.time-series-chart.axis.y-axis-settings'), + axisSettings: deepClone(this.modelValue), advanced: this.advanced }; const yAxisSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, TimeSeriesChartYAxisSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, + this.viewContainerRef, TimeSeriesChartAxisSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, ctx, {}, {}, {}, true); yAxisSettingsPanelPopover.tbComponentRef.instance.popover = yAxisSettingsPanelPopover; - yAxisSettingsPanelPopover.tbComponentRef.instance.yAxisSettingsApplied.subscribe((yAxisSettings) => { + yAxisSettingsPanelPopover.tbComponentRef.instance.axisSettingsApplied.subscribe((yAxisSettings) => { yAxisSettingsPanelPopover.hide(); this.modelValue = {...this.modelValue, ...yAxisSettings}; this.axisFormGroup.patchValue( diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.html index 239c6ea18c..28c79a1310 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
widgets.time-series-chart.no-aggregation-bar-width-strategy
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.ts index 36d8cb917a..8601c48159 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component.ts @@ -29,6 +29,7 @@ import { timeSeriesChartNoAggregationBarWidthStrategyTranslations } from '@home/components/widget/lib/chart/time-series-chart.models'; import { merge } from 'rxjs'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-time-series-no-aggregation-bar-width-settings', @@ -53,6 +54,10 @@ export class TimeSeriesNoAggregationBarWidthSettingsComponent implements OnInit, @Input() disabled: boolean; + @Input() + @coerceBoolean() + stroked = false; + private modelValue: TimeSeriesChartNoAggregationBarWidthSettings; private propagateChange = null; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.html index 94831b530f..d5cc9a183e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.html @@ -19,6 +19,7 @@ [class.tb-suffix-absolute]="!keysFormControl.value?.length">
@@ -49,7 +50,8 @@ (click)="editKey()" mat-icon-button class="tb-mat-24"> edit - diff --git a/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.ts b/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.ts index 260a7667f8..a1183acf88 100644 --- a/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.ts +++ b/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, forwardRef, Input, OnDestroy, Renderer2, ViewContainerRef } from '@angular/core'; +import { ChangeDetectorRef, Component, forwardRef, Input, OnDestroy } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -24,10 +24,13 @@ import { DndDropEvent } from 'ngx-drag-drop'; import { isUndefined } from '@core/utils'; import { coerceBoolean } from '@shared/decorators/coercion'; import { ImageLinkType } from '@shared/components/image/gallery-image-input.component'; -import { TbPopoverService } from '@shared/components/popover.service'; -import { MatButton } from '@angular/material/button'; -import { ImageGalleryComponent } from '@shared/components/image/image-gallery.component'; -import { prependTbImagePrefixToUrls, removeTbImagePrefixFromUrls } from '@shared/models/resource.models'; +import { + ImageResourceInfo, + prependTbImagePrefixToUrls, + removeTbImagePrefixFromUrls +} from '@shared/models/resource.models'; +import { MatDialog } from '@angular/material/dialog'; +import { ImageGalleryDialogComponent } from '@shared/components/image/image-gallery-dialog.component'; @Component({ selector: 'tb-multiple-gallery-image-input', @@ -66,10 +69,8 @@ export class MultipleGalleryImageInputComponent extends PageComponent implements private propagateChange = null; constructor(protected store: Store, - private cd: ChangeDetectorRef, - private renderer: Renderer2, - private viewContainerRef: ViewContainerRef, - private popoverService: TbPopoverService) { + private dialog: MatDialog, + private cd: ChangeDetectorRef) { super(store); } @@ -130,31 +131,21 @@ export class MultipleGalleryImageInputComponent extends PageComponent implements this.updateModel(); } - toggleGallery($event: Event, browseGalleryButton: MatButton) { + toggleGallery($event: Event) { if ($event) { $event.stopPropagation(); } - const trigger = browseGalleryButton._elementRef.nativeElement; - if (this.popoverService.hasPopover(trigger)) { - this.popoverService.hidePopover(trigger); - } else { - const ctx: any = { - pageMode: false, - popoverMode: true, - mode: 'grid', - selectionMode: true - }; - const imageGalleryPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, ImageGalleryComponent, 'top', true, null, - ctx, - {}, - {}, {}, true); - imageGalleryPopover.tbComponentRef.instance.imageSelected.subscribe((image) => { - imageGalleryPopover.hide(); + this.dialog.open(ImageGalleryDialogComponent, { + autoFocus: false, + disableClose: false, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed().subscribe((image) => { + if (image) { this.imageUrls.push(image.link); this.updateModel(); - }); - } + } + }); } imageDragStart(index: number) { diff --git a/ui-ngx/src/app/shared/models/asset.models.ts b/ui-ngx/src/app/shared/models/asset.models.ts index bcdde08009..6ba8bc8abb 100644 --- a/ui-ngx/src/app/shared/models/asset.models.ts +++ b/ui-ngx/src/app/shared/models/asset.models.ts @@ -24,8 +24,6 @@ import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { DashboardId } from '@shared/models/id/dashboard-id'; import { EntityInfoData, HasTenantId } from '@shared/models/entity.models'; -export const TB_SERVICE_QUEUE = 'TbServiceQueue'; - export interface AssetProfile extends BaseData, HasTenantId, ExportableEntity { tenantId?: TenantId; name: string; diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 194327f671..885ea90929 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -39,6 +39,7 @@ export enum EntityType { OTA_PACKAGE = 'OTA_PACKAGE', RPC = 'RPC', QUEUE = 'QUEUE', + QUEUE_STATS = 'QUEUE_STATS', NOTIFICATION = 'NOTIFICATION', NOTIFICATION_REQUEST = 'NOTIFICATION_REQUEST', NOTIFICATION_RULE = 'NOTIFICATION_RULE', @@ -362,6 +363,13 @@ export const entityTypeTranslations = new Map ({ export interface WidgetComparisonSettings { comparisonEnabled?: boolean; - timeForComparison?: moment_.unitOfTime.DurationConstructor; + timeForComparison?: ComparisonDuration; comparisonCustomIntervalValue?: number; } +export interface DataKeyComparisonSettings { + showValuesForComparison: boolean; + comparisonValuesLabel: string; + color: string; +} + +export interface DataKeySettingsWithComparison { + comparisonSettings?: DataKeyComparisonSettings; +} + +export const isDataKeySettingsWithComparison = (settings: any): settings is DataKeySettingsWithComparison => + 'comparisonSettings' in settings; + export interface WidgetSettings { [key: string]: any; } diff --git a/ui-ngx/src/assets/dashboard/api_usage.json b/ui-ngx/src/assets/dashboard/api_usage.json index c0f3645156..5564223512 100644 --- a/ui-ngx/src/assets/dashboard/api_usage.json +++ b/ui-ngx/src/assets/dashboard/api_usage.json @@ -14,6 +14,7 @@ "datasources": [ { "type": "entity", + "entityAliasId": "2e4c97b0-257a-a1b9-690c-141d9bf2ec6f", "dataKeys": [ { "name": "ruleEngineException", @@ -52,7 +53,29 @@ "_hash": 0.7255162989552142 } ], - "entityAliasId": "2e4c97b0-257a-a1b9-690c-141d9bf2ec6f" + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + }, + "latestDataKeys": [ + { + "name": "queueName", + "type": "entityField", + "label": "Queue name", + "color": "#ffc107", + "settings": {}, + "_hash": 0.6889245277142959 + }, + { + "name": "serviceId", + "type": "entityField", + "label": "Service Id", + "color": "#607d8b", + "settings": {}, + "_hash": 0.7972226575450785 + } + ] } ], "timewindow": { @@ -7690,7 +7713,25 @@ "statusList": [ "ACTIVE" ] - } + }, + "latestDataKeys": [ + { + "name": "queueName", + "type": "entityField", + "label": "Queue name", + "color": "#ffc107", + "settings": {}, + "_hash": 0.7021721434431745 + }, + { + "name": "serviceId", + "type": "entityField", + "label": "Service Id", + "color": "#607d8b", + "settings": {}, + "_hash": 0.5924381120750077 + } + ] } ], "timewindow": { @@ -8080,7 +8121,25 @@ "statusList": [ "ACTIVE" ] - } + }, + "latestDataKeys": [ + { + "name": "queueName", + "type": "entityField", + "label": "Queue name", + "color": "#f44336", + "settings": {}, + "_hash": 0.7066844328378095 + }, + { + "name": "serviceId", + "type": "entityField", + "label": "Service Id", + "color": "#ffc107", + "settings": {}, + "_hash": 0.1371570237026627 + } + ] } ], "timewindow": { @@ -8674,12 +8733,9 @@ "id": "2e4c97b0-257a-a1b9-690c-141d9bf2ec6f", "alias": "TbServiceQueues", "filter": { - "type": "assetType", + "type": "entityType", "resolveMultiple": true, - "assetNameFilter": "", - "assetTypes": [ - "TbServiceQueue" - ] + "entityType": "QUEUE_STATS" } } }, diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/chart/ticks_generator_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/chart/ticks_generator_fn.md new file mode 100644 index 0000000000..e3b5bf817a --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/chart/ticks_generator_fn.md @@ -0,0 +1,61 @@ +#### Ticks generator function + +
+
+ +*function (extent): {value: number}[]* + +A JavaScript function used to generate Y axis ticks. + +**Parameters:** + +
    +
  • extent: number[] - An array of two numbers holding axis min and max values [axisMin, axisMax]. +
  • +
+ +**Returns:** + +An array of tick values with the following structure: + +```typescript +{ + value: number +} +``` + +
+ +##### Examples + +* Always display only one tick in the middle: + +```javascript +return extent ? [{ value: (extent[0] + extent[1]) / 2}] : []; +{:copy-code} +``` + +* Display only min and max ticks: + +```javascript +if (extent) { + return [ {value: extent[0]}, {value: extent[1]} ]; +} else { + return []; +} +{:copy-code} +``` + +* Disable ticks: + +```javascript +return []; +{:copy-code} +``` + +* Constant ticks (1,2,3): + +```javascript +return [ {value: 1}, {value: 2}, {value: 3} ]; +{:copy-code} +``` diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index f8f752b5b3..b62aac95c2 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -283,6 +283,7 @@ "domain-schema-https": "HTTPS", "domain-schema-mixed": "HTTP+HTTPS", "enable": "Enable OAuth2 settings", + "edge-enable": "Propagate to Edge", "domains": "Domains", "mobile-apps": "Mobile applications", "no-mobile-apps": "No applications configured", @@ -2244,6 +2245,8 @@ "type-ota-package": "OTA package", "type-rpc": "RPC", "type-queue": "Queue", + "type-queue-stats": "Queue statistics", + "type-queues-stats": "Queues statistics", "type-notification": "Notification", "type-notification-rule": "Notification rule", "type-notification-rules": "Notification rules", @@ -2271,7 +2274,9 @@ "address2": "Address 2", "zip": "Zip", "phone": "Phone", - "label": "Label" + "label": "Label", + "queue-name": "Queue name", + "service-id": "Service Id" }, "entity-view": { "entity-view": "Entity view", @@ -2730,7 +2735,7 @@ "rpc-command-result": "Response", "rpc-command-edit-params": "Edit parameters", "gateway-configuration": "General Configuration", - "docker-label": "Use the following instruction to run IoT Gateway in in Docker compose with credentials for selected device", + "docker-label": "Use the following instruction to run IoT Gateway in Docker compose with credentials for selected device", "install-docker-compose": "Use the instructions to download, install and setup docker compose", "download-configuration-file": "Download configuration file", "download-docker-compose": "Download docker-compose.yml for your gateway", @@ -5015,7 +5020,9 @@ "device-credentials-conflict": "Failed to load the device with external id {{entityId}}
due to the same credentials are already present in the database for another device.
Please consider disabling the load credentials setting in the restore form.", "missing-referenced-entity": "Failed to load the {{sourceEntityTypeName}} with external id {{sourceEntityId}}
because it references missing {{targetEntityTypeName}} with id {{targetEntityId}}.", "runtime-failed": "Failed: {{message}}", - "auto-commit-settings-read-only-hint": "Auto-commit feature doesn't work with enabled read-only option in Repository settings." + "auto-commit-settings-read-only-hint": "Auto-commit feature doesn't work with enabled read-only option in Repository settings.", + "rollback-on-error": "Rollback on error", + "rollback-on-error-hint": "If you have a large amount of entities to restore, consider disabling this option to increase performance.\n Note, if an error occurs over the course of version loading, already persisted entities (with relations, attributes, etc.) will stay as is" }, "widget": { "widget-library": "Widgets library", @@ -5422,7 +5429,7 @@ "bar-appearance": "Bar appearance", "label-on-bar": "Label on bar", "value-on-bar": "Value on bar", - "bar-chart-card-style": "Bar chart card style" + "bar-chart-style": "Bar chart style" }, "battery-level": { "layout": "Layout", @@ -6172,10 +6179,14 @@ "range-chart": { "chart": "Chart", "data-zoom": "Data zoom", + "range-chart-appearance": "Range chart appearance", "range-colors": "Range colors", "out-of-range-color": "Out of range color", + "show-range-thresholds": "Show range thresholds", + "range-thresholds-settings": "Range thresholds settings", "fill-area": "Fill area", - "range-chart-card-style": "Range chart card style" + "fill-area-opacity": "Fill area opacity", + "range-chart-style": "Range chart style" }, "rpc": { "value-settings": "Value settings", @@ -6738,6 +6749,15 @@ "bar-width": "Bar width", "bar-width-relative": "Percentage of time window", "bar-width-absolute": "Absolute (ms)", + "comparison": { + "comparison": "Comparison", + "show": "Show", + "settings": "Comparison settings", + "show-values-for-comparison": "Show historical data for comparison", + "comparison-values-label": "Comparison key label", + "comparison-values-label-auto": "Auto", + "comparison-data-color": "Comparison data color" + }, "threshold": { "thresholds": "Thresholds", "source": "Source", @@ -6769,13 +6789,34 @@ "label-position-inside-middle-bottom": "Inside middle bottom", "label-position-inside-end": "Inside end", "label-position-inside-end-top": "Inside end top", - "label-position-inside-end-bottom": "Inside end bottom" + "label-position-inside-end-bottom": "Inside end bottom", + "label-background": "Label background" + }, + "state": { + "states": "States", + "label": "Label", + "ticks-value": "Ticks value", + "source": "Source", + "value-range": "Value / Range", + "no-states": "No states configured", + "add-state": "Add state", + "type-constant": "Constant", + "type-range": "Range", + "from": "From", + "to": "To", + "remove-state": "Remove state" + }, + "grid": { + "grid": "Grid", + "background-color": "Background color", + "border": "Border" }, "axis": { "axes": "Axes", "x-axis": "X axis", "y-axis": "Y axis", "y-axis-settings": "Y axis settings", + "comparison-x-axis-settings": "Comparison X axis settings", "remove-y-axis": "Remove Y axis", "id": "Id", "label": "Label", @@ -6786,6 +6827,7 @@ "position-bottom": "Bottom", "tick-labels": "Tick labels", "ticks-formatter-function": "Ticks formatter function", + "ticks-generator-function": "Ticks generator function", "show-ticks": "Show ticks", "show-line": "Show line", "show-split-lines": "Show split lines", @@ -6850,6 +6892,7 @@ "show-points": "Show points", "point-label": "Point label", "point-label-hint": "Display label with value over the series point.", + "point-label-background": "Point label background", "point-shape": "Point shape", "point-size": "Point size" }, @@ -6858,7 +6901,8 @@ "border-width": "Border width", "border-radius": "Border radius", "label": "Label", - "label-hint": "Display label with value over the bar." + "label-hint": "Display label with value over the bar.", + "label-background": "Label background" } } }, diff --git a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json index 014666017c..58d4a0168f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json +++ b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json @@ -5172,7 +5172,7 @@ "bar-appearance":"Wygląd słupka", "label-on-bar":"Etykieta na słupku", "value-on-bar":"Wartość na słupku", - "bar-chart-card-style":"Styl karty wykresu słupkowego" + "bar-chart-style":"Styl wykresu słupkowego" }, "battery-level":{ "layout":"Układ", @@ -5924,7 +5924,7 @@ "range-colors":"Kolory zakresu", "out-of-range-color":"Kolor poza zakresem", "fill-area":"Wypełnij obszar", - "range-chart-card-style":"Styl karty wykresu zakresu" + "range-chart-style":"Styl wykresu zakresu" }, "rpc":{ "value-settings":"Ustawienia wartości", diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index fdc0274cdf..886151fdf2 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -5491,7 +5491,7 @@ "range-colors": "范围颜色", "out-of-range-color": "超出范围颜色", "fill-area": "填充区域", - "range-chart-card-style": "范围图表卡片样式" + "range-chart-style": "范围图样式" }, "rpc": { "value-settings": "值设置", diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index f4d3cdb398..1f9ca93c88 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -216,6 +216,18 @@ flex: 1; width: auto; } + &.flex-xs { + @media #{$mat-xs} { + width: auto; + flex: 1; + } + } + &.flex-lt-md { + @media #{$mat-lt-md} { + width: auto; + flex: 1; + } + } } .fixed-title-width { min-width: 200px; diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index ca5c179ad5..ddcd48243b 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -1582,10 +1582,10 @@ dependencies: tslib "^2.2.0" -"@geoman-io/leaflet-geoman-free@^2.13.0": - version "2.16.0" - resolved "https://registry.yarnpkg.com/@geoman-io/leaflet-geoman-free/-/leaflet-geoman-free-2.16.0.tgz#c8a92fcc2cdd5770ebf43f8ef9f03cc8ef842359" - integrity sha512-BnKAAoTXraWVFfhX/0gT/iBgAz1BPfpbdQ9dJamEFI4lIku9UNXXluu/E0k7YkZETq0tENX2GPnKLB4p+VgrSw== +"@geoman-io/leaflet-geoman-free@2.14.2": + version "2.14.2" + resolved "https://registry.yarnpkg.com/@geoman-io/leaflet-geoman-free/-/leaflet-geoman-free-2.14.2.tgz#c84c2115c263f34d11dc0b43859551639fe3d56b" + integrity sha512-6lIyG8RvSVdFjVjiQgBPyNASjymSyqzsiUeBW0pA+q41lB5fAg4SDC6SfJvWdEyDHa81Jb5FWjUkCc9O+u0gbg== dependencies: "@turf/boolean-contains" "^6.5.0" "@turf/kinks" "^6.5.0" @@ -3079,28 +3079,28 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/leaflet-polylinedecorator@^1.6.1": +"@types/leaflet-polylinedecorator@1.6.4": version "1.6.4" resolved "https://registry.yarnpkg.com/@types/leaflet-polylinedecorator/-/leaflet-polylinedecorator-1.6.4.tgz#2270b84585bd28bbf1fef237be8480196c44ac06" integrity sha512-meH/jC58Drt/9aqLhuzMCUQjHgMTud+UuQBK5gT6E8M6OfM/rftz/VgvdxOCCAQC9isrp4pqahDgEswesb5X3A== dependencies: "@types/leaflet" "*" -"@types/leaflet-providers@^1.2.1": +"@types/leaflet-providers@1.2.4": version "1.2.4" resolved "https://registry.yarnpkg.com/@types/leaflet-providers/-/leaflet-providers-1.2.4.tgz#284e8a197d4c2dbfeda436842e03b2adb502be16" integrity sha512-4wYEpreixp+G5t510s202eQ5eubOmxHevIfCNrzUZbzp50XQsC9lSetX7MnPl3ANRJnUBbCWOzZ9EQFiH0Jm+g== dependencies: "@types/leaflet" "*" -"@types/leaflet.gridlayer.googlemutant@^0.4.6": +"@types/leaflet.gridlayer.googlemutant@0.4.9": version "0.4.9" resolved "https://registry.yarnpkg.com/@types/leaflet.gridlayer.googlemutant/-/leaflet.gridlayer.googlemutant-0.4.9.tgz#9090ddf4578ce631d22b8aafc5ed12525b4fbf90" integrity sha512-u/5Avs1KKkeABRDixGbGp2ldFs67+fYIATpkvvFgN+LQPNfq7dFd5adkksshiQBfYJ4SaQg1VJgN2dNirIC0aQ== dependencies: "@types/leaflet" "*" -"@types/leaflet.markercluster@^1.5.1": +"@types/leaflet.markercluster@1.5.4": version "1.5.4" resolved "https://registry.yarnpkg.com/@types/leaflet.markercluster/-/leaflet.markercluster-1.5.4.tgz#2ab43417cf3f6a42d0f1baf4e1c8f659cf1dc3a1" integrity sha512-tfMP8J62+wfsVLDLGh5Zh1JZxijCaBmVsMAX78MkLPwvPitmZZtSin5aWOVRhZrCS+pEOZwNzexbfWXlY+7yjg== @@ -3114,7 +3114,7 @@ dependencies: "@types/geojson" "*" -"@types/leaflet@~1.8.0": +"@types/leaflet@1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.8.0.tgz#dc92d3e868fb6d5067b4b59fa08cd4441f84fabe" integrity sha512-+sXFmiJTFdhaXXIGFlV5re9AdqtAODoXbGAvxx02e5SHXL3ir7ClP5J7pahO8VmzKY3dth4RUS1nf2BTT+DW1A== @@ -5437,10 +5437,9 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -echarts@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/echarts/-/echarts-5.5.0.tgz#c13945a7f3acdd67c134d8a9ac67e917830113ac" - integrity sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw== +"echarts@https://github.com/thingsboard/echarts/archive/5.5.0-TB.tar.gz": + version "5.5.0-TB" + resolved "https://github.com/thingsboard/echarts/archive/5.5.0-TB.tar.gz#d1017728576b3fd65532eb17e503d1349f7feaed" dependencies: tslib "2.3.0" zrender "5.5.0" @@ -7750,14 +7749,14 @@ lcov-parse@1.0.0: resolved "https://registry.yarnpkg.com/lcov-parse/-/lcov-parse-1.0.0.tgz#eb0d46b54111ebc561acb4c408ef9363bdc8f7e0" integrity sha512-aprLII/vPzuQvYZnDRU78Fns9I2Ag3gi4Ipga/hxnVMCZC8DnR2nI7XBqrPoywGfxqIx/DgarGvDJZAD3YBTgQ== -leaflet-polylinedecorator@^1.6.0: +leaflet-polylinedecorator@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/leaflet-polylinedecorator/-/leaflet-polylinedecorator-1.6.0.tgz#9ef79fd1b5302d67b72efe959a8ecd2553f27266" integrity sha512-kn3krmZRetgvN0wjhgYL8kvyLS0tUogAl0vtHuXQnwlYNjbl7aLQpkoFUo8UB8gVZoB0dhI4Tb55VdTJAcYzzQ== dependencies: leaflet-rotatedmarker "^0.2.0" -leaflet-providers@^1.13.0: +leaflet-providers@1.13.0: version "1.13.0" resolved "https://registry.yarnpkg.com/leaflet-providers/-/leaflet-providers-1.13.0.tgz#10c843a23d5823a65096d40ad53f27029e13434b" integrity sha512-f/sN5wdgBbVA2jcCYzScIfYNxKdn2wBJP9bu+5cRX9Xj6g8Bt1G9Sr8WgJAt/ckIFIc3LVVxCBNFpSCfTuUElg== @@ -7767,17 +7766,17 @@ leaflet-rotatedmarker@^0.2.0: resolved "https://registry.yarnpkg.com/leaflet-rotatedmarker/-/leaflet-rotatedmarker-0.2.0.tgz#4467f49f98d1bfd56959bd9c6705203dd2601277" integrity sha512-yc97gxLXwbZa+Gk9VCcqI0CkvIBC9oNTTjFsHqq4EQvANrvaboib4UdeQLyTnEqDpaXHCqzwwVIDHtvz2mUiDg== -leaflet.gridlayer.googlemutant@^0.13.5: - version "0.13.5" - resolved "https://registry.yarnpkg.com/leaflet.gridlayer.googlemutant/-/leaflet.gridlayer.googlemutant-0.13.5.tgz#7182c26f479e726bff8b85a7ebf9d3f44dd3ea57" - integrity sha512-DHUEXpo1t0WZ9tpdLUHHkrTK7LldCXr/gqkV5E/4hHidDJB2srceLLCZj4PV/pl0jPdTkVBT+Lr8nL9EZDB5hw== +leaflet.gridlayer.googlemutant@0.14.1: + version "0.14.1" + resolved "https://registry.yarnpkg.com/leaflet.gridlayer.googlemutant/-/leaflet.gridlayer.googlemutant-0.14.1.tgz#c282209aa1a39eb2f87d8aaa4e9894181c9e20a0" + integrity sha512-/OYxEjmgxO1U1KOhTzg+m8c0b95J0943LU8DXQmdJu/x2f+1Ur78rvEPO2QCS0cmwZ3m6FvE5I3zXnBzJNWRCA== -leaflet.markercluster@^1.5.3: +leaflet.markercluster@1.5.3: version "1.5.3" resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz#9cdb52a4eab92671832e1ef9899669e80efc4056" integrity sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA== -leaflet@~1.8.0: +leaflet@1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.8.0.tgz#4615db4a22a304e8e692cae9270b983b38a2055e" integrity sha512-gwhMjFCQiYs3x/Sf+d49f10ERXaEFCPr+nVTryhAW8DWbMGqJqt9G4XuIaHmFW08zYvhgdzqXGr8AlW8v8dQkA==