diff --git a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json index 16a155cebb..04f4042fae 100644 --- a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json @@ -3,7 +3,9 @@ "alias": "alarm_widgets", "title": "Alarm widgets", "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAOPElEQVR42u2deVsTVx+G/Xb9ALVeffuHtmql2kVrVWytWltr64KoqIgbolRFRMQNtCwioiLIqii7grIoShARwnbee3Js3pgibzYQzPNcXLmGyUwmc+ae35kE7jOzjDHDw8MvFCVCGRoaAqpZlqqxsTGjKGEHkMAJqGaJKmUy2HLAUlsokY0DVk9PjxpCiWyASmApAksRWIrAEliKwFI+DLD4pst3gbKyss7OTjWcEi5Yubm5X3zxxejoqP01ISGhtLRUDaeEC9ZPP/30xx9/UKj8wBoYGLh9+3Z5ebllrrW1taOj49atW263u6mpqaWl5ebNm/39/c+ePbt+/bq3yD1//vzatWsNDQ1q+qgG6/Hjx+vWrauvr9+8ebMvWPyVccWKFZmZmXv37k1MTGT+rl27li1bduLECfD67LPPjh07lpyc/N133+3YsSMrK2vhwoUvX77s7u5mzvnz59evX3/lyhW1fvSCBR95eXlMAERvb68XLMoSzLW3t9+9e3fx4sUWrJKSEiYAKzY21q4+b948ChsT27Ztq6mpoZKtXLmSFYHs0aNHav0oBYs+jquruLi4PXv2fPPNN1QaL1ivX7/++eefjx49mpaWFhMTY8G6c+eOBWv16tX2FebPn287SupWZWUlE3SdW7duXbVqFVVQrR+lYAEQPWCnJw8ePKCn84IFJfBh+8pFixYFCFZtbS0XXvxaUVFBDVPrRylYf/75p/ea3V7FNzY2WrC4Kl++fDld3u+///7pp59CTyBg0ZlydbVp0yYY9X1lJRo/FU4Qe/0UbAYHB71fXigCS1EEliKwFIGlKAJLEViKwFIUgaUILEVgKYrAUgSWIrAURWApAksRWIoisBSBpXz4YO3evXvleEHameJ3WVBQoEP14YAFQ0HNn7xM/RZHRkaCWp4RLt7j1qMOLLyd77///qOPPlq6dOmpU6dmBFjV1dWYkrzt1NTUwNc6c+YMCi7jXIQv4m7YsIH9xRB++vQptrDfC+JjFhUVqWI5WbBgAboYLYLyigN98ODBnJwchDCU1ydPnvT19WEvopG5XC6/FfGt/bZoBxCf1Ozfv/+vv/7CI2JkAEb/PXLkCO8TrRIpnIYCHexIhp9g5qVLl3DdEHoZZODcuXNYcXPnzv31119Pnz798OHDGzduWDU8qKAwffzxx69evUIc5xVQyeGMRkPDZAKLk+3SnnV1dbxJe7lCecO941lWiUawkKcZwYEDk5KSQnNw2VRYWMhR2bdvH3MOHTrEkfNdC4eR1gQ77xZpbpZn/qS2CEeRQVC+/vpryi2jnvz444/FxcWIkOCSnZ3NBOOaoFJyFBmi4t69exRj9o63SqnjbGEt9vHw4cPIlZw2IbyBq1evfvXVV7wUkO3cuZMXbGtr4zWTkpIOHDgA0zxCGJqn8QxiANw4w7wf63VGHVhM/Pbbb19++SWNAliczRwJ5lCrOOE44/99wgEfvjWaK1u0VE3BUCKXL19mKGnomTNnDiQx9gnvLT8/n5n0j2vXrmX8nNmzZzOTksZ7pkQZz+AUXrA4hehM6ctC2DrbZR+plCDFJixYbJ2CRO2kNFqwQJZhCsw/o2Pgl9OMdAVRChb7D1i2gHOQOC/Bi2PDYaDCc2D+vS6tDE9scWqoMp6BBRiogi1u374dpildHFR7gYj8zTFmgpJGwaAHZzwmX7BYzDYOJ0xo15T0a7/88gutQXVvbm5OT0/nBRkcyl72bdmyhSsKOlwuvBhWg01/8sknjOFD1WQV2zlG3adCPjfR2XGRa8ECKe8nKS5oJvgcRN1ii1M87JHvxdy4n/je9TGQFbno5g1T2CKydTvtuzk7x6/RpuDqM2Jg8X3VuN9jMSZWCFuiaFHbbVtQpWj9wNdl/KMZ9EmboZ0Y9UTfY+mbd0VgKQJLEVgCSxFYisBSBJaiCCxlRoDFXzF143UlsgEqVSxFXaEisBSBJbAUgaUILEVgKYrAUgSWIrD8M30Ue+WDAmv6KPbTKSNmbNLviee1JDAD8N6CXZ07vX/gYCFB4KOiK/GI/BmpN42CPIlN0pRsbi14M125xtRufuvZzlzTM7nCMboi5hl3IcXIOHv2bAh+M45hVFQsa95ZICAM/47GQn1GRqXhsFXxQnkP9LDcvhUP2C6JnFnjCQtwO9aMjAy6Zqxz5EScO3xo5hiPeIi7zKuxOiIQE2zCaoyhg1U027xsMO4eU/wfB6zhl6Z2m6neYPrbTFehcVWb/sfm3iZzP84M95vWDHNvc6RoQ1XFSbRF6+LFixYs7EUkVSsqImTTSjQCFj86K24c06iqqNKsgi3NNI5hFIGFvLtx40YGQUD1ZKwLWoQ/gNtGRNFk2lpl69atwyjn9r4suWbNmlxPcOiQNquqqhDevWekfQQjjE2W5JDQprwswOGZhQVWfaKpTzCt6c4EYIERhaphv2lIMi1HTcffpiLWmfnkvGlJNTUbTddVp4uM0PFAp/b+asFir2kNWg+J3LYDbcgjd3oHph9++IGhHDC2UTVh0SqvUQQWDUR5t2cYYFF7qDSccxYRwKLYGI9DTJMhGbMkjQhVLE8FYj53leaW0l6kLJQWLDuHtRhuhBU5m8MC60mOKVvm/DwvdcDqLDA1Gxyk6ve+Aaskxgy5nPnQBli9dRG8ukK89qtYGNgFnrS3t9t2AB1aiUFWaBkMafssbcUwE1HXFdJSmOmMlnHhwoVxwUJsT/IE69cuSctasBgNgTOSzpFlWJ5SzzlKj0CRYzwML1gM0QG7LBbWWEKA1Z5jWk+ZugTzvNwBq7vEIenOcqfLs2B15pmKFQ55/a2RBctWKXYNm55RGyxY6OPsFDuL8m/b4dtvv7VgsTy/UqqxzBlAhaEl6DSp31H3qfBdZrqtWL7P+i3pa5SPeGLG88ontvXDuPbxvJnhAefSqqvozcfDSQv77nc/9omHI/A+y/XANBm0beoU+4lDSecyYrp/1eB+YVrPvIFMCRksRRFYisBSBJaiCCxFYCkCS1EEljKdwJJir0ixV9QVKgJLDaEILEVgKQJLUQSWIrAUgeUXmdDKpIAV1Sb02/9y7vzq+7/kE9zDDQs5HKvRE5nQ/ycz0oTu7jbcHHX1ahTH/8HETTrz8kxdncGvev3aJCW9c3V8obqwpB2Z0IFmhpnQKSkmJ8eZ4IalbW0mPt6sXeuAVVho0GUXLjS4ZYcPO8whleAc44Bw7+dNm5wf7mAdHlgyoYMGa8aY0Bs3Gt8iMWcOHZLJzXWAKyhwkHrxwixdyj3BTUKC89TZs6a21sGO3fz77zDBkgkdNFgzxoQGHTAimZlOKZo715n+N1gXL5rUVKduUdWYmZhotmyh0oYJlkzooMGaMSY0RWjJEqeP4wdufMGi15s3z+EGsFwu51KMprhwgf7JmV60yNBbhX2NJRM6lMwYE3pwcPz5brff/vhPRCIyoSOWmWFCKxEBS1EEliKwFIGlKAJLEViKwFIUgaVMJ7BkQisyoRV1hYrAUkMoAksRWIrAUhSBpQgsRWD5RSa0MilgTXMTmv/v9k5b99d3zlRmZHTkvex1VIMVlONmzacA17KOio21dOycMD3pQ3eTXwz0lHXeyWzI4tftZfFjZmxiqi42Z0f2qKAhlZeX+81EbRoYGEAe7OrqElhvIYJKiquE1YTShMrMI9OoWjyOC5Z1ne0jRhRd8PHjx9GbUDpxUex8BClec/HixXbJoqIiPGnMuxyPzWzt4aCS+zCv+llNen3G/qoDPYM9KfdSe929p+szTj5I63W/LOkoOV13pvb5/cyGc8fvnyxoLRweGb788Epnf+e5pgssXNlVNWrGeDajPjO7JSeEQ8If2rgIsdoqAzewv7jg9fX1S5YswZxDm6PF7CnE/iLG4cnRArhfUQpWbW0terilh7EuUlJSsOFoEY496rNdBj72eTJ//nzztvGMIc0j6qbb7T5y5Ah9H/NdLldcXBzz8en8lme7HIBUnNIg0+hqutScDSKZjVnFT24WtBV29HdUP7sLK9cfF4NLRVely+3aVbFn1IxuLY0bHB3cU7mvsacp7cGpvqE+pmu677Lu4Mjg9rIdIRySkydPMuYFxiXmUnJyMpIqEjnnHnhhYmI/AxnWIY4hDcUOcgrBWXp6epSCxamWn5/PBDIqFwrx8fGHPKG93tUVWtfZFxc0c1uHaFzmdHZ2ckL7LWMfaXE8fY5KsC3iHnFvK4271JINTFtKtz3qbb3RfutMQxb9XeHja4D1sPdRn7vvQNVBFo6/s9MLVlbjebrFXeV76EZzH+XbZ0M4JCjzEMObZ6AA6hZ4UZ4pSL5gARMNiLXLApQxNOjm5uboAouOyRYhKjxIQRJnJPMxnlGcORHp494FFhix4ueff/4usJhGfabysRXvMrGxsdQzLkToO0JrFKpR3Yv6V0Ov1havh5XKp5V7q/YlViXR5QUCFs/urkg8cT8thIqFzG3bh3OPRgCdzZ6wv3R8aZ4wTQ8YExPDYkzTqrQSlxZR/akwWE93OADD2O81WYU5IBvW0CBvZ3RsNPCF6QRvdty63VF6rPZ4BD5y/qN9O9XUz8O2722SboM9eWBNpQkd2cAWVL3H5qbglbTfHhgZ0PdYiiKwFIGlCCxFEViKwFIElqIILGWagyUTWpEJragrVASWGkIRWIrAUgSWoggsRWApAssvMqGVSQFLJnSAkQn9HsCSCR1CZEIHB5ZM6EAiEzo4sGRCBxiZ0MGBJRM6wMiEDigyoYOKTOhQP0DJhA6+xWRCT5fIhJ7WYCmKwFIEliKwFEVgKdMMLL5hUkMokQ1QOWDNlL+ZKzMi4OSANTQ0JLaUyFLFl4izjOcbxe7ubv709lRRwggIAZL9avq/0p2LbK71A+cAAAAASUVORK5CYII=", - "description": "Visualization of alarms for devices, assets and other entities." + "description": "Visualization of alarms for devices, assets and other entities.", + "externalId": null, + "name": "Alarm widgets" }, "widgetTypes": [ { @@ -23,7 +25,9 @@ "dataKeySettingsSchema": "", "settingsDirective": "tb-alarms-table-widget-settings", "dataKeySettingsDirective": "tb-alarms-table-key-settings", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"allowAssign\":true,\"displayActivity\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418},{\"name\":\"assignee\",\"type\":\"alarm\",\"label\":\"Assignee\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.5008441077416634}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false}" + "hasBasicMode": true, + "basicModeDirective": "tb-alarms-table-basic-config", + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"allowAssign\":true,\"displayActivity\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true,\"entitiesTitle\":null,\"alarmsTitle\":\"Alarms\"},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418},{\"name\":\"assignee\",\"type\":\"alarm\",\"label\":\"Assignee\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.5008441077416634}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"warning\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false,\"configMode\":\"basic\",\"alarmFilterConfig\":null}" } } ] diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 9ab33665e6..473d3d215b 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -64,7 +64,9 @@ "settingsDirective": "tb-timeseries-table-widget-settings", "dataKeySettingsDirective": "tb-timeseries-table-key-settings", "latestDataKeySettingsDirective": "tb-timeseries-table-latest-key-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true}" + "hasBasicMode": true, + "basicModeDirective": "tb-timeseries-table-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true,\"configMode\":\"basic\"}" } }, { 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 7ccce1d8c6..8dac2b5f5f 100644 --- a/application/src/main/data/json/system/widget_bundles/charts.json +++ b/application/src/main/data/json/system/widget_bundles/charts.json @@ -161,7 +161,9 @@ "settingsDirective": "tb-flot-line-widget-settings", "dataKeySettingsDirective": "tb-flot-line-key-settings", "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", - "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\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}" + "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\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"waterfall_chart\",\"iconColor\":\"#1F6BDD\"}" } }, { @@ -183,7 +185,9 @@ "settingsDirective": "tb-flot-line-widget-settings", "dataKeySettingsDirective": "tb-flot-line-key-settings", "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"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,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Timeseries Line Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}" + "hasBasicMode": true, + "basicModeDirective": "tb-flot-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"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,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Timeseries Line Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\"}" } }, { @@ -204,7 +208,9 @@ "settingsDirective": "tb-flot-bar-widget-settings", "dataKeySettingsDirective": "tb-flot-bar-key-settings", "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":true,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"defaultBarWidth\":600,\"barAlignment\":\"left\",\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Timeseries Bar Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{}}" + "hasBasicMode": true, + "basicModeDirective": "tb-flot-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":true,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"defaultBarWidth\":600,\"barAlignment\":\"left\",\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Timeseries Bar Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\"}" } } ] diff --git a/application/src/main/data/upgrade/3.5.1/schema_update.sql b/application/src/main/data/upgrade/3.5.1/schema_update.sql index 58031ce5c0..1655ecb978 100644 --- a/application/src/main/data/upgrade/3.5.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.5.1/schema_update.sql @@ -53,6 +53,69 @@ $$; -- NOTIFICATION CONFIGS VERSION CONTROL END +-- EDGE EVENTS MIGRATION START +DO +$$ + DECLARE table_partition RECORD; + BEGIN + -- in case of running the upgrade script a second time: + IF NOT (SELECT exists(SELECT FROM pg_tables WHERE tablename = 'old_edge_event')) THEN + ALTER TABLE edge_event RENAME TO old_edge_event; + CREATE INDEX IF NOT EXISTS idx_old_edge_event_created_time_tmp ON old_edge_event(created_time); + ALTER INDEX IF EXISTS idx_edge_event_tenant_id_and_created_time RENAME TO idx_old_edge_event_tenant_id_and_created_time; + + FOR table_partition IN SELECT tablename AS name, split_part(tablename, '_', 3) AS partition_ts + FROM pg_tables WHERE tablename LIKE 'edge_event_%' + LOOP + EXECUTE format('ALTER TABLE %s RENAME TO old_edge_event_%s', table_partition.name, table_partition.partition_ts); + END LOOP; + ELSE + RAISE NOTICE 'Table old_edge_event already exists, leaving as is'; + END IF; + END; +$$; + +CREATE TABLE IF NOT EXISTS edge_event ( + seq_id INT GENERATED ALWAYS AS IDENTITY, + id uuid NOT NULL, + created_time bigint NOT NULL, + edge_id uuid, + edge_event_type varchar(255), + edge_event_uid varchar(255), + entity_id uuid, + edge_event_action varchar(255), + body varchar(10000000), + tenant_id uuid, + ts bigint NOT NULL +) PARTITION BY RANGE (created_time); +CREATE INDEX IF NOT EXISTS idx_edge_event_tenant_id_and_created_time ON edge_event(tenant_id, created_time DESC); +CREATE INDEX IF NOT EXISTS idx_edge_event_id ON edge_event(id); +ALTER TABLE IF EXISTS edge_event ALTER COLUMN seq_id SET CYCLE; + +CREATE OR REPLACE PROCEDURE migrate_edge_event(IN start_time_ms BIGINT, IN end_time_ms BIGINT, IN partition_size_ms BIGINT) + LANGUAGE plpgsql AS +$$ +DECLARE + p RECORD; + partition_end_ts BIGINT; +BEGIN + FOR p IN SELECT DISTINCT (created_time - created_time % partition_size_ms) AS partition_ts FROM old_edge_event + WHERE created_time >= start_time_ms AND created_time < end_time_ms + LOOP + partition_end_ts = p.partition_ts + partition_size_ms; + RAISE NOTICE '[edge_event] Partition to create : [%-%]', p.partition_ts, partition_end_ts; + EXECUTE format('CREATE TABLE IF NOT EXISTS edge_event_%s PARTITION OF edge_event ' || + 'FOR VALUES FROM ( %s ) TO ( %s )', p.partition_ts, p.partition_ts, partition_end_ts); + END LOOP; + + INSERT INTO edge_event (id, created_time, edge_id, edge_event_type, edge_event_uid, entity_id, edge_event_action, body, tenant_id, ts) + SELECT id, created_time, edge_id, edge_event_type, edge_event_uid, entity_id, edge_event_action, body, tenant_id, ts + FROM old_edge_event + WHERE created_time >= start_time_ms AND created_time < end_time_ms; +END; +$$; +-- EDGE EVENTS MIGRATION END + ALTER TABLE resource ADD COLUMN IF NOT EXISTS etag varchar; 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 e04c7052db..fc822ce226 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -536,6 +536,10 @@ public class ActorSystemContext { @Getter private int maxRpcRetries; + @Value("${actors.rule.external.force_ack:false}") + @Getter + private boolean externalNodeForceAck; + @Getter @Setter private TbActorSystem actorSystem; diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index fb6fbbdff2..1461654216 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -73,10 +73,14 @@ public class AppActor extends ContextAwareActor { @Override protected boolean doProcess(TbActorMsg msg) { if (!ruleChainsInitialized) { - initTenantActors(); - ruleChainsInitialized = true; - if (msg.getMsgType() != MsgType.APP_INIT_MSG && msg.getMsgType() != MsgType.PARTITION_CHANGE_MSG) { - log.warn("Rule Chains initialized by unexpected message: {}", msg); + if (MsgType.APP_INIT_MSG.equals(msg.getMsgType())) { + initTenantActors(); + ruleChainsInitialized = true; + } else { + if (!msg.getMsgType().isIgnoreOnStart()) { + log.warn("Attempt to initialize Rule Chains by unexpected message: {}", msg); + } + return true; } } switch (msg.getMsgType()) { 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 8b76924d96..30d909cd5a 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 @@ -34,6 +34,7 @@ import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; @@ -214,6 +215,12 @@ class DefaultTbContext implements TbContext { enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbRelationTypes.FAILURE), failureMessage, null, null); } + @Override + public void enqueueForTellFailure(TbMsg tbMsg, Throwable th) { + TopicPartitionInfo tpi = resolvePartition(tbMsg); + enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbRelationTypes.FAILURE), getFailureMessage(th), null, null); + } + @Override public void enqueueForTellNext(TbMsg tbMsg, String relationType) { TopicPartitionInfo tpi = resolvePartition(tbMsg); @@ -311,16 +318,7 @@ class DefaultTbContext implements TbContext { if (nodeCtx.getSelf().isDebugMode()) { mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbRelationTypes.FAILURE, th); } - String failureMessage; - if (th != null) { - if (!StringUtils.isEmpty(th.getMessage())) { - failureMessage = th.getMessage(); - } else { - failureMessage = th.getClass().getSimpleName(); - } - } else { - failureMessage = null; - } + String failureMessage = getFailureMessage(th); nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getRuleChainId(), nodeCtx.getSelf().getId(), Collections.singleton(TbRelationTypes.FAILURE), msg, failureMessage)); @@ -724,6 +722,11 @@ class DefaultTbContext implements TbContext { return mainCtx.getSlackService(); } + @Override + public boolean isExternalNodeForceAck() { + return mainCtx.isExternalNodeForceAck(); + } + @Override public RuleEngineRpcService getRpcService() { return mainCtx.getTbRuleEngineDeviceRpcService(); @@ -840,10 +843,24 @@ class DefaultTbContext implements TbContext { } @Override - public void checkTenantEntity(EntityId entityId) { + public void checkTenantEntity(EntityId entityId) throws TbNodeException { if (!this.getTenantId().equals(TenantIdLoader.findTenantId(this, entityId))) { - throw new RuntimeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant."); + throw new TbNodeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant.", true); + } + } + + private static String getFailureMessage(Throwable th) { + String failureMessage; + if (th != null) { + if (!StringUtils.isEmpty(th.getMessage())) { + failureMessage = th.getMessage(); + } else { + failureMessage = th.getClass().getSimpleName(); + } + } else { + failureMessage = null; } + return failureMessage; } private class SimpleTbQueueCallback implements TbQueueCallback { diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java index 9534104ec1..7f919754fc 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.actors.TbEntityActorId; import org.thingsboard.server.actors.TbEntityTypeActorIdPredicate; import org.thingsboard.server.actors.service.ContextAwareActor; import org.thingsboard.server.actors.service.DefaultActorService; +import org.thingsboard.server.actors.shared.RuleChainErrorActor; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; @@ -31,6 +32,7 @@ import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbActorMsg; +import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.dao.rule.RuleChainService; import java.util.function.Function; @@ -86,7 +88,12 @@ public abstract class RuleChainManagerActor extends ContextAwareActor { () -> DefaultActorService.RULE_DISPATCHER_NAME, () -> { RuleChain ruleChain = provider.apply(ruleChainId); - return new RuleChainActor.ActorCreator(systemContext, tenantId, ruleChain); + if (ruleChain == null) { + return new RuleChainErrorActor.ActorCreator(systemContext, tenantId, + new RuleEngineException("Rule Chain with id: " + ruleChainId + " not found!")); + } else { + return new RuleChainActor.ActorCreator(systemContext, tenantId, ruleChain); + } }); } diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/RuleChainErrorActor.java b/application/src/main/java/org/thingsboard/server/actors/shared/RuleChainErrorActor.java new file mode 100644 index 0000000000..a204c7b286 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/actors/shared/RuleChainErrorActor.java @@ -0,0 +1,78 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.actors.shared; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.actors.TbActor; +import org.thingsboard.server.actors.TbActorId; +import org.thingsboard.server.actors.TbStringActorId; +import org.thingsboard.server.actors.service.ContextAwareActor; +import org.thingsboard.server.actors.service.ContextBasedCreator; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.TbActorMsg; +import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg; +import org.thingsboard.server.common.msg.queue.RuleEngineException; + +import java.util.UUID; + +@Slf4j +public class RuleChainErrorActor extends ContextAwareActor { + + private final TenantId tenantId; + private final RuleEngineException error; + + private RuleChainErrorActor(ActorSystemContext systemContext, TenantId tenantId, RuleEngineException error) { + super(systemContext); + this.tenantId = tenantId; + this.error = error; + } + + @Override + protected boolean doProcess(TbActorMsg msg) { + if (msg instanceof RuleChainAwareMsg) { + log.debug("[{}] Reply with {} for message {}", tenantId, error.getMessage(), msg); + var rcMsg = (RuleChainAwareMsg) msg; + rcMsg.getMsg().getCallback().onFailure(error); + return true; + } else { + return false; + } + } + + public static class ActorCreator extends ContextBasedCreator { + + private final TenantId tenantId; + private final RuleEngineException error; + + public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleEngineException error) { + super(context); + this.tenantId = tenantId; + this.error = error; + } + + @Override + public TbActorId createActorId() { + return new TbStringActorId(UUID.randomUUID().toString()); + } + + @Override + public TbActor createActor() { + return new RuleChainErrorActor(context, tenantId, error); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index 1cb794c2ec..a6a49f6b3c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -207,42 +207,222 @@ public class ControllerConstants { protected static final String IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION = "A Boolean value representing the Server SecurityInfo for future Bootstrap client mode settings. Values: 'true' for Bootstrap Server; 'false' for Lwm2m Server. "; - protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION = + protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION = "{\n" + " \"device\": {\n" + - " \"name\": \"LwRpk00000000\",\n" + - " \"type\": \"lwm2mProfileRpk\"\n" + - " },\n" + + " \"name\":\"Name_DeviceWithCredantial_AccessToken\",\n" + + " \"label\":\"Label_DeviceWithCredantial_AccessToken\",\n" + + " \"deviceProfileId\":{\n" + + " \"id\":\"9d9588c0-06c9-11ee-b618-19be30fdeb60\",\n" + + " \"entityType\":\"DEVICE_PROFILE\"\n" + + " }\n" + + " },\n" + " \"credentials\": {\n" + - " \"id\": \"null\",\n" + - " \"createdTime\": 0,\n" + - " \"deviceId\": \"null\",\n" + - " \"credentialsType\": \"LWM2M_CREDENTIALS\",\n" + - " \"credentialsId\": \"LwRpk00000000\",\n" + - " \"credentialsValue\": {\n" + - " \"client\": {\n" + - " \"endpoint\": \"LwRpk00000000\",\n" + - " \"securityConfigClientMode\": \"RPK\",\n" + - " \"key\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\"\n" + - " },\n" + - " \"bootstrap\": {\n" + - " \"bootstrapServer\": {\n" + - " \"securityMode\": \"RPK\",\n" + - " \"clientPublicKeyOrId\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\",\n" + - " \"clientSecretKey\": \"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\"\n" + - " },\n" + - " \"lwm2mServer\": {\n" + - " \"securityMode\": \"RPK\",\n" + - " \"clientPublicKeyOrId\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\",\n" + - " \"clientSecretKey\": \"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\"\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + + " \"credentialsType\": \"ACCESS_TOKEN\",\n" + + " \"credentialsId\": \"6hmxew8pmmzng4e3une2\"\n" + + " }\n" + "}"; - protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN = - MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END; + protected static final String DEVICE_UPDATE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION = + "{\n" + + " \"id\": {\n" + + " \"id\":\"c886a090-168d-11ee-87c9-6f157dbc816a\"\n" + + " },\n" + + " \"deviceId\": {\n" + + " \"id\":\"c5fb3ac0-168d-11ee-87c9-6f157dbc816a\",\n" + + " \"entityType\":\"DEVICE\"\n" + + " },\n" + + " \"credentialsType\": \"ACCESS_TOKEN\",\n" + + " \"credentialsId\": \"6hmxew8pmmzng4e3une4\"\n" + + "}"; + + protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_DEFAULT_PARAM_DESCRIPTION = + "{\n" + + " \"device\": {\n" + + " \"name\":\"Name_DeviceWithCredantial_AccessToken_Default\",\n" + + " \"label\":\"Label_DeviceWithCredantial_AccessToken_Default\",\n" + + " \"type\": \"default\"\n" + + " },\n" + + " \"credentials\": {\n" + + " \"credentialsType\": \"ACCESS_TOKEN\",\n" + + " \"credentialsId\": \"6hmxew8pmmzng4e3une3\"\n" + + " }\n" + + "}"; + + protected static final String certificateValue = "\"-----BEGIN CERTIFICATE----- " + + "MIICMTCCAdegAwIBAgIUI9dBuwN6pTtK6uZ03rkiCwV4wEYwCgYIKoZIzj0EAwIwbjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGluZ3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVByb3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTYxN1oXDTI0MDMyODE0NTYxN1owbjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGluZ3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVByb3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9Zo791qKQiGNBm11r4ZGxh+w+ossZL3xc46ufq5QckQHP7zkD2XDAcmP5GvdkM1sBFN9AWaCkQfNnWmfERsOOKNTMFEwHQYDVR0OBBYEFFFc5uyCyglQoZiKhzXzMcQ3BKORMB8GA1UdIwQYMBaAFFFc5uyCyglQoZiKhzXzMcQ3BKORMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhANbA9CuhoOifZMMmqkpuld+65CR+ItKdXeRAhLMZuccuAiB0FSQB34zMutXrZj1g8Gl5OkE7YryFHbei1z0SveHR8g== " + + "-----END CERTIFICATE-----\""; + + protected static final String certificateId = "\"84f5911765abba1f96bf4165604e9e90338fc6214081a8e623b6ff9669aedb27\""; + + protected static final String certificateValueUpdate = "\"-----BEGIN CERTIFICATE----- " + + "MIICMTCCAdegAwIBAgIUUEKxS9hTz4l+oLUMF0LV6TC/gCIwCgYIKoZIzj0EAwIwbjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGluZ3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlUHJvZmlsZUNlcnRAWDUwOVByb3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTczNloXDTI0MDMyODE0NTczNlowbjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGluZ3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlUHJvZmlsZUNlcnRAWDUwOVByb3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECMlWO72krDoUL9FQjUmSCetkhaEGJUfQkdSfkLSNa0GyAEIMbfmzI4zITeapunu4rGet3EMyLydQzuQanBicp6NTMFEwHQYDVR0OBBYEFHpZ78tPnztNii4Da/yCw6mhEIL3MB8GA1UdIwQYMBaAFHpZ78tPnztNii4Da/yCw6mhEIL3MA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgJ7qyMFqNcwSYkH6o+UlQXzLWfwZbNjVk+aR7foAZNGsCIQDsd7v3WQIGHiArfZeDs1DLEDuV/2h6L+ZNoGNhEKL+1A== " + + "-----END CERTIFICATE-----\""; + + protected static final String certificateIdUpdate = "\"6b8adb49015500e51a527acd332b51684ab9b49b4ade03a9582a44c455e2e9b6\""; + + protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_X509_CERTIFICATE_PARAM_DESCRIPTION = + "{\n" + + " \"device\": {\n" + + " \"name\":\"Name_DeviceWithCredantial_X509_Certificate\",\n" + + " \"label\":\"Label_DeviceWithCredantial_X509_Certificate\",\n" + + " \"deviceProfileId\":{\n" + + " \"id\":\"9d9588c0-06c9-11ee-b618-19be30fdeb60\",\n" + + " \"entityType\":\"DEVICE_PROFILE\"\n" + + " }\n" + + " },\n" + + " \"credentials\": {\n" + + " \"credentialsType\": \"X509_CERTIFICATE\",\n" + + " \"credentialsId\": " + certificateId + ",\n" + + " \"credentialsValue\": " + certificateValue + "\n" + + " }\n" + + "}"; + + protected static final String DEVICE_UPDATE_CREDENTIALS_X509_CERTIFICATE_PARAM_DESCRIPTION = + "{\n" + + " \"id\": {\n" + + " \"id\":\"309bd9c0-14f4-11ee-9fc9-d9b7463abb63\"\n" + + " },\n" + + " \"deviceId\": {\n" + + " \"id\":\"3092b200-14f4-11ee-9fc9-d9b7463abb63\",\n" + + " \"entityType\":\"DEVICE\"\n" + + " },\n" + + " \"credentialsType\": \"X509_CERTIFICATE\",\n" + + " \"credentialsId\": " + certificateIdUpdate + ",\n" + + " \"credentialsValue\": " + certificateValueUpdate + "\n" + + "}"; + + protected static final String MQTT_BASIC_VALUE = "\"{\\\"clientId\\\":\\\"5euh5nzm34bjjh1efmlt\\\",\\\"userName\\\":\\\"onasd1lgwasmjl7v2v7h\\\",\\\"password\\\":\\\"b9xtm4ny8kt9zewaga5o\\\"}\""; + + protected static final String MQTT_BASIC_VALUE_UPDATE = "\"{\\\"clientId\\\":\\\"juy03yv4owqxcmqhqtvk\\\",\\\"userName\\\":\\\"ov19fxca0cyjn7lm7w7u\\\",\\\"password\\\":\\\"twy94he114dfi9usyk1o\\\"}\""; + + protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_MQTT_BASIC_PARAM_DESCRIPTION = + "{\n" + + " \"device\": {\n" + + " \"name\":\"Name_DeviceWithCredantial_MQTT_Basic\",\n" + + " \"label\":\"Label_DeviceWithCredantial_MQTT_Basic\",\n" + + " \"deviceProfileId\":{\n" + + " \"id\":\"9d9588c0-06c9-11ee-b618-19be30fdeb60\",\n" + + " \"entityType\":\"DEVICE_PROFILE\"\n" + + " }\n" + + " },\n" + + " \"credentials\": {\n" + + " \"credentialsType\": \"MQTT_BASIC\",\n" + + " \"credentialsValue\": " + MQTT_BASIC_VALUE + "\n" + + " }\n" + + "}"; + + protected static final String DEVICE_UPDATE_CREDENTIALS_MQTT_BASIC_PARAM_DESCRIPTION = + "{\n" + + " \"id\": {\n" + + " \"id\":\"d877ffb0-14f5-11ee-9fc9-d9b7463abb63\"\n" + + " },\n" + + " \"deviceId\": {\n" + + " \"id\":\"d875dcd0-14f5-11ee-9fc9-d9b7463abb63\",\n" + + " \"entityType\":\"DEVICE\"\n" + + " },\n" + + " \"credentialsType\": \"MQTT_BASIC\",\n" + + " \"credentialsValue\": " + MQTT_BASIC_VALUE_UPDATE + "\n" + + "}"; + + protected static final String CREDENTIALS_VALUE_LVM2M_RPK_DESCRIPTION = + " \"{" + + "\\\"client\\\":{ " + + "\\\"endpoint\\\":\\\"LwRpk00000000\\\", " + + "\\\"securityConfigClientMode\\\":\\\"RPK\\\", " + + "\\\"key\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\"" + + " }, " + + "\\\"bootstrap\\\":{ " + + "\\\"bootstrapServer\\\":{ " + + "\\\"securityMode\\\":\\\"RPK\\\", " + + "\\\"clientPublicKeyOrId\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\", " + + "\\\"clientSecretKey\\\":\\\"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\\\"" + + "}, " + + "\\\"lwm2mServer\\\":{ \\\"securityMode\\\":\\\"RPK\\\", " + + "\\\"clientPublicKeyOrId\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\", " + + "\\\"clientSecretKey\\\":\\\"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\\\"" + + "}" + + "} " + + "}\""; + + protected static final String CREDENTIALS_VALUE_UPDATE_LVM2M_RPK_DESCRIPTION = + " \"{" + + "\\\"client\\\":{ " + + "\\\"endpoint\\\":\\\"LwRpk00000000\\\", " + + "\\\"securityConfigClientMode\\\":\\\"RPK\\\", " + + "\\\"key\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdvBZZ2vQRK9wgDhctj6B1c7bxR3Z0wYg1+YdoYFnVUKWb+rIfTTyYK9tmQJx5Vlb5fxdLnVv1RJOPiwsLIQbAA==\\\"" + + " }, " + + "\\\"bootstrap\\\":{ " + + "\\\"bootstrapServer\\\":{ " + + "\\\"securityMode\\\":\\\"RPK\\\", " + + "\\\"clientPublicKeyOrId\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\", " + + "\\\"clientSecretKey\\\":\\\"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\\\"" + + "}, " + + "\\\"lwm2mServer\\\":{ \\\"securityMode\\\":\\\"RPK\\\", " + + "\\\"clientPublicKeyOrId\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\", " + + "\\\"clientSecretKey\\\":\\\"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\\\"" + + "}" + + "} " + + "}\""; + + protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION = + "{\n" + + " \"device\": {\n" + + " \"name\":\"Name_LwRpk00000000\",\n" + + " \"label\":\"Label_LwRpk00000000\",\n" + + " \"deviceProfileId\":{\n" + + " \"id\":\"a660bd50-10ef-11ee-8737-b5634e73c779\",\n" + + " \"entityType\":\"DEVICE_PROFILE\"\n" + + " }\n" + + " },\n" + + " \"credentials\": {\n" + + " \"credentialsType\": \"LWM2M_CREDENTIALS\",\n" + + " \"credentialsId\": \"LwRpk00000000\",\n" + + " \"credentialsValue\":\n" + CREDENTIALS_VALUE_LVM2M_RPK_DESCRIPTION + "\n" + + " }\n" + + "}"; + + protected static final String DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION = + "{\n" + + " \"id\": {\n" + + " \"id\":\"e238d4d0-1689-11ee-98c6-1713c1be5a8e\"\n" + + " },\n" + + " \"deviceId\": {\n" + + " \"id\":\"e232e160-1689-11ee-98c6-1713c1be5a8e\",\n" + + " \"entityType\":\"DEVICE\"\n" + + " },\n" + + " \"credentialsType\": \"LWM2M_CREDENTIALS\",\n" + + " \"credentialsId\": \"LwRpk00000000\",\n" + + " \"credentialsValue\":\n" + CREDENTIALS_VALUE_UPDATE_LVM2M_RPK_DESCRIPTION + "\n" + + "}"; + + protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN = + MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END; + + protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN = + MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_DEFAULT_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END; + + protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN = + MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_X509_CERTIFICATE_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END; + + protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN = + MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_MQTT_BASIC_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END; + + protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN = + MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION + MARKDOWN_CODE_BLOCK_END; + + protected static final String DEVICE_UPDATE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN = + MARKDOWN_CODE_BLOCK_START + DEVICE_UPDATE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END; + + protected static final String DEVICE_UPDATE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN = + MARKDOWN_CODE_BLOCK_START + DEVICE_UPDATE_CREDENTIALS_X509_CERTIFICATE_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END; + + protected static final String DEVICE_UPDATE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN = + MARKDOWN_CODE_BLOCK_START + DEVICE_UPDATE_CREDENTIALS_MQTT_BASIC_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END; + + protected static final String DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN = + MARKDOWN_CODE_BLOCK_START + DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION + MARKDOWN_CODE_BLOCK_END; + protected static final String FILTER_VALUE_TYPE = NEW_LINE + "## Value Type and Operations" + NEW_LINE + @@ -254,7 +434,7 @@ public class ControllerConstants { " * 'BOOLEAN' - used for boolean values. Operations: EQUAL, NOT_EQUAL;\n" + " * 'DATE_TIME' - similar to numeric, transforms value to milliseconds since epoch. Operations: EQUAL, NOT_EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL; \n"; - protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE = MARKDOWN_CODE_BLOCK_START + + protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "{\n" + " \"schedule\":{\n" + " \"type\":\"SPECIFIC_TIME\",\n" + @@ -269,7 +449,7 @@ public class ControllerConstants { " }\n" + "}" + MARKDOWN_CODE_BLOCK_END; - protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE = MARKDOWN_CODE_BLOCK_START + + protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "{\n" + " \"schedule\":{\n" + " \"type\":\"CUSTOM\",\n" + @@ -321,9 +501,9 @@ public class ControllerConstants { " }\n" + "}" + MARKDOWN_CODE_BLOCK_END; - protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "\"schedule\": null" + MARKDOWN_CODE_BLOCK_END; + protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "\"schedule\": null" + MARKDOWN_CODE_BLOCK_END; - protected static final String DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE = MARKDOWN_CODE_BLOCK_START + + protected static final String DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "{\n" + " \"spec\":{\n" + " \"type\":\"REPEATING\",\n" + @@ -339,7 +519,8 @@ public class ControllerConstants { " }\n" + "}" + MARKDOWN_CODE_BLOCK_END; - protected static final String DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START + + + protected static final String DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "{\n" + " \"spec\":{\n" + " \"type\":\"DURATION\",\n" + diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 100fa8234c..260c1c91e0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -78,6 +78,7 @@ import org.thingsboard.server.service.security.system.SystemSecurityService; import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; import java.net.URISyntaxException; +import javax.validation.Valid; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -97,7 +98,15 @@ import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFI import static org.thingsboard.server.controller.ControllerConstants.DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TYPE_DESCRIPTION; -import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_UPDATE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_UPDATE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_UPDATE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN; import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION; @@ -206,18 +215,29 @@ public class DeviceController extends BaseController { @ApiOperation(value = "Create Device (saveDevice) with credentials ", notes = "Create or update the Device. When creating device, platform generates Device Id as " + UUID_WIKI_LINK + - "Requires to provide the Device Credentials object as well. Useful to create device and credentials in one request. " + - "You may find the example of LwM2M device and RPK credentials below: \n\n" + - DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN + + "Requires to provide the Device Credentials object as well as an existing device profile ID or use \"default\".\n" + + "You may find the example of device with different type of credentials below: \n\n" + + "- Credentials type: \"Access token\" with device profile ID below: \n\n" + + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" + + "- Credentials type: \"Access token\" with device profile default below: \n\n" + + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN + "\n\n" + + "- Credentials type: \"X509\" with device profile ID below: \n\n" + + "Note: credentialsId - format Sha3Hash, certificateValue - format PEM (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" + + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" + + "- Credentials type: \"MQTT_BASIC\" with device profile ID below: \n\n" + + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" + + "- You may find the example of LwM2M device and RPK credentials below: \n\n" + + "Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" + + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" + "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Device entity. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/device-with-credentials", method = RequestMethod.POST) @ResponseBody public Device saveDeviceWithCredentials(@ApiParam(value = "The JSON object with device and credentials. See method description above for example.") - @RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials) throws ThingsboardException { - Device device = checkNotNull(deviceAndCredentials.getDevice()); - DeviceCredentials credentials = checkNotNull(deviceAndCredentials.getCredentials()); + @Valid @RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials) throws ThingsboardException { + Device device = deviceAndCredentials.getDevice(); + DeviceCredentials credentials = deviceAndCredentials.getCredentials(); device.setTenantId(getCurrentUser().getTenantId()); checkEntity(device.getId(), device, Resource.DEVICE); return tbDeviceService.saveDeviceWithCredentials(device, credentials, getCurrentUser()); @@ -301,10 +321,27 @@ public class DeviceController extends BaseController { return tbDeviceService.getDeviceCredentialsByDeviceId(device, getCurrentUser()); } - @ApiOperation(value = "Update device credentials (updateDeviceCredentials)", notes = "During device creation, platform generates random 'ACCESS_TOKEN' credentials. " + - "Use this method to update the device credentials. First use 'getDeviceCredentialsByDeviceId' to get the credentials id and value. " + - "Then use current method to update the credentials type and value. It is not possible to create multiple device credentials for the same device. " + - "The structure of device credentials id and value is simple for the 'ACCESS_TOKEN' but is much more complex for the 'MQTT_BASIC' or 'LWM2M_CREDENTIALS'." + TENANT_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Update device credentials (updateDeviceCredentials)", + notes = "During device creation, platform generates random 'ACCESS_TOKEN' credentials. \" +\n" + + "Use this method to update the device credentials. First use 'getDeviceCredentialsByDeviceId' to get the credentials id and value.\n" + + "Then use current method to update the credentials type and value. It is not possible to create multiple device credentials for the same device.\n" + + "The structure of device credentials id and value is simple for the 'ACCESS_TOKEN' but is much more complex for the 'MQTT_BASIC' or 'LWM2M_CREDENTIALS'.\n" + + "You may find the example of device with different type of credentials below: \n\n" + + "- Credentials type: \"Access token\" with device ID and with device ID below: \n\n" + + DEVICE_UPDATE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" + + "- Credentials type: \"X509\" with device profile ID below: \n\n" + + "Note: credentialsId - format Sha3Hash, certificateValue - format PEM (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" + + DEVICE_UPDATE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" + + "- Credentials type: \"MQTT_BASIC\" with device profile ID below: \n\n" + + DEVICE_UPDATE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" + + "- You may find the example of LwM2M device and RPK credentials below: \n\n" + + "Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" + + DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" + + "Update to real value:\n" + + " - 'id' (this is id of Device Credentials -> \"Get Device Credentials (getDeviceCredentialsByDeviceId)\",\n" + + " - 'deviceId.id' (this is id of Device).\n" + + "Remove 'tenantId' and optionally 'customerId' from the request body example (below) to create new Device entity." + + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/device/credentials", method = RequestMethod.POST) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java index fc85f439e0..386b1eab01 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java @@ -85,6 +85,6 @@ public class EdgeEventController extends BaseController { EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); checkEdgeId(edgeId, Operation.READ); TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); - return checkNotNull(edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, false)); + return checkNotNull(edgeEventService.findEdgeEvents(tenantId, edgeId, 0L, null, pageLink)); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index b7334ebf5a..e7214177d2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -341,7 +341,10 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i sessionNewEvents.put(edgeId, false); Futures.addCallback(session.processEdgeEvents(), new FutureCallback<>() { @Override - public void onSuccess(Void result) { + public void onSuccess(Boolean newEventsAdded) { + if (Boolean.TRUE.equals(newEventsAdded)) { + sessionNewEvents.put(edgeId, true); + } scheduleEdgeEventsCheck(session); } 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 1dd0f31c20..4f0f5d277a 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 @@ -24,6 +24,7 @@ import io.grpc.stub.StreamObserver; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.data.util.Pair; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.edge.Edge; @@ -35,6 +36,8 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg; @@ -68,17 +71,15 @@ import org.thingsboard.server.service.edge.rpc.fetch.GeneralEdgeEventFetcher; import java.io.Closeable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.stream.Collectors; @Slf4j @Data @@ -89,6 +90,7 @@ public final class EdgeGrpcSession implements Closeable { private static final int MAX_DOWNLINK_ATTEMPTS = 10; // max number of attemps to send downlink message if edge connected private static final String QUEUE_START_TS_ATTR_KEY = "queueStartTs"; + private static final String QUEUE_START_SEQ_ID_ATTR_KEY = "queueStartSeqId"; private final UUID sessionId; private final BiConsumer sessionOpenListener; @@ -103,6 +105,12 @@ public final class EdgeGrpcSession implements Closeable { private boolean connected; private boolean syncCompleted; + private Long newStartTs; + private Long previousStartTs; + private Long newStartSeqId; + private Long previousStartSeqId; + private Long seqIdEnd; + private EdgeVersion edgeVersion; private int maxInboundMessageSize; @@ -204,10 +212,10 @@ public final class EdgeGrpcSession implements Closeable { EdgeEventFetcher next = cursor.getNext(); log.info("[{}][{}] starting sync process, cursor current idx = {}, class = {}", edge.getTenantId(), edge.getId(), cursor.getCurrentIdx(), next.getClass().getSimpleName()); - ListenableFuture uuidListenableFuture = startProcessingEdgeEvents(next); - Futures.addCallback(uuidListenableFuture, new FutureCallback<>() { + ListenableFuture> future = startProcessingEdgeEvents(next); + Futures.addCallback(future, new FutureCallback<>() { @Override - public void onSuccess(@Nullable UUID result) { + public void onSuccess(@Nullable Pair result) { doSync(cursor); } @@ -307,36 +315,51 @@ public final class EdgeGrpcSession implements Closeable { sendDownlinkMsg(edgeConfigMsg); } - ListenableFuture processEdgeEvents() throws Exception { - SettableFuture result = SettableFuture.create(); + ListenableFuture processEdgeEvents() throws Exception { + SettableFuture result = SettableFuture.create(); log.trace("[{}] starting processing edge events", this.sessionId); if (isConnected() && isSyncCompleted()) { - Long queueStartTs = getQueueStartTs().get(); + Pair startTsAndSeqId = getQueueStartTsAndSeqId().get(); + this.previousStartTs = startTsAndSeqId.getFirst(); + this.previousStartSeqId = startTsAndSeqId.getSecond(); GeneralEdgeEventFetcher fetcher = new GeneralEdgeEventFetcher( - queueStartTs, + this.previousStartTs, + this.previousStartSeqId, + this.seqIdEnd, + false, + Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()), ctx.getEdgeEventService()); - ListenableFuture ifOffsetFuture = startProcessingEdgeEvents(fetcher); - Futures.addCallback(ifOffsetFuture, new FutureCallback<>() { + Futures.addCallback(startProcessingEdgeEvents(fetcher), new FutureCallback<>() { @Override - public void onSuccess(@Nullable UUID ifOffset) { - if (ifOffset != null) { - Long newStartTs = Uuids.unixTimestamp(ifOffset); - ListenableFuture> updateFuture = updateQueueStartTs(newStartTs); + public void onSuccess(@Nullable Pair newStartTsAndSeqId) { + if (newStartTsAndSeqId != null) { + ListenableFuture> updateFuture = updateQueueStartTsAndSeqId(newStartTsAndSeqId); Futures.addCallback(updateFuture, new FutureCallback<>() { @Override public void onSuccess(@Nullable List list) { - log.debug("[{}] queue offset was updated [{}][{}]", sessionId, ifOffset, newStartTs); - result.set(null); + log.debug("[{}] queue offset was updated [{}]", sessionId, newStartTsAndSeqId); + if (fetcher.isSeqIdNewCycleStarted()) { + seqIdEnd = fetcher.getSeqIdEnd(); + boolean newEventsAvailable = isNewEdgeEventsAvailable(); + result.set(newEventsAvailable); + } else { + seqIdEnd = null; + boolean newEventsAvailable = isSeqIdStartedNewCycle(); + if (!newEventsAvailable) { + newEventsAvailable = isNewEdgeEventsAvailable(); + } + result.set(newEventsAvailable); + } } @Override public void onFailure(Throwable t) { - log.error("[{}] Failed to update queue offset [{}]", sessionId, ifOffset, t); + log.error("[{}] Failed to update queue offset [{}]", sessionId, newStartTsAndSeqId, t); result.setException(t); } }, ctx.getGrpcCallbackExecutorService()); } else { - log.trace("[{}] ifOffset is null. Skipping iteration without db update", sessionId); + log.trace("[{}] newStartTsAndSeqId is null. Skipping iteration without db update", sessionId); result.set(null); } } @@ -354,14 +377,14 @@ public final class EdgeGrpcSession implements Closeable { return result; } - private ListenableFuture startProcessingEdgeEvents(EdgeEventFetcher fetcher) { - SettableFuture result = SettableFuture.create(); + private ListenableFuture> startProcessingEdgeEvents(EdgeEventFetcher fetcher) { + SettableFuture> result = SettableFuture.create(); PageLink pageLink = fetcher.getPageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()); processEdgeEvents(fetcher, pageLink, result); return result; } - private void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture result) { + private void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture> result) { try { PageData pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink); if (isConnected() && !pageData.getData().isEmpty()) { @@ -377,8 +400,15 @@ public final class EdgeGrpcSession implements Closeable { if (isConnected() && pageData.hasNext()) { processEdgeEvents(fetcher, pageLink.nextPageLink(), result); } else { - UUID ifOffset = pageData.getData().get(pageData.getData().size() - 1).getUuidId(); - result.set(ifOffset); + EdgeEvent latestEdgeEvent = pageData.getData().get(pageData.getData().size() - 1); + UUID idOffset = latestEdgeEvent.getUuidId(); + if (idOffset != null) { + Long newStartTs = Uuids.unixTimestamp(idOffset); + long newStartSeqId = latestEdgeEvent.getSeqId(); + result.set(Pair.of(newStartTs, newStartSeqId)); + } else { + result.set(null); + } } } } @@ -461,69 +491,113 @@ public final class EdgeGrpcSession implements Closeable { } } - private DownlinkMsg convertToDownlinkMsg(EdgeEvent edgeEvent) { - log.trace("[{}][{}] converting edge event to downlink msg [{}]", edge.getTenantId(), this.sessionId, edgeEvent); - 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 CREDENTIALS_UPDATED: - case RELATION_ADD_OR_UPDATE: - case RELATION_DELETED: - case ASSIGNED_TO_CUSTOMER: - case UNASSIGNED_FROM_CUSTOMER: - case CREDENTIALS_REQUEST: - case RPC_CALL: - downlinkMsg = convertEntityEventToDownlink(edgeEvent); - log.trace("[{}][{}] entity message processed [{}]", edgeEvent.getTenantId(), 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 [{}]", edge.getTenantId(), this.sessionId, edgeEvent.getAction()); + private List convertToDownlinkMsgsPack(List edgeEvents) { + List result = new ArrayList<>(); + for (EdgeEvent edgeEvent : edgeEvents) { + log.trace("[{}][{}] converting edge event to downlink msg [{}]", edge.getTenantId(), this.sessionId, edgeEvent); + 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 CREDENTIALS_UPDATED: + case RELATION_ADD_OR_UPDATE: + case RELATION_DELETED: + case CREDENTIALS_REQUEST: + case RPC_CALL: + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + downlinkMsg = convertEntityEventToDownlink(edgeEvent); + log.trace("[{}][{}] entity message processed [{}]", edgeEvent.getTenantId(), 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 [{}]", edge.getTenantId(), this.sessionId, edgeEvent.getAction()); + } + } catch (Exception e) { + log.error("[{}][{}] Exception during converting edge event to downlink msg", edge.getTenantId(), this.sessionId, e); + } + if (downlinkMsg != null) { + result.add(downlinkMsg); + } + } + return result; + } + + private ListenableFuture> getQueueStartTsAndSeqId() { + ListenableFuture> future = + ctx.getAttributesService().find(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, Arrays.asList(QUEUE_START_TS_ATTR_KEY, QUEUE_START_SEQ_ID_ATTR_KEY)); + return Futures.transform(future, attributeKvEntries -> { + long startTs = 0L; + long startSeqId = 0L; + for (AttributeKvEntry attributeKvEntry : attributeKvEntries) { + if (QUEUE_START_TS_ATTR_KEY.equals(attributeKvEntry.getKey())) { + startTs = attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L; + } + if (QUEUE_START_SEQ_ID_ATTR_KEY.equals(attributeKvEntry.getKey())) { + startSeqId = attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L; + } + } + if (startSeqId == 0L) { + startSeqId = findStartSeqIdFromOldestEventIfAny(); } + return Pair.of(startTs, startSeqId); + }, ctx.getGrpcCallbackExecutorService()); + } + + private boolean isSeqIdStartedNewCycle() { + try { + TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, this.newStartTs, System.currentTimeMillis()); + PageData edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), 0L, this.previousStartSeqId == 0 ? null : this.previousStartSeqId - 1, pageLink); + return !edgeEvents.getData().isEmpty(); } catch (Exception e) { - log.error("[{}][{}] Exception during converting edge event to downlink msg", edge.getTenantId(), this.sessionId, e); + log.error("[{}][{}][{}] Failed to execute isSeqIdStartedNewCycle", edge.getTenantId(), edge.getId(), sessionId, e); } - return downlinkMsg; + return false; } - private List convertToDownlinkMsgsPack(List edgeEvents) { - return edgeEvents - .stream() - .map(this::convertToDownlinkMsg) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + private boolean isNewEdgeEventsAvailable() { + try { + TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, this.newStartTs, System.currentTimeMillis()); + PageData edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), this.newStartSeqId, null, pageLink); + return !edgeEvents.getData().isEmpty(); + } catch (Exception e) { + log.error("[{}][{}][{}] Failed to execute isNewEdgeEventsAvailable", edge.getTenantId(), edge.getId(), sessionId, e); + } + return false; } - private ListenableFuture getQueueStartTs() { - ListenableFuture> future = - ctx.getAttributesService().find(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, QUEUE_START_TS_ATTR_KEY); - return Futures.transform(future, attributeKvEntryOpt -> { - if (attributeKvEntryOpt != null && attributeKvEntryOpt.isPresent()) { - AttributeKvEntry attributeKvEntry = attributeKvEntryOpt.get(); - return attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L; - } else { - return 0L; + private long findStartSeqIdFromOldestEventIfAny() { + long startSeqId = 0L; + try { + TimePageLink pageLink = new TimePageLink(1, 0, null, new SortOrder("createdTime"), null, null); + PageData edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), null, null, pageLink); + if (!edgeEvents.getData().isEmpty()) { + startSeqId = edgeEvents.getData().get(0).getSeqId() - 1; } - }, ctx.getGrpcCallbackExecutorService()); + } catch (Exception e) { + log.error("[{}][{}][{}] Failed to execute findStartSeqIdFromOldestEventIfAny", edge.getTenantId(), edge.getId(), sessionId, e); + } + return startSeqId; } - private ListenableFuture> updateQueueStartTs(Long newStartTs) { - log.trace("[{}] updating QueueStartTs [{}][{}]", this.sessionId, edge.getId(), newStartTs); - List attributes = Collections.singletonList( - new BaseAttributeKvEntry( - new LongDataEntry(QUEUE_START_TS_ATTR_KEY, newStartTs), System.currentTimeMillis())); + private ListenableFuture> updateQueueStartTsAndSeqId(Pair pair) { + this.newStartTs = pair.getFirst(); + this.newStartSeqId = pair.getSecond(); + log.trace("[{}] updateQueueStartTsAndSeqId [{}][{}][{}]", this.sessionId, edge.getId(), this.newStartTs, this.newStartSeqId); + List attributes = Arrays.asList( + new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_TS_ATTR_KEY, this.newStartTs), System.currentTimeMillis()), + new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_SEQ_ID_ATTR_KEY, this.newStartSeqId), System.currentTimeMillis())); return ctx.getAttributesService().save(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, attributes); } @@ -693,8 +767,11 @@ public final class EdgeGrpcSession implements Closeable { } private void interruptPreviousSendDownlinkMsgsTask() { - log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", edge.getTenantId(), edge.getId(), this.sessionId); - stopCurrentSendDownlinkMsgsTask(true); + if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone() + || sessionState.getScheduledSendDownlinkTask() != null && !sessionState.getScheduledSendDownlinkTask().isCancelled()) { + log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", edge.getTenantId(), edge.getId(), this.sessionId); + stopCurrentSendDownlinkMsgsTask(true); + } } private void interruptGeneralProcessingOnSync() { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java index 69a83da0a0..447a73e5cf 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java @@ -18,7 +18,10 @@ package org.thingsboard.server.service.edge.rpc.constructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityViewId; @@ -47,13 +50,22 @@ public class AlarmMsgConstructor { String entityName = null; switch (alarm.getOriginator().getEntityType()) { case DEVICE: - entityName = deviceService.findDeviceById(tenantId, new DeviceId(alarm.getOriginator().getId())).getName(); + Device deviceById = deviceService.findDeviceById(tenantId, new DeviceId(alarm.getOriginator().getId())); + if (deviceById != null) { + entityName = deviceById.getName(); + } break; case ASSET: - entityName = assetService.findAssetById(tenantId, new AssetId(alarm.getOriginator().getId())).getName(); + Asset assetById = assetService.findAssetById(tenantId, new AssetId(alarm.getOriginator().getId())); + if (assetById != null) { + entityName = assetById.getName(); + } break; case ENTITY_VIEW: - entityName = entityViewService.findEntityViewById(tenantId, new EntityViewId(alarm.getOriginator().getId())).getName(); + EntityView entityViewById = entityViewService.findEntityViewById(tenantId, new EntityViewId(alarm.getOriginator().getId())); + if (entityViewById != null) { + entityName = entityViewById.getName(); + } break; } AlarmUpdateMsg.Builder builder = AlarmUpdateMsg.newBuilder() diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java index 327184e6a9..24008ece09 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java @@ -16,19 +16,27 @@ package org.thingsboard.server.service.edge.rpc.fetch; import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; 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.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.edge.EdgeEventService; @AllArgsConstructor +@Slf4j public class GeneralEdgeEventFetcher implements EdgeEventFetcher { private final Long queueStartTs; + private Long seqIdStart; + @Getter + private Long seqIdEnd; + @Getter + private boolean seqIdNewCycleStarted; + private Long maxReadRecordsCount; private final EdgeEventService edgeEventService; @Override @@ -37,13 +45,32 @@ public class GeneralEdgeEventFetcher implements EdgeEventFetcher { pageSize, 0, null, - new SortOrder("createdTime", SortOrder.Direction.ASC), + null, queueStartTs, - null); + System.currentTimeMillis()); } @Override public PageData fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) { - return edgeEventService.findEdgeEvents(tenantId, edge.getId(), (TimePageLink) pageLink, true); + try { + PageData edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), seqIdStart, seqIdEnd, (TimePageLink) pageLink); + if (edgeEvents.getData().isEmpty()) { + this.seqIdEnd = Math.max(this.maxReadRecordsCount, seqIdStart - this.maxReadRecordsCount); + edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), 0L, seqIdEnd, (TimePageLink) pageLink); + if (edgeEvents.getData().stream().anyMatch(ee -> ee.getSeqId() < seqIdStart)) { + log.info("[{}] seqId column of edge_event table started new cycle [{}]", tenantId, edge.getId()); + this.seqIdNewCycleStarted = true; + this.seqIdStart = 0L; + } else { + edgeEvents = new PageData<>(); + log.warn("[{}] unexpected edge notification message received. " + + "no new events found and seqId column of edge_event table doesn't started new cycle [{}]", tenantId, edge.getId()); + } + } + return edgeEvents; + } catch (Exception e) { + log.error("[{}] failed to find edge events [{}]", tenantId, edge.getId()); + } + return new PageData<>(); } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index f832b04c21..dafccfdad6 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -202,22 +202,27 @@ public class DefaultDataUpdateService implements DataUpdateService { } else { log.info("Skipping audit logs migration"); } - boolean skipEdgeEventsMigration = getEnv("TB_SKIP_EDGE_EVENTS_MIGRATION", false); - if (!skipEdgeEventsMigration) { - log.info("Starting edge events migration. Can be skipped with TB_SKIP_EDGE_EVENTS_MIGRATION env variable set to true"); - edgeEventDao.migrateEdgeEvents(); - } else { - log.info("Skipping edge events migration"); - } + migrateEdgeEvents("Starting edge events migration. "); break; case "3.5.1": log.info("Updating data from version 3.5.1 to 3.5.2 ..."); + migrateEdgeEvents("Starting edge events migration - adding seq_id column. "); break; default: throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion); } } + private void migrateEdgeEvents(String logPrefix) { + boolean skipEdgeEventsMigration = getEnv("TB_SKIP_EDGE_EVENTS_MIGRATION", false); + if (!skipEdgeEventsMigration) { + log.info(logPrefix + "Can be skipped with TB_SKIP_EDGE_EVENTS_MIGRATION env variable set to true"); + edgeEventDao.migrateEdgeEvents(); + } else { + log.info("Skipping edge events migration"); + } + } + @Override public void upgradeRuleNodes() { try { diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java index d69d502aa2..a942dce736 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java @@ -108,6 +108,8 @@ public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor> consumer, Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) { - consumersExecutor.execute(() -> consumerLoop(consumer, configuration, stats, threadSuffix)); + if (isReady) { + consumersExecutor.execute(() -> consumerLoop(consumer, configuration, stats, threadSuffix)); + } else { + scheduleLaunchConsumer(consumer, configuration, stats, threadSuffix); + } + } + + private void scheduleLaunchConsumer(TbQueueConsumer> consumer, Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) { + repartitionExecutor.schedule(() -> { + if (isReady) { + consumersExecutor.execute(() -> consumerLoop(consumer, configuration, stats, threadSuffix)); + } else { + scheduleLaunchConsumer(consumer, configuration, stats, threadSuffix); + } + }, 10, TimeUnit.SECONDS); } void consumerLoop(TbQueueConsumer> consumer, org.thingsboard.server.common.data.queue.Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java index 2d517a2213..b59086a350 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java @@ -68,7 +68,7 @@ public abstract class AbstractConsumerService 0 && attemptIdx > settings.getMaxActorInitAttempts())) { log.info("[{}] Failed to init actor, attempt {}, going to stop attempts.", selfId, attempt, t); stopReason = TbActorStopReason.INIT_FAILED; @@ -88,6 +94,13 @@ public final class TbActorMailbox implements TbActorCtx { } } + private static boolean isUnrecoverable(Throwable t) { + if (t instanceof TbActorException && t.getCause() != null) { + t = t.getCause(); + } + return t instanceof TbActorError && ((TbActorError) t).isUnrecoverable(); + } + private void enqueue(TbActorMsg msg, boolean highPriority) { if (!destroyInProgress.get()) { if (highPriority) { diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java b/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java new file mode 100644 index 0000000000..9db53f86c6 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2023 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.cache.resourceInfo; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.io.Serializable; + +@Getter +@EqualsAndHashCode +@RequiredArgsConstructor +@Builder +public class ResourceInfoCacheKey implements Serializable { + + private final TenantId tenantId; + private final TbResourceId tbResourceId; + + @Override + public String toString() { + return tenantId + "_" + tbResourceId; + } +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCaffeineCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCaffeineCache.java new file mode 100644 index 0000000000..95754d891a --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCaffeineCache.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2023 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.cache.resourceInfo; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.TbResourceInfo; + + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("ResourceInfoCache") +public class ResourceInfoCaffeineCache extends CaffeineTbTransactionalCache { + + public ResourceInfoCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.RESOURCE_INFO_CACHE); + } + +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoEvictEvent.java b/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoEvictEvent.java new file mode 100644 index 0000000000..11272a5e24 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoEvictEvent.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 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.cache.resourceInfo; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +public class ResourceInfoEvictEvent { + private final TenantId tenantId; + private final TbResourceId resourceId; +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoRedisCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoRedisCache.java new file mode 100644 index 0000000000..fee14e1ca1 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoRedisCache.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2023 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.cache.resourceInfo; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbFSTRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.TbResourceInfo; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("ResourceInfoCache") +public class ResourceInfoRedisCache extends RedisTbTransactionalCache { + + public ResourceInfoRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.RESOURCE_INFO_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>()); + } +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeEventService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeEventService.java index dcb3a5232a..9055202f4f 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeEventService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeEventService.java @@ -26,7 +26,7 @@ public interface EdgeEventService { ListenableFuture saveAsync(EdgeEvent edgeEvent); - PageData findEdgeEvents(TenantId tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate); + PageData findEdgeEvents(TenantId tenantId, EdgeId edgeId, Long seqIdStart, Long seqIdEnd, TimePageLink pageLink); /** * Executes stored procedure to cleanup old edge events. diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index ff0032f435..f21b13a674 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -44,4 +44,5 @@ public class CacheConstants { public static final String USER_SETTINGS_CACHE = "userSettings"; public static final String DASHBOARD_TITLES_CACHE = "dashboardTitles"; public static final String ENTITY_COUNT_CACHE = "entityCount"; + public static final String RESOURCE_INFO_CACHE = "resourceInfo"; } 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 f8c74a5155..836a5bff93 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 @@ -20,13 +20,17 @@ import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.security.DeviceCredentials; +import javax.validation.constraints.NotNull; + @ApiModel @Data public class SaveDeviceWithCredentialsRequest { @ApiModelProperty(position = 1, value = "The JSON with device entity.", required = true) + @NotNull private final Device device; @ApiModelProperty(position = 2, value = "The JSON with credentials entity.", required = true) + @NotNull private final DeviceCredentials credentials; } 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 34fec2cd82..c96b55b61d 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 @@ -153,7 +153,6 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha } public static AlarmStatus toStatus(boolean cleared, boolean acknowledged) { - if (cleared) { return acknowledged ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK; } else { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEvent.java index 71c35f4bd8..3688f5c6c2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEvent.java @@ -31,6 +31,7 @@ import java.util.UUID; @ToString(callSuper = true) public class EdgeEvent extends BaseData { + private long seqId; private TenantId tenantId; private EdgeId edgeId; private EdgeEventActionType action; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java index d3af30907d..fd1b4ee0f8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java @@ -42,6 +42,8 @@ public class AlarmNotificationInfo implements RuleOriginatedNotificationInfo { private String alarmOriginatorName; private AlarmSeverity alarmSeverity; private AlarmStatus alarmStatus; + private boolean acknowledged; + private boolean cleared; private CustomerId alarmCustomerId; @Override diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java index 04b5d3fb35..0a39d8d51b 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.msg; +import lombok.Getter; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; @@ -28,7 +29,7 @@ public enum MsgType { * * See {@link PartitionChangeMsg} */ - PARTITION_CHANGE_MSG, + PARTITION_CHANGE_MSG(true), APP_INIT_MSG, @@ -108,7 +109,7 @@ public enum MsgType { * Message that is sent from the Device Actor to Rule Engine. Requires acknowledgement */ - SESSION_TIMEOUT_MSG, + SESSION_TIMEOUT_MSG(true), STATS_PERSIST_TICK_MSG, @@ -130,4 +131,14 @@ public enum MsgType { EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG, EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG; + @Getter + private final boolean ignoreOnStart; + + MsgType() { + this.ignoreOnStart = false; + } + + MsgType(boolean ignoreOnStart) { + this.ignoreOnStart = ignoreOnStart; + } } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbActorError.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbActorError.java new file mode 100644 index 0000000000..fc27feb096 --- /dev/null +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbActorError.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2023 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.msg; + +public interface TbActorError { + + boolean isUnrecoverable(); + +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index f04104bc51..17bc7c0a8f 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java @@ -277,6 +277,11 @@ public final class TbMsg implements Serializable { this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx, callback); } + public TbMsg copyWithNewCtx() { + return new TbMsg(this.queueName, this.id, this.ts, this.type, this.originator, this.customerId, + this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx.copy(), TbMsgCallback.EMPTY); + } + public TbMsgCallback getCallback() { // May be null in case of deserialization; if (callback != null) { diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/RuleChainAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/RuleChainAwareMsg.java index d0e90ae421..5fbc857e0c 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/RuleChainAwareMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/RuleChainAwareMsg.java @@ -17,9 +17,12 @@ package org.thingsboard.server.common.msg.aware; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.msg.TbActorMsg; +import org.thingsboard.server.common.msg.TbMsg; public interface RuleChainAwareMsg extends TbActorMsg { RuleChainId getRuleChainId(); + + TbMsg getMsg(); } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 6c50561363..b337612011 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -15,6 +15,8 @@ */ package org.thingsboard.script.api.tbel; +import com.google.common.primitives.Bytes; +import org.apache.commons.lang3.ArrayUtils; import org.mvel2.ExecutionContext; import org.mvel2.ParserConfiguration; import org.mvel2.execution.ExecutionArrayList; @@ -25,11 +27,13 @@ import org.thingsboard.server.common.data.StringUtils; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; +import java.math.BigInteger; import java.math.RoundingMode; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.Base64; import java.util.Collection; import java.util.List; @@ -61,6 +65,10 @@ public class TbUtils { String.class))); parserConfig.addImport("parseInt", new MethodStub(TbUtils.class.getMethod("parseInt", String.class, int.class))); + parserConfig.addImport("parseLong", new MethodStub(TbUtils.class.getMethod("parseLong", + String.class))); + parserConfig.addImport("parseLong", new MethodStub(TbUtils.class.getMethod("parseLong", + String.class, int.class))); parserConfig.addImport("parseFloat", new MethodStub(TbUtils.class.getMethod("parseFloat", String.class))); parserConfig.addImport("parseDouble", new MethodStub(TbUtils.class.getMethod("parseDouble", @@ -81,8 +89,58 @@ public class TbUtils { byte[].class, int.class, int.class))); parserConfig.addImport("parseBytesToInt", new MethodStub(TbUtils.class.getMethod("parseBytesToInt", byte[].class, int.class, int.class, boolean.class))); + parserConfig.addImport("parseLittleEndianHexToLong", new MethodStub(TbUtils.class.getMethod("parseLittleEndianHexToLong", + String.class))); + parserConfig.addImport("parseBigEndianHexToLong", new MethodStub(TbUtils.class.getMethod("parseBigEndianHexToLong", + String.class))); + parserConfig.addImport("parseHexToLong", new MethodStub(TbUtils.class.getMethod("parseHexToLong", + String.class))); + parserConfig.addImport("parseHexToLong", new MethodStub(TbUtils.class.getMethod("parseHexToLong", + String.class, boolean.class))); + parserConfig.addImport("parseBytesToLong", new MethodStub(TbUtils.class.getMethod("parseBytesToLong", + List.class, int.class, int.class))); + parserConfig.addImport("parseBytesToLong", new MethodStub(TbUtils.class.getMethod("parseBytesToLong", + List.class, int.class, int.class, boolean.class))); + parserConfig.addImport("parseBytesToLong", new MethodStub(TbUtils.class.getMethod("parseBytesToLong", + byte[].class, int.class, int.class))); + parserConfig.addImport("parseBytesToLong", new MethodStub(TbUtils.class.getMethod("parseBytesToLong", + byte[].class, int.class, int.class, boolean.class))); + parserConfig.addImport("parseLittleEndianHexToFloat", new MethodStub(TbUtils.class.getMethod("parseLittleEndianHexToFloat", + String.class))); + parserConfig.addImport("parseBigEndianHexToFloat", new MethodStub(TbUtils.class.getMethod("parseBigEndianHexToFloat", + String.class))); + parserConfig.addImport("parseHexToFloat", new MethodStub(TbUtils.class.getMethod("parseHexToFloat", + String.class))); + parserConfig.addImport("parseHexToFloat", new MethodStub(TbUtils.class.getMethod("parseHexToFloat", + String.class, boolean.class))); + parserConfig.addImport("parseBytesToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesToFloat", + byte[].class, int.class, boolean.class))); + parserConfig.addImport("parseBytesToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesToFloat", + byte[].class, int.class))); + parserConfig.addImport("parseBytesToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesToFloat", + List.class, int.class, boolean.class))); + parserConfig.addImport("parseBytesToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesToFloat", + List.class, int.class))); + parserConfig.addImport("parseLittleEndianHexToDouble", new MethodStub(TbUtils.class.getMethod("parseLittleEndianHexToDouble", + String.class))); + parserConfig.addImport("parseBigEndianHexToDouble", new MethodStub(TbUtils.class.getMethod("parseBigEndianHexToDouble", + String.class))); + parserConfig.addImport("parseHexToDouble", new MethodStub(TbUtils.class.getMethod("parseHexToDouble", + String.class))); + parserConfig.addImport("parseHexToDouble", new MethodStub(TbUtils.class.getMethod("parseHexToDouble", + String.class, boolean.class))); + parserConfig.addImport("parseBytesToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesToDouble", + byte[].class, int.class))); + parserConfig.addImport("parseBytesToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesToDouble", + byte[].class, int.class, boolean.class))); + parserConfig.addImport("parseBytesToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesToDouble", + List.class, int.class))); + parserConfig.addImport("parseBytesToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesToDouble", + List.class, int.class, boolean.class))); parserConfig.addImport("toFixed", new MethodStub(TbUtils.class.getMethod("toFixed", double.class, int.class))); + parserConfig.addImport("toFixed", new MethodStub(TbUtils.class.getMethod("toFixed", + float.class, int.class))); parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes", ExecutionContext.class, String.class))); parserConfig.addImport("base64ToHex", new MethodStub(TbUtils.class.getMethod("base64ToHex", @@ -154,37 +212,73 @@ public class TbUtils { } public static Integer parseInt(String value) { - if (value != null) { + int radix = getRadix(value); + return parseInt(value, radix); + } + + public static Integer parseInt(String value, int radix) { + if (StringUtils.isNotBlank(value)) { try { - int radix = 10; - if (isHexadecimal(value)) { - radix = 16; + String valueP = prepareNumberString(value); + isValidRadix(valueP, radix); + try { + return Integer.parseInt(valueP, radix); + } catch (NumberFormatException e) { + BigInteger bi = new BigInteger(valueP, radix); + if (bi.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) + throw new NumberFormatException("Value \"" + value + "\" is greater than the maximum Integer value " + Integer.MAX_VALUE + " !"); + if (bi.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0) + throw new NumberFormatException("Value \"" + value + "\" is less than the minimum Integer value " + Integer.MIN_VALUE + " !"); + Float f = parseFloat(valueP); + if (f != null) { + return f.intValue(); + } else { + throw new NumberFormatException(e.getMessage()); + } } - return Integer.parseInt(prepareNumberString(value), radix); } catch (NumberFormatException e) { - Float f = parseFloat(value); - if (f != null) { - return f.intValue(); - } + throw new NumberFormatException(e.getMessage()); } } return null; } - public static Integer parseInt(String value, int radix) { - if (value != null) { + public static Long parseLong(String value) { + int radix = getRadix(value); + return parseLong(value, radix); + } + + public static Long parseLong(String value, int radix) { + if (StringUtils.isNotBlank(value)) { try { - return Integer.parseInt(prepareNumberString(value), radix); - } catch (NumberFormatException e) { - Float f = parseFloat(value); - if (f != null) { - return f.intValue(); + String valueP = prepareNumberString(value); + isValidRadix(valueP, radix); + try { + return Long.parseLong(valueP, radix); + } catch (NumberFormatException e) { + BigInteger bi = new BigInteger(valueP, radix); + if (bi.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0) + throw new NumberFormatException("Value \"" + value + "\"is greater than the maximum Long value " + Long.MAX_VALUE + " !"); + if (bi.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) + throw new NumberFormatException("Value \"" + value + "\" is less than the minimum Long value " + Long.MIN_VALUE + " !"); + Double dd = parseDouble(valueP); + if (dd != null) { + return dd.longValue(); + } else { + throw new NumberFormatException(e.getMessage()); + } } + } catch (NumberFormatException e) { + throw new NumberFormatException(e.getMessage()); } } return null; } + private static int getRadix(String value, int... radixS) { + return radixS.length > 0 ? radixS[0] : isHexadecimal(value) ? 16 : 10; + } + public static Float parseFloat(String value) { if (value != null) { try { @@ -218,8 +312,64 @@ public class TbUtils { } public static int parseHexToInt(String hex, boolean bigEndian) { + byte[] data = prepareHexToBytesNumber(hex, 8); + return parseBytesToInt(data, 0, data.length, bigEndian); + } + + public static long parseLittleEndianHexToLong(String hex) { + return parseHexToLong(hex, false); + } + + public static long parseBigEndianHexToLong(String hex) { + return parseHexToLong(hex, true); + } + + public static long parseHexToLong(String hex) { + return parseHexToLong(hex, true); + } + + public static long parseHexToLong(String hex, boolean bigEndian) { + byte[] data = prepareHexToBytesNumber(hex, 16); + return parseBytesToLong(data, 0, data.length, bigEndian); + } + + public static float parseLittleEndianHexToFloat(String hex) { + return parseHexToFloat(hex, false); + } + + public static float parseBigEndianHexToFloat(String hex) { + return parseHexToFloat(hex, true); + } + + public static float parseHexToFloat(String hex) { + return parseHexToFloat(hex, true); + } + + public static float parseHexToFloat(String hex, boolean bigEndian) { + byte[] data = prepareHexToBytesNumber(hex, 8); + return parseBytesToFloat(data, 0, bigEndian); + } + + public static double parseLittleEndianHexToDouble(String hex) { + return parseHexToDouble(hex, false); + } + + public static double parseBigEndianHexToDouble(String hex) { + return parseHexToDouble(hex, true); + } + + public static double parseHexToDouble(String hex) { + return parseHexToDouble(hex, true); + } + + public static double parseHexToDouble(String hex, boolean bigEndian) { + byte[] data = prepareHexToBytesNumber(hex, 16); + return parseBytesToDouble(data, 0, bigEndian); + } + + private static byte[] prepareHexToBytesNumber(String hex, int len) { int length = hex.length(); - if (length > 8) { + if (length > len) { throw new IllegalArgumentException("Hex string is too large. Maximum 8 symbols allowed."); } if (length % 2 > 0) { @@ -229,7 +379,7 @@ public class TbUtils { for (int i = 0; i < length; i += 2) { data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i + 1), 16)); } - return parseBytesToInt(data, 0, data.length, bigEndian); + return data; } public static ExecutionArrayList hexToBytes(ExecutionContext ctx, String hex) { @@ -293,6 +443,91 @@ public class TbUtils { return bb.getInt(); } + public static long parseBytesToLong(List data, int offset, int length) { + return parseBytesToLong(data, offset, length, true); + } + + public static long parseBytesToLong(List data, int offset, int length, boolean bigEndian) { + final byte[] bytes = new byte[data.size()]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = data.get(i); + } + return parseBytesToLong(bytes, offset, length, bigEndian); + } + + public static long parseBytesToLong(byte[] data, int offset, int length) { + return parseBytesToLong(data, offset, length, true); + } + + public static long parseBytesToLong(byte[] data, int offset, int length, boolean bigEndian) { + if (offset > data.length) { + throw new IllegalArgumentException("Offset: " + offset + " is out of bounds for array with length: " + data.length + "!"); + } + if (length > 8) { + throw new IllegalArgumentException("Length: " + length + " is too large. Maximum 4 bytes is allowed!"); + } + if (offset + length > data.length) { + throw new IllegalArgumentException("Offset: " + offset + " and Length: " + length + " is out of bounds for array with length: " + data.length + "!"); + } + var bb = ByteBuffer.allocate(8); + if (!bigEndian) { + bb.order(ByteOrder.LITTLE_ENDIAN); + } + bb.position(bigEndian ? 8 - length : 0); + bb.put(data, offset, length); + bb.position(0); + return bb.getLong(); + } + + public static float parseBytesToFloat(byte[] data, int offset) { + return parseBytesToFloat(data, offset, true); + } + + public static float parseBytesToFloat(List data, int offset) { + return parseBytesToFloat(data, offset, true); + } + + public static float parseBytesToFloat(List data, int offset, boolean bigEndian) { + return parseBytesToFloat(Bytes.toArray(data), offset, bigEndian); + } + + public static float parseBytesToFloat(byte[] data, int offset, boolean bigEndian) { + byte[] bytesToNumber = prepareBytesToNumber(data, offset, 4, bigEndian); + return ByteBuffer.wrap(bytesToNumber).getFloat(); + } + + + public static double parseBytesToDouble(byte[] data, int offset) { + return parseBytesToDouble(data, offset, true); + } + + public static double parseBytesToDouble(List data, int offset) { + return parseBytesToDouble(data, offset, true); + } + + public static double parseBytesToDouble(List data, int offset, boolean bigEndian) { + return parseBytesToDouble(Bytes.toArray(data), offset, bigEndian); + } + + public static double parseBytesToDouble(byte[] data, int offset, boolean bigEndian) { + byte[] bytesToNumber = prepareBytesToNumber(data, offset, 8, bigEndian); + return ByteBuffer.wrap(bytesToNumber).getDouble(); + } + + private static byte[] prepareBytesToNumber(byte[] data, int offset, int length, boolean bigEndian) { + if (offset > data.length) { + throw new IllegalArgumentException("Offset: " + offset + " is out of bounds for array with length: " + data.length + "!"); + } + if ((offset + length) > data.length) { + throw new IllegalArgumentException("Default length is always " + length + " bytes. Offset: " + offset + " and Length: " + length + " is out of bounds for array with length: " + data.length + "!"); + } + byte[] dataBytesArray = Arrays.copyOfRange(data, offset, (offset + length)); + if (!bigEndian) { + ArrayUtils.reverse(dataBytesArray); + } + return dataBytesArray; + } + public static String bytesToHex(ExecutionArrayList bytesList) { byte[] bytes = new byte[bytesList.size()]; for (int i = 0; i < bytesList.size(); i++) { @@ -315,6 +550,10 @@ public class TbUtils { return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).doubleValue(); } + public static float toFixed(float value, int precision) { + return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).floatValue(); + } + private static boolean isHexadecimal(String value) { return value != null && (value.contains("0x") || value.contains("0X")); } @@ -388,4 +627,19 @@ public class TbUtils { } } } + + public static boolean isValidRadix(String value, int radix) { + for (int i = 0; i < value.length(); i++) { + if (i == 0 && value.charAt(i) == '-') { + if (value.length() == 1) + throw new NumberFormatException("Failed radix [" + radix + "] for value: \"" + value + "\"!"); + else + continue; + } + if (Character.digit(value.charAt(i), radix) < 0) + throw new NumberFormatException("Failed radix: [" + radix + "] for value: \"" + value + "\"!"); + } + return true; + } + } diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 82cd74ca30..b6d5395af8 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.script.api.tbel; +import com.google.common.primitives.Bytes; import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Assert; @@ -26,6 +27,7 @@ import org.mvel2.SandboxedParserConfiguration; import org.mvel2.execution.ExecutionArrayList; import org.mvel2.execution.ExecutionHashMap; +import java.math.BigInteger; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Calendar; @@ -38,6 +40,23 @@ public class TbUtilsTest { private ExecutionContext ctx; + private final String intValHex = "41EA62CC"; + private final float floatVal = 29.29824f; + private final String floatValStr = "29.29824"; + + + private final String floatValHexRev = "CC62EA41"; + private final float floatValRev = -5.948442E7f; + + private final long longVal = 0x409B04B10CB295EAL; + private final String longValHex = "409B04B10CB295EA"; + private final long longValRev = 0xEA95B20CB1049B40L; + private final String longValHexRev = "EA95B20CB1049B40"; + private final String doubleValStr = "1729.1729"; + private final double doubleVal = 1729.1729; + private final double doubleValRev = -2.7208640774822924E205; + + @Before public void before() { SandboxedParserConfiguration parserConfig = ParserContext.enableSandboxedMode(); @@ -62,6 +81,7 @@ public class TbUtilsTest { @Test public void parseHexToInt() { Assert.assertEquals(0xAB, TbUtils.parseHexToInt("AB")); + Assert.assertEquals(0xABBA, TbUtils.parseHexToInt("ABBA", true)); Assert.assertEquals(0xBAAB, TbUtils.parseHexToInt("ABBA", false)); Assert.assertEquals(0xAABBCC, TbUtils.parseHexToInt("AABBCC", true)); @@ -182,9 +202,150 @@ public class TbUtilsTest { Assert.assertEquals(expectedMapWithoutPaths, actualMapWithoutPaths); } + @Test + public void parseInt() { + Assert.assertNull(TbUtils.parseInt(null)); + Assert.assertNull(TbUtils.parseInt("")); + Assert.assertNull(TbUtils.parseInt(" ")); + + Assert.assertEquals(java.util.Optional.of(0).get(), TbUtils.parseInt("0")); + Assert.assertEquals(java.util.Optional.of(0).get(), TbUtils.parseInt("-0")); + Assert.assertEquals(java.util.Optional.of(473).get(), TbUtils.parseInt("473")); + Assert.assertEquals(java.util.Optional.of(-255).get(), TbUtils.parseInt("-0xFF")); + Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("FF")); + Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("0xFG")); + + Assert.assertEquals(java.util.Optional.of(102).get(), TbUtils.parseInt("1100110", 2)); + Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("1100210", 2)); + + Assert.assertEquals(java.util.Optional.of(63).get(), TbUtils.parseInt("77", 8)); + Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("18", 8)); + + Assert.assertEquals(java.util.Optional.of(-255).get(), TbUtils.parseInt("-FF", 16)); + Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("FG", 16)); + + + Assert.assertEquals(java.util.Optional.of(Integer.MAX_VALUE).get(), TbUtils.parseInt(Integer.toString(Integer.MAX_VALUE), 10)); + Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt(BigInteger.valueOf(Integer.MAX_VALUE).add(BigInteger.valueOf(1)).toString(10), 10)); + Assert.assertEquals(java.util.Optional.of(Integer.MIN_VALUE).get(), TbUtils.parseInt(Integer.toString(Integer.MIN_VALUE), 10)); + Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt(BigInteger.valueOf(Integer.MIN_VALUE).subtract(BigInteger.valueOf(1)).toString(10), 10)); + + Assert.assertEquals(java.util.Optional.of(506070563).get(), TbUtils.parseInt("KonaIn", 30)); + Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("KonaIn", 10)); + } + + @Test + public void parseFloat() { + Assert.assertEquals(java.util.Optional.of(floatVal).get(), TbUtils.parseFloat(floatValStr)); + } + + @Test + public void toFixedFloat() { + float actualF = TbUtils.toFixed(floatVal, 3); + Assert.assertEquals(1, Float.compare(floatVal, actualF)); + Assert.assertEquals(0, Float.compare(29.298f, actualF)); + } + + @Test + public void parseHexToFloat() { + Assert.assertEquals(0, Float.compare(floatVal, TbUtils.parseHexToFloat(intValHex))); + Assert.assertEquals(0, Float.compare(floatValRev, TbUtils.parseHexToFloat(intValHex, false))); + Assert.assertEquals(0, Float.compare(floatVal, TbUtils.parseBigEndianHexToFloat(intValHex))); + Assert.assertEquals(0, Float.compare(floatVal, TbUtils.parseLittleEndianHexToFloat(floatValHexRev))); + } + + @Test + public void arseBytesToFloat() { + byte[] floatValByte = {65, -22, 98, -52}; + Assert.assertEquals(0, Float.compare(floatVal, TbUtils.parseBytesToFloat(floatValByte, 0))); + Assert.assertEquals(0, Float.compare(floatValRev, TbUtils.parseBytesToFloat(floatValByte, 0, false))); + + List floatVaList = Bytes.asList(floatValByte); + Assert.assertEquals(0, Float.compare(floatVal, TbUtils.parseBytesToFloat(floatVaList, 0))); + Assert.assertEquals(0, Float.compare(floatValRev, TbUtils.parseBytesToFloat(floatVaList, 0, false))); + } + + @Test + public void parseLong() { + Assert.assertNull(TbUtils.parseLong(null)); + Assert.assertNull(TbUtils.parseLong("")); + Assert.assertNull(TbUtils.parseLong(" ")); + + Assert.assertEquals(java.util.Optional.of(0L).get(), TbUtils.parseLong("0")); + Assert.assertEquals(java.util.Optional.of(0L).get(), TbUtils.parseLong("-0")); + Assert.assertEquals(java.util.Optional.of(473L).get(), TbUtils.parseLong("473")); + Assert.assertEquals(java.util.Optional.of(-65535L).get(), TbUtils.parseLong("-0xFFFF")); + Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("FFFFFFFF")); + Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("0xFGFFFFFF")); + + Assert.assertEquals(java.util.Optional.of(13158L).get(), TbUtils.parseLong("11001101100110", 2)); + Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("11001101100210", 2)); + + Assert.assertEquals(java.util.Optional.of(9223372036854775807L).get(), TbUtils.parseLong("777777777777777777777", 8)); + Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("1787", 8)); - private static String keyToValue(String key, String extraSymbol) { - return key + "Value" + (extraSymbol == null ? "" : extraSymbol); + Assert.assertEquals(java.util.Optional.of(-255L).get(), TbUtils.parseLong("-FF", 16)); + Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("FG", 16)); + + + Assert.assertEquals(java.util.Optional.of(Long.MAX_VALUE).get(), TbUtils.parseLong(Long.toString(Long.MAX_VALUE), 10)); + Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.valueOf(1)).toString(10), 10)); + Assert.assertEquals(java.util.Optional.of(Long.MIN_VALUE).get(), TbUtils.parseLong(Long.toString(Long.MIN_VALUE), 10)); + Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong(BigInteger.valueOf(Long.MIN_VALUE).subtract(BigInteger.valueOf(1)).toString(10), 10)); + + Assert.assertEquals(java.util.Optional.of(218840926543L).get(), TbUtils.parseLong("KonaLong", 27)); + Assert.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("KonaLong", 10)); + } + + @Test + public void parseHexToLong() { + Assert.assertEquals(longVal, TbUtils.parseHexToLong(longValHex)); + Assert.assertEquals(longVal, TbUtils.parseHexToLong(longValHexRev, false)); + Assert.assertEquals(longVal, TbUtils.parseBigEndianHexToLong(longValHex)); + Assert.assertEquals(longVal, TbUtils.parseLittleEndianHexToLong(longValHexRev)); + } + + @Test + public void parseBytesToLong() { + byte[] longValByte = {64, -101, 4, -79, 12, -78, -107, -22}; + Assert.assertEquals(longVal, TbUtils.parseBytesToLong(longValByte, 0, 8)); + Bytes.reverse(longValByte); + Assert.assertEquals(longVal, TbUtils.parseBytesToLong(longValByte, 0, 8, false)); + + List longVaList = Bytes.asList(longValByte); + Assert.assertEquals(longVal, TbUtils.parseBytesToLong(longVaList, 0, 8, false)); + Assert.assertEquals(longValRev, TbUtils.parseBytesToLong(longVaList, 0, 8)); + } + + @Test + public void parsDouble() { + Assert.assertEquals(java.util.Optional.of(doubleVal).get(), TbUtils.parseDouble(doubleValStr)); + } + + @Test + public void toFixedDouble() { + double actualD = TbUtils.toFixed(doubleVal, 3); + Assert.assertEquals(-1, Double.compare(doubleVal, actualD)); + Assert.assertEquals(0, Double.compare(1729.173, actualD)); + } + + @Test + public void parseHexToDouble() { + Assert.assertEquals(0, Double.compare(doubleVal, TbUtils.parseHexToDouble(longValHex))); + Assert.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseHexToDouble(longValHex, false))); + Assert.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBigEndianHexToDouble(longValHex))); + Assert.assertEquals(0, Double.compare(doubleVal, TbUtils.parseLittleEndianHexToDouble(longValHexRev))); + } + + @Test + public void parseBytesToDouble() { + byte[] doubleValByte = {64, -101, 4, -79, 12, -78, -107, -22}; + Assert.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBytesToDouble(doubleValByte, 0))); + Assert.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleValByte, 0, false))); + + List doubleVaList = Bytes.asList(doubleValByte); + Assert.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBytesToDouble(doubleVaList, 0))); + Assert.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleVaList, 0, false))); } private static List toList(byte[] data) { @@ -194,5 +355,5 @@ public class TbUtilsTest { } return result; } - } + 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 9d0f9c38ac..91d591171f 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 @@ -236,10 +236,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findEdgeEvents(TenantId tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate) { - return edgeEventDao.findEdgeEvents(tenantId.getId(), edgeId, pageLink, withTsUpdate); + public PageData findEdgeEvents(TenantId tenantId, EdgeId edgeId, Long seqIdStart, Long seqIdEnd, TimePageLink pageLink) { + return edgeEventDao.findEdgeEvents(tenantId.getId(), edgeId, seqIdStart, seqIdEnd, pageLink); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeEventDao.java index 84bf8c40d2..942a536674 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeEventDao.java @@ -43,10 +43,12 @@ public interface EdgeEventDao extends Dao { * * @param tenantId the tenantId * @param edgeId the edgeId + * @param seqIdStart the seq id start + * @param seqIdEnd the seq id end * @param pageLink the pageLink * @return the event list */ - PageData findEdgeEvents(UUID tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate); + PageData findEdgeEvents(UUID tenantId, EdgeId edgeId, Long seqIdStart, Long seqIdEnd, TimePageLink pageLink); /** * Executes stored procedure to cleanup old edge 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 102d5f181e..552f83c749 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 @@ -535,6 +535,7 @@ public class ModelConstants { */ public static final String EDGE_EVENT_TABLE_NAME = "edge_event"; public static final String EDGE_EVENT_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + public static final String EDGE_EVENT_SEQUENTIAL_ID_PROPERTY = "seq_id"; public static final String EDGE_EVENT_EDGE_ID_PROPERTY = "edge_id"; public static final String EDGE_EVENT_TYPE_PROPERTY = "edge_event_type"; public static final String EDGE_EVENT_ACTION_PROPERTY = "edge_event_action"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java index 1edc47f197..55a30383d1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java @@ -43,6 +43,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_BODY_PR import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_EDGE_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_ENTITY_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_SEQUENTIAL_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TENANT_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TYPE_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_UID_PROPERTY; @@ -57,6 +58,9 @@ import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN; @NoArgsConstructor public class EdgeEventEntity extends BaseSqlEntity implements BaseEntity { + @Column(name = EDGE_EVENT_SEQUENTIAL_ID_PROPERTY) + protected long seqId; + @Column(name = EDGE_EVENT_TENANT_ID_PROPERTY) private UUID tenantId; @@ -120,6 +124,7 @@ public class EdgeEventEntity extends BaseSqlEntity implements BaseEnt edgeEvent.setAction(edgeEventAction); edgeEvent.setBody(entityBody); edgeEvent.setUid(edgeEventUid); + edgeEvent.setSeqId(seqId); return edgeEvent; } 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 974e3665f1..bc4f47040b 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 @@ -20,7 +20,11 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.server.cache.device.DeviceCacheKey; +import org.thingsboard.server.cache.resourceInfo.ResourceInfoEvictEvent; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.cache.resourceInfo.ResourceInfoCacheKey; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; @@ -31,6 +35,7 @@ import org.thingsboard.server.common.data.id.TbResourceId; 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.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -45,7 +50,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId; @Service("TbResourceDaoService") @Slf4j @AllArgsConstructor -public class BaseResourceService implements ResourceService { +public class BaseResourceService extends AbstractCachedEntityService implements ResourceService { public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId "; private final TbResourceDao resourceDao; @@ -55,10 +60,12 @@ public class BaseResourceService implements ResourceService { @Override public TbResource saveResource(TbResource resource) { resourceValidator.validate(resource, TbResourceInfo::getTenantId); - try { - return resourceDao.save(resource.getTenantId(), resource); + TbResource saved = resourceDao.save(resource.getTenantId(), resource); + publishEvictEvent(new ResourceInfoEvictEvent(resource.getTenantId(), resource.getId())); + return saved; } catch (Exception t) { + publishEvictEvent(new ResourceInfoEvictEvent(resource.getTenantId(), resource.getId())); ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("resource_unq_key")) { String field = ResourceType.LWM2M_MODEL.equals(resource.getResourceType()) ? "resourceKey" : "fileName"; @@ -86,7 +93,9 @@ public class BaseResourceService implements ResourceService { public TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId) { log.trace("Executing findResourceInfoById [{}] [{}]", tenantId, resourceId); Validator.validateId(resourceId, INCORRECT_RESOURCE_ID + resourceId); - return resourceInfoDao.findById(tenantId, resourceId.getId()); + + return cache.getAndPutInTransaction(new ResourceInfoCacheKey(tenantId, resourceId), + () -> resourceInfoDao.findById(tenantId, resourceId.getId()), true); } @Override @@ -169,13 +178,11 @@ public class BaseResourceService implements ResourceService { } }; - protected Optional extractConstraintViolationException(Exception t) { - if (t instanceof ConstraintViolationException) { - return Optional.of((ConstraintViolationException) t); - } else if (t.getCause() instanceof ConstraintViolationException) { - return Optional.of((ConstraintViolationException) (t.getCause())); - } else { - return Optional.empty(); + @TransactionalEventListener(classes = ResourceInfoEvictEvent.class) + @Override + public void handleEvictEvent(ResourceInfoEvictEvent event) { + if (event.getResourceId() != null) { + cache.evict(new ResourceInfoCacheKey(event.getTenantId(), event.getResourceId())); } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventRepository.java index c4827f1ffe..c3a9697ad6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventRepository.java @@ -30,8 +30,10 @@ public interface EdgeEventRepository extends JpaRepository :startTime) " + + "AND (:startTime IS NULL OR e.createdTime >= :startTime) " + "AND (:endTime IS NULL OR e.createdTime <= :endTime) " + + "AND (:seqIdStart IS NULL OR e.seqId > :seqIdStart) " + + "AND (:seqIdEnd IS NULL OR e.seqId < :seqIdEnd) " + "AND LOWER(e.edgeEventType) LIKE LOWER(CONCAT('%', :textSearch, '%'))" ) Page findEdgeEventsByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, @@ -39,20 +41,7 @@ public interface EdgeEventRepository extends JpaRepository :startTime) " + - "AND (:endTime IS NULL OR e.createdTime <= :endTime) " + - "AND e.edgeEventAction <> 'TIMESERIES_UPDATED' " + - "AND LOWER(e.edgeEventType) LIKE LOWER(CONCAT('%', :textSearch, '%'))" - ) - Page findEdgeEventsByTenantIdAndEdgeIdWithoutTimeseriesUpdated(@Param("tenantId") UUID tenantId, - @Param("edgeId") UUID edgeId, - @Param("textSearch") String textSearch, - @Param("startTime") Long startTime, - @Param("endTime") Long endTime, - Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java index bb825f504d..9f2eaae273 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.EdgeEventId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; @@ -43,7 +44,9 @@ import org.thingsboard.server.dao.util.SqlDao; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; +import java.util.ArrayList; import java.util.Comparator; +import java.util.List; import java.util.Objects; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -118,7 +121,7 @@ public class JpaBaseEdgeEventDao extends JpaAbstractDao(params, hashcodeFunction, 1, statsFactory); - queue.init(logExecutor, v -> edgeEventInsertRepository.save(v), + queue.init(logExecutor, edgeEventInsertRepository::save, Comparator.comparing(EdgeEventEntity::getTs) ); } @@ -171,29 +174,23 @@ public class JpaBaseEdgeEventDao extends JpaAbstractDao findEdgeEvents(UUID tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate) { - if (withTsUpdate) { - return DaoUtil.toPageData( - edgeEventRepository - .findEdgeEventsByTenantIdAndEdgeId( - tenantId, - edgeId.getId(), - Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getStartTime(), - pageLink.getEndTime(), - DaoUtil.toPageable(pageLink))); - } else { - return DaoUtil.toPageData( - edgeEventRepository - .findEdgeEventsByTenantIdAndEdgeIdWithoutTimeseriesUpdated( - tenantId, - edgeId.getId(), - Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getStartTime(), - pageLink.getEndTime(), - DaoUtil.toPageable(pageLink))); - + public PageData findEdgeEvents(UUID tenantId, EdgeId edgeId, Long seqIdStart, Long seqIdEnd, TimePageLink pageLink) { + List sortOrders = new ArrayList<>(); + if (pageLink.getSortOrder() != null) { + sortOrders.add(pageLink.getSortOrder()); } + sortOrders.add(new SortOrder("seqId")); + return DaoUtil.toPageData( + edgeEventRepository + .findEdgeEventsByTenantIdAndEdgeId( + tenantId, + edgeId.getId(), + Objects.toString(pageLink.getTextSearch(), ""), + pageLink.getStartTime(), + pageLink.getEndTime(), + seqIdStart, + seqIdEnd, + DaoUtil.toPageable(pageLink, sortOrders))); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java index d7b11dc1bf..078984c06d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2023 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.util; import org.thingsboard.common.util.JacksonUtil; diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 7fe3ec6e67..bfb2eed805 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -720,6 +720,7 @@ CREATE TABLE IF NOT EXISTS edge ( ); CREATE TABLE IF NOT EXISTS edge_event ( + seq_id INT GENERATED ALWAYS AS IDENTITY, id uuid NOT NULL, created_time bigint NOT NULL, edge_id uuid, @@ -731,6 +732,7 @@ CREATE TABLE IF NOT EXISTS edge_event ( tenant_id uuid, ts bigint NOT NULL ) PARTITION BY RANGE(created_time); +ALTER TABLE IF EXISTS edge_event ALTER COLUMN seq_id SET CYCLE; CREATE TABLE IF NOT EXISTS rpc ( id uuid NOT NULL CONSTRAINT rpc_pkey PRIMARY KEY, diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EdgeEventServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EdgeEventServiceTest.java index 63958fe1e4..26e7cc2e1f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EdgeEventServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EdgeEventServiceTest.java @@ -71,7 +71,7 @@ public class EdgeEventServiceTest extends AbstractServiceTest { EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, deviceId, EdgeEventActionType.ADDED); edgeEventService.saveAsync(edgeEvent).get(); - PageData edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, new TimePageLink(1), false); + PageData edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, 0L, null, new TimePageLink(1)); Assert.assertFalse(edgeEvents.getData().isEmpty()); EdgeEvent saved = edgeEvents.getData().get(0); @@ -113,7 +113,7 @@ public class EdgeEventServiceTest extends AbstractServiceTest { Futures.allAsList(futures).get(); TimePageLink pageLink = new TimePageLink(2, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), startTime, endTime); - PageData edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, true); + PageData edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, 0L, null, pageLink); Assert.assertNotNull(edgeEvents.getData()); Assert.assertEquals(2, edgeEvents.getData().size()); @@ -122,7 +122,7 @@ public class EdgeEventServiceTest extends AbstractServiceTest { Assert.assertTrue(edgeEvents.hasNext()); Assert.assertNotNull(pageLink.nextPageLink()); - edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink.nextPageLink(), true); + edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, 0L, null, pageLink.nextPageLink()); Assert.assertNotNull(edgeEvents.getData()); Assert.assertEquals(1, edgeEvents.getData().size()); @@ -132,26 +132,6 @@ public class EdgeEventServiceTest extends AbstractServiceTest { edgeEventService.cleanupEvents(1); } - @Test - public void findEdgeEventsWithTsUpdateAndWithout() throws Exception { - EdgeId edgeId = new EdgeId(Uuids.timeBased()); - DeviceId deviceId = new DeviceId(Uuids.timeBased()); - TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); - TimePageLink pageLink = new TimePageLink(1, 0, null, new SortOrder("createdTime", SortOrder.Direction.ASC)); - - EdgeEvent edgeEventWithTsUpdate = generateEdgeEvent(tenantId, edgeId, deviceId, EdgeEventActionType.TIMESERIES_UPDATED); - edgeEventService.saveAsync(edgeEventWithTsUpdate).get(); - - PageData allEdgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, true); - PageData edgeEventsWithoutTsUpdate = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, false); - - Assert.assertNotNull(allEdgeEvents.getData()); - Assert.assertNotNull(edgeEventsWithoutTsUpdate.getData()); - Assert.assertEquals(1, allEdgeEvents.getData().size()); - Assert.assertEquals(allEdgeEvents.getData().get(0).getUuidId(), edgeEventWithTsUpdate.getUuidId()); - Assert.assertTrue(edgeEventsWithoutTsUpdate.getData().isEmpty()); - } - private ListenableFuture saveEdgeEventWithProvidedTime(long time, EdgeId edgeId, EntityId entityId, TenantId tenantId) throws Exception { EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, entityId, EdgeEventActionType.ADDED); edgeEvent.setId(new EdgeEventId(Uuids.startOf(time))); diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index d89211cb2f..98f9091318 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -74,6 +74,9 @@ cache.specs.dashboardTitles.maxSize=10000 cache.specs.entityCount.timeToLiveInMinutes=1440 cache.specs.entityCount.maxSize=10000 +cache.specs.resourceInfo.timeToLiveInMinutes=1440 +cache.specs.resourceInfo.maxSize=10000 + redis.connection.host=localhost redis.connection.port=6379 redis.connection.db=0 diff --git a/dao/src/test/resources/sql/system-test-psql.sql b/dao/src/test/resources/sql/system-test-psql.sql index 172731b9c5..21af327f13 100644 --- a/dao/src/test/resources/sql/system-test-psql.sql +++ b/dao/src/test/resources/sql/system-test-psql.sql @@ -1,2 +1,5 @@ --PostgreSQL specific truncate to fit constraints -TRUNCATE TABLE device_credentials, device, device_profile, asset, asset_profile, ota_package, rule_node_state, rule_node, rule_chain, alarm_comment, alarm, entity_alarm; \ No newline at end of file +TRUNCATE TABLE device_credentials, device, device_profile, asset, asset_profile, ota_package, rule_node_state, rule_node, rule_chain, alarm_comment, alarm, entity_alarm; + +-- Decrease seq_id column to make sure to cover cases of new sequential cycle during the tests +ALTER SEQUENCE edge_event_seq_id_seq MAXVALUE 256; diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 1561dac05e..88bf80e9f9 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -167,6 +167,8 @@ public interface TbContext { void enqueueForTellFailure(TbMsg msg, String failureMessage); + void enqueueForTellFailure(TbMsg tbMsg, Throwable t); + void enqueueForTellNext(TbMsg msg, String relationType); void enqueueForTellNext(TbMsg msg, Set relationTypes); @@ -210,7 +212,7 @@ public interface TbContext { void schedule(Runnable runnable, long delay, TimeUnit timeUnit); - void checkTenantEntity(EntityId entityId); + void checkTenantEntity(EntityId entityId) throws TbNodeException; boolean isLocalEntity(EntityId entityId); @@ -302,6 +304,8 @@ public interface TbContext { SlackService getSlackService(); + boolean isExternalNodeForceAck(); + /** * Creates JS Script Engine * @deprecated diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java index 32341dbf2b..a8ce9d7e42 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java @@ -15,17 +15,33 @@ */ package org.thingsboard.rule.engine.api; +import lombok.Getter; +import org.thingsboard.server.common.msg.TbActorError; + /** * Created by ashvayka on 19.01.18. */ -public class TbNodeException extends Exception { +public class TbNodeException extends Exception implements TbActorError { + + @Getter + private final boolean unrecoverable; public TbNodeException(String message) { + this(message, false); + } + + public TbNodeException(String message, boolean unrecoverable) { super(message); + this.unrecoverable = unrecoverable; } public TbNodeException(Exception e) { + this(e, false); + } + + public TbNodeException(Exception e, boolean unrecoverable) { super(e); + this.unrecoverable = unrecoverable; } } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java index 45ac5f30f5..bbba1855c2 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java @@ -44,7 +44,7 @@ public class TbNodeUtils { try { return JacksonUtil.treeToValue(configuration.getData(), clazz); } catch (IllegalArgumentException e) { - throw new TbNodeException(e); + throw new TbNodeException(e, true); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java index bf146a52fb..84e82b76e8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java @@ -66,7 +66,7 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode ctx.tellFailure(processException(ctx, msg, t), t)); + m -> tellSuccess(ctx, m), + t -> tellFailure(ctx, processException(ctx, msg, t), t)); + ackIfNeeded(ctx, msg); } private ListenableFuture publishMessageAsync(TbContext ctx, TbMsg msg) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java index 7386d30c7c..f8ebc8e295 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java @@ -31,6 +31,7 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -55,7 +56,7 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; configDirective = "tbExternalNodeSqsConfig", iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+" ) -public class TbSqsNode implements TbNode { +public class TbSqsNode extends TbAbstractExternalNode { private static final String MESSAGE_ID = "messageId"; private static final String REQUEST_ID = "requestId"; @@ -69,6 +70,7 @@ public class TbSqsNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + super.init(ctx); this.config = TbNodeUtils.convert(configuration, TbSqsNodeConfiguration.class); AWSCredentials awsCredentials = new BasicAWSCredentials(this.config.getAccessKeyId(), this.config.getSecretAccessKey()); AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials); @@ -85,8 +87,9 @@ public class TbSqsNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { withCallback(publishMessageAsync(ctx, msg), - ctx::tellSuccess, - t -> ctx.tellFailure(processException(ctx, msg, t), t)); + m -> tellSuccess(ctx, m), + t -> tellFailure(ctx, processException(ctx, msg, t), t)); + ackIfNeeded(ctx, msg); } private ListenableFuture publishMessageAsync(TbContext ctx, TbMsg msg) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java new file mode 100644 index 0000000000..f5f8c34ab3 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2023 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.rule.engine.external; + +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.server.common.msg.TbMsg; + +public abstract class TbAbstractExternalNode implements TbNode { + + private boolean forceAck; + + public void init(TbContext ctx) { + this.forceAck = ctx.isExternalNodeForceAck(); + } + + protected void tellSuccess(TbContext ctx, TbMsg tbMsg) { + if (forceAck) { + ctx.enqueueForTellNext(tbMsg.copyWithNewCtx(), TbRelationTypes.SUCCESS); + } else { + ctx.tellSuccess(tbMsg); + } + } + + protected void tellFailure(TbContext ctx, TbMsg tbMsg, Throwable t) { + if (forceAck) { + if (t == null) { + ctx.enqueueForTellNext(tbMsg.copyWithNewCtx(), TbRelationTypes.FAILURE); + } else { + ctx.enqueueForTellFailure(tbMsg.copyWithNewCtx(), t); + } + } else { + if (t == null) { + ctx.tellNext(tbMsg, TbRelationTypes.FAILURE); + } else { + ctx.tellFailure(tbMsg, t); + } + } + } + + protected void ackIfNeeded(TbContext ctx, TbMsg msg) { + if (forceAck) { + ctx.ack(msg); + } + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java index b0198e210c..3b927f05a6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java @@ -32,6 +32,7 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -53,7 +54,7 @@ import java.util.concurrent.TimeUnit; configDirective = "tbExternalNodePubSubConfig", iconUrl = "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMTI4IDEyOCI+Cjx0aXRsZT5DbG91ZCBQdWJTdWI8L3RpdGxlPgo8Zz4KPHBhdGggZD0iTTEyNi40Nyw1OC4xMmwtMjYuMy00NS43NEExMS41NiwxMS41NiwwLDAsMCw5MC4zMSw2LjVIMzcuN2ExMS41NSwxMS41NSwwLDAsMC05Ljg2LDUuODhMMS41Myw1OGExMS40OCwxMS40OCwwLDAsMCwwLDExLjQ0bDI2LjMsNDZhMTEuNzcsMTEuNzcsMCwwLDAsOS44Niw2LjA5SDkwLjNhMTEuNzMsMTEuNzMsMCwwLDAsOS44Ny02LjA2bDI2LjMtNDUuNzRBMTEuNzMsMTEuNzMsMCwwLDAsMTI2LjQ3LDU4LjEyWiIgc3R5bGU9ImZpbGw6ICM3MzViMmYiLz4KPHBhdGggZD0iTTg5LjIyLDQ3Ljc0LDgzLjM2LDQ5bC0xNC42LTE0LjZMNjQuMDksNDMuMSw2MS41NSw1My4ybDQuMjksNC4yOUw1Ny42LDU5LjE4LDQ2LjMsNDcuODhsLTcuNjcsNy4zOEw1Mi43Niw2OS4zN2wtMTUsMTEuOUw3OCwxMjEuNUg5MC4zYTExLjczLDExLjczLDAsMCwwLDkuODctNi4wNmwyMC43Mi0zNloiIHN0eWxlPSJvcGFjaXR5OiAwLjA3MDAwMDAwMDI5ODAyMztpc29sYXRpb246IGlzb2xhdGUiLz4KPHBhdGggZD0iTTgyLjg2LDQ3YTUuMzIsNS4zMiwwLDEsMS0xLjk1LDcuMjdBNS4zMiw1LjMyLDAsMCwxLDgyLjg2LDQ3IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNMzkuODIsNTYuMThhNS4zMiw1LjMyLDAsMSwxLDcuMjctMS45NSw1LjMyLDUuMzIsMCwwLDEtNy4yNywxLjk1IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNNjkuMzIsODguODVBNS4zMiw1LjMyLDAsMSwxLDY0LDgzLjUyYTUuMzIsNS4zMiwwLDAsMSw1LjMyLDUuMzIiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxnPgo8cGF0aCBkPSJNNjQsNTIuOTRhMTEuMDYsMTEuMDYsMCwwLDEsMi40Ni4yOFYzOS4xNUg2MS41NFY1My4yMkExMS4wNiwxMS4wNiwwLDAsMSw2NCw1Mi45NFoiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxwYXRoIGQ9Ik03NC41Nyw2Ny4yNmExMSwxMSwwLDAsMS0yLjQ3LDQuMjVsMTIuMTksNywyLjQ2LTQuMjZaIiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNNTMuNDMsNjcuMjZsLTEyLjE4LDcsMi40Niw0LjI2LDEyLjE5LTdBMTEsMTEsMCwwLDEsNTMuNDMsNjcuMjZaIiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8L2c+CjxwYXRoIGQ9Ik03Mi42LDY0QTguNiw4LjYsMCwxLDEsNjQsNTUuNCw4LjYsOC42LDAsMCwxLDcyLjYsNjQiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxwYXRoIGQ9Ik0zOS4xLDcwLjU3YTYuNzYsNi43NiwwLDEsMS0yLjQ3LDkuMjMsNi43Niw2Ljc2LDAsMCwxLDIuNDctOS4yMyIgc3R5bGU9ImZpbGw6ICNmZmYiLz4KPHBhdGggZD0iTTgyLjE0LDgyLjI3YTYuNzYsNi43NiwwLDEsMSw5LjIzLTIuNDcsNi43NSw2Ljc1LDAsMCwxLTkuMjMsMi40NyIgc3R5bGU9ImZpbGw6ICNmZmYiLz4KPHBhdGggZD0iTTcwLjc2LDM5LjE1QTYuNzYsNi43NiwwLDEsMSw2NCwzMi4zOWE2Ljc2LDYuNzYsMCwwLDEsNi43Niw2Ljc2IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8L2c+Cjwvc3ZnPgo=" ) -public class TbPubSubNode implements TbNode { +public class TbPubSubNode extends TbAbstractExternalNode { private static final String MESSAGE_ID = "messageId"; private static final String ERROR = "error"; @@ -63,8 +64,9 @@ public class TbPubSubNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + super.init(ctx); + this.config = TbNodeUtils.convert(configuration, TbPubSubNodeConfiguration.class); try { - this.config = TbNodeUtils.convert(configuration, TbPubSubNodeConfiguration.class); this.pubSubClient = initPubSubClient(); } catch (Exception e) { throw new TbNodeException(e); @@ -74,6 +76,7 @@ public class TbPubSubNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { publishMessage(ctx, msg); + ackIfNeeded(ctx, msg); } @Override @@ -101,12 +104,12 @@ public class TbPubSubNode implements TbNode { ApiFutures.addCallback(messageIdFuture, new ApiFutureCallback() { public void onSuccess(String messageId) { TbMsg next = processPublishResult(ctx, msg, messageId); - ctx.tellSuccess(next); + tellSuccess(ctx, next); } public void onFailure(Throwable t) { TbMsg next = processException(ctx, msg, t); - ctx.tellFailure(next, t); + tellFailure(ctx, next, t); } }, ctx.getExternalCallExecutor()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java index de1abea224..ea8e3edb9d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java @@ -33,6 +33,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.exception.ThingsboardKafkaClientError; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -56,7 +57,7 @@ import java.util.Properties; configDirective = "tbExternalNodeKafkaConfig", iconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUzOCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMjU2IDQxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTIwMS44MTYgMjMwLjIxNmMtMTYuMTg2IDAtMzAuNjk3IDcuMTcxLTQwLjYzNCAxOC40NjFsLTI1LjQ2My0xOC4wMjZjMi43MDMtNy40NDIgNC4yNTUtMTUuNDMzIDQuMjU1LTIzLjc5NyAwLTguMjE5LTEuNDk4LTE2LjA3Ni00LjExMi0yMy40MDhsMjUuNDA2LTE3LjgzNWM5LjkzNiAxMS4yMzMgMjQuNDA5IDE4LjM2NSA0MC41NDggMTguMzY1IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI5Ljg3OS0yNC4zMDktNTQuMTg0LTU0LjE4NC01NC4xODQtMjkuODc1IDAtNTQuMTg0IDI0LjMwNS01NC4xODQgNTQuMTg0IDAgNS4zNDguODA4IDEwLjUwNSAyLjI1OCAxNS4zODlsLTI1LjQyMyAxNy44NDRjLTEwLjYyLTEzLjE3NS0yNS45MTEtMjIuMzc0LTQzLjMzMy0yNS4xODJ2LTMwLjY0YzI0LjU0NC01LjE1NSA0My4wMzctMjYuOTYyIDQzLjAzNy01My4wMTlDMTI0LjE3MSAyNC4zMDUgOTkuODYyIDAgNjkuOTg3IDAgNDAuMTEyIDAgMTUuODAzIDI0LjMwNSAxNS44MDMgNTQuMTg0YzAgMjUuNzA4IDE4LjAxNCA0Ny4yNDYgNDIuMDY3IDUyLjc2OXYzMS4wMzhDMjUuMDQ0IDE0My43NTMgMCAxNzIuNDAxIDAgMjA2Ljg1NGMwIDM0LjYyMSAyNS4yOTIgNjMuMzc0IDU4LjM1NSA2OC45NHYzMi43NzRjLTI0LjI5OSA1LjM0MS00Mi41NTIgMjcuMDExLTQyLjU1MiA1Mi44OTQgMCAyOS44NzkgMjQuMzA5IDU0LjE4NCA1NC4xODQgNTQuMTg0IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI1Ljg4My0xOC4yNTMtNDcuNTUzLTQyLjU1Mi01Mi44OTR2LTMyLjc3NWE2OS45NjUgNjkuOTY1IDAgMCAwIDQyLjYtMjQuNzc2bDI1LjYzMyAxOC4xNDNjLTEuNDIzIDQuODQtMi4yMiA5Ljk0Ni0yLjIyIDE1LjI0IDAgMjkuODc5IDI0LjMwOSA1NC4xODQgNTQuMTg0IDU0LjE4NCAyOS44NzUgMCA1NC4xODQtMjQuMzA1IDU0LjE4NC01NC4xODQgMC0yOS44NzktMjQuMzA5LTU0LjE4NC01NC4xODQtNTQuMTg0em0wLTEyNi42OTVjMTQuNDg3IDAgMjYuMjcgMTEuNzg4IDI2LjI3IDI2LjI3MXMtMTEuNzgzIDI2LjI3LTI2LjI3IDI2LjI3LTI2LjI3LTExLjc4Ny0yNi4yNy0yNi4yN2MwLTE0LjQ4MyAxMS43ODMtMjYuMjcxIDI2LjI3LTI2LjI3MXptLTE1OC4xLTQ5LjMzN2MwLTE0LjQ4MyAxMS43ODQtMjYuMjcgMjYuMjcxLTI2LjI3czI2LjI3IDExLjc4NyAyNi4yNyAyNi4yN2MwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3em01Mi41NDEgMzA3LjI3OGMwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3YzAtMTQuNDgzIDExLjc4NC0yNi4yNyAyNi4yNzEtMjYuMjdzMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3em0tMjYuMjcyLTExNy45N2MtMjAuMjA1IDAtMzYuNjQyLTE2LjQzNC0zNi42NDItMzYuNjM4IDAtMjAuMjA1IDE2LjQzNy0zNi42NDIgMzYuNjQyLTM2LjY0MiAyMC4yMDQgMCAzNi42NDEgMTYuNDM3IDM2LjY0MSAzNi42NDIgMCAyMC4yMDQtMTYuNDM3IDM2LjYzOC0zNi42NDEgMzYuNjM4em0xMzEuODMxIDY3LjE3OWMtMTQuNDg3IDAtMjYuMjctMTEuNzg4LTI2LjI3LTI2LjI3MXMxMS43ODMtMjYuMjcgMjYuMjctMjYuMjcgMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3YzAgMTQuNDgzLTExLjc4MyAyNi4yNzEtMjYuMjcgMjYuMjcxeiIvPjwvc3ZnPg==" ) -public class TbKafkaNode implements TbNode { +public class TbKafkaNode extends TbAbstractExternalNode { private static final String OFFSET = "offset"; private static final String PARTITION = "partition"; @@ -78,6 +79,7 @@ public class TbKafkaNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + super.init(ctx); this.config = TbNodeUtils.convert(configuration, TbKafkaNodeConfiguration.class); this.initError = null; Properties properties = new Properties(); @@ -129,6 +131,7 @@ public class TbKafkaNode implements TbNode { return null; }); } + ackIfNeeded(ctx, msg); } catch (Exception e) { ctx.tellFailure(msg, e); } @@ -164,11 +167,9 @@ public class TbKafkaNode implements TbNode { private void processRecord(TbContext ctx, TbMsg msg, RecordMetadata metadata, Exception e) { if (e == null) { - TbMsg next = processResponse(ctx, msg, metadata); - ctx.tellNext(next, TbRelationTypes.SUCCESS); + tellSuccess(ctx, processResponse(ctx, msg, metadata)); } else { - TbMsg next = processException(ctx, msg, e); - ctx.tellFailure(next, e); + tellFailure(ctx, processException(ctx, msg, e), e); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java index 50823a4c90..eec403cdb9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java @@ -22,10 +22,10 @@ import org.springframework.mail.javamail.JavaMailSenderImpl; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbEmail; -import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -47,7 +47,7 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; configDirective = "tbExternalNodeSendEmailConfig", icon = "send" ) -public class TbSendEmailNode implements TbNode { +public class TbSendEmailNode extends TbAbstractExternalNode { private static final String MAIL_PROP = "mail."; static final String SEND_EMAIL_TYPE = "SEND_EMAIL"; @@ -56,8 +56,9 @@ public class TbSendEmailNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + super.init(ctx); + this.config = TbNodeUtils.convert(configuration, TbSendEmailNodeConfiguration.class); try { - this.config = TbNodeUtils.convert(configuration, TbSendEmailNodeConfiguration.class); if (!this.config.isUseSystemSmtpSettings()) { mailSender = createMailSender(); } @@ -75,8 +76,9 @@ public class TbSendEmailNode implements TbNode { sendEmail(ctx, msg, email); return null; }), - ok -> ctx.tellSuccess(msg), - fail -> ctx.tellFailure(msg, fail)); + ok -> tellSuccess(ctx, msg), + fail -> tellFailure(ctx, msg, fail)); + ackIfNeeded(ctx, msg); } catch (Exception ex) { ctx.tellFailure(msg, ex); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 478e733958..8fac7b1683 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -32,6 +32,7 @@ import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.credentials.BasicCredentials; import org.thingsboard.rule.engine.credentials.ClientCredentials; import org.thingsboard.rule.engine.credentials.CredentialsType; +import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.plugin.ComponentClusteringMode; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -55,7 +56,7 @@ import java.util.concurrent.TimeoutException; configDirective = "tbExternalNodeMqttConfig", icon = "call_split" ) -public class TbMqttNode implements TbNode { +public class TbMqttNode extends TbAbstractExternalNode { private static final Charset UTF8 = Charset.forName("UTF-8"); @@ -67,8 +68,9 @@ public class TbMqttNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + super.init(ctx); + this.mqttNodeConfiguration = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class); try { - this.mqttNodeConfiguration = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class); this.mqttClient = initClient(ctx); } catch (Exception e) { throw new TbNodeException(e); @@ -81,13 +83,13 @@ public class TbMqttNode implements TbNode { this.mqttClient.publish(topic, Unpooled.wrappedBuffer(msg.getData().getBytes(UTF8)), MqttQoS.AT_LEAST_ONCE, mqttNodeConfiguration.isRetainedMessage()) .addListener(future -> { if (future.isSuccess()) { - ctx.tellSuccess(msg); + tellSuccess(ctx, msg); } else { - TbMsg next = processException(ctx, msg, future.cause()); - ctx.tellFailure(next, future.cause()); + tellFailure(ctx, processException(ctx, msg, future.cause()), future.cause()); } } ); + ackIfNeeded(ctx, msg); } private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable e) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java index 5720f66038..3ea96fa967 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java @@ -48,8 +48,9 @@ import javax.net.ssl.SSLException; public class TbAzureIotHubNode extends TbMqttNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + super.init(ctx); + this.mqttNodeConfiguration = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class); try { - this.mqttNodeConfiguration = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class); mqttNodeConfiguration.setPort(8883); mqttNodeConfiguration.setCleanSession(true); ClientCredentials credentials = mqttNodeConfiguration.getCredentials(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java index 057702cb2b..bf57628d64 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java @@ -23,6 +23,7 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequestConfig; import org.thingsboard.server.common.data.notification.info.RuleEngineOriginatedNotificationInfo; @@ -42,12 +43,13 @@ import java.util.concurrent.ExecutionException; configDirective = "tbExternalNodeNotificationConfig", icon = "notifications" ) -public class TbNotificationNode implements TbNode { +public class TbNotificationNode extends TbAbstractExternalNode { private TbNotificationNodeConfiguration config; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + super.init(ctx); this.config = TbNodeUtils.convert(configuration, TbNotificationNodeConfiguration.class); } @@ -69,15 +71,16 @@ public class TbNotificationNode implements TbNode { .originatorEntityId(ctx.getSelf().getRuleChainId()) .build(); - DonAsynchron.withCallback(ctx.getNotificationExecutor().executeAsync(() -> { - return ctx.getNotificationCenter().processNotificationRequest(ctx.getTenantId(), notificationRequest, stats -> { - TbMsgMetaData metaData = msg.getMetaData().copy(); - metaData.putValue("notificationRequestResult", JacksonUtil.toString(stats)); - ctx.tellSuccess(TbMsg.transformMsg(msg, metaData)); - }); - }), - r -> {}, - e -> ctx.tellFailure(msg, e)); + DonAsynchron.withCallback(ctx.getNotificationExecutor().executeAsync(() -> + ctx.getNotificationCenter().processNotificationRequest(ctx.getTenantId(), notificationRequest, stats -> { + TbMsgMetaData metaData = msg.getMetaData().copy(); + metaData.putValue("notificationRequestResult", JacksonUtil.toString(stats)); + tellSuccess(ctx, TbMsg.transformMsg(msg, metaData)); + })), + r -> { + }, + e -> tellFailure(ctx, msg, e)); + ackIfNeeded(ctx, msg); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java index 544a864931..fd56043848 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java @@ -22,6 +22,7 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -37,12 +38,13 @@ import java.util.concurrent.ExecutionException; configDirective = "tbExternalNodeSlackConfig", iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTYsMTVBMiwyIDAgMCwxIDQsMTdBMiwyIDAgMCwxIDIsMTVBMiwyIDAgMCwxIDQsMTNINlYxNU03LDE1QTIsMiAwIDAsMSA5LDEzQTIsMiAwIDAsMSAxMSwxNVYyMEEyLDIgMCAwLDEgOSwyMkEyLDIgMCAwLDEgNywyMFYxNU05LDdBMiwyIDAgMCwxIDcsNUEyLDIgMCAwLDEgOSwzQTIsMiAwIDAsMSAxMSw1VjdIOU05LDhBMiwyIDAgMCwxIDExLDEwQTIsMiAwIDAsMSA5LDEySDRBMiwyIDAgMCwxIDIsMTBBMiwyIDAgMCwxIDQsOEg5TTE3LDEwQTIsMiAwIDAsMSAxOSw4QTIsMiAwIDAsMSAyMSwxMEEyLDIgMCAwLDEgMTksMTJIMTdWMTBNMTYsMTBBMiwyIDAgMCwxIDE0LDEyQTIsMiAwIDAsMSAxMiwxMFY1QTIsMiAwIDAsMSAxNCwzQTIsMiAwIDAsMSAxNiw1VjEwTTE0LDE4QTIsMiAwIDAsMSAxNiwyMEEyLDIgMCAwLDEgMTQsMjJBMiwyIDAgMCwxIDEyLDIwVjE4SDE0TTE0LDE3QTIsMiAwIDAsMSAxMiwxNUEyLDIgMCAwLDEgMTQsMTNIMTlBMiwyIDAgMCwxIDIxLDE1QTIsMiAwIDAsMSAxOSwxN0gxNFoiIC8+PC9zdmc+" ) -public class TbSlackNode implements TbNode { +public class TbSlackNode extends TbAbstractExternalNode { private TbSlackNodeConfiguration config; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + super.init(ctx); this.config = TbNodeUtils.convert(configuration, TbSlackNodeConfiguration.class); } @@ -62,8 +64,9 @@ public class TbSlackNode implements TbNode { DonAsynchron.withCallback(ctx.getExternalCallExecutor().executeAsync(() -> { ctx.getSlackService().sendMessage(ctx.getTenantId(), token, config.getConversation().getId(), message); }), - r -> ctx.tellSuccess(msg), - e -> ctx.tellFailure(msg, e)); + r -> tellSuccess(ctx, msg), + e -> tellFailure(ctx, msg, e)); + ackIfNeeded(ctx, msg); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java index ba987678f3..e2986677b5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java @@ -76,6 +76,10 @@ public class TbDeviceProfileNode implements TbNode { this.ctx = ctx; scheduleAlarmHarvesting(ctx, null); ctx.addDeviceProfileListeners(this::onProfileUpdate, this::onDeviceUpdate); + initAlarmRuleState(false); + } + + private void initAlarmRuleState(boolean printNewlyAddedDeviceStates) { if (config.isFetchAlarmRulesStateOnStart()) { log.info("[{}] Fetching alarm rule state", ctx.getSelfId()); int fetchCount = 0; @@ -86,7 +90,7 @@ public class TbDeviceProfileNode implements TbNode { for (RuleNodeState rns : states.getData()) { fetchCount++; if (rns.getEntityId().getEntityType().equals(EntityType.DEVICE) && ctx.isLocalEntity(rns.getEntityId())) { - getOrCreateDeviceState(ctx, new DeviceId(rns.getEntityId().getId()), rns); + getOrCreateDeviceState(ctx, new DeviceId(rns.getEntityId().getId()), rns, printNewlyAddedDeviceStates); } } } @@ -130,7 +134,7 @@ public class TbDeviceProfileNode implements TbNode { removeDeviceState(deviceId); ctx.tellSuccess(msg); } else { - DeviceState deviceState = getOrCreateDeviceState(ctx, deviceId, null); + DeviceState deviceState = getOrCreateDeviceState(ctx, deviceId, null, false); if (deviceState != null) { deviceState.process(ctx, msg); } else { @@ -148,6 +152,7 @@ public class TbDeviceProfileNode implements TbNode { public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) { // Cleanup the cache for all entities that are no longer assigned to current server partitions deviceStates.entrySet().removeIf(entry -> !ctx.isLocalEntity(entry.getKey())); + initAlarmRuleState(true); } @Override @@ -156,13 +161,16 @@ public class TbDeviceProfileNode implements TbNode { deviceStates.clear(); } - protected DeviceState getOrCreateDeviceState(TbContext ctx, DeviceId deviceId, RuleNodeState rns) { + protected DeviceState getOrCreateDeviceState(TbContext ctx, DeviceId deviceId, RuleNodeState rns, boolean printNewlyAddedDeviceStates) { DeviceState deviceState = deviceStates.get(deviceId); if (deviceState == null) { DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceId); if (deviceProfile != null) { deviceState = new DeviceState(ctx, config, deviceId, new ProfileState(deviceProfile), rns); deviceStates.put(deviceId, deviceState); + if (printNewlyAddedDeviceStates) { + log.info("[{}][{}] Device [{}] was added during PartitionChangeMsg", ctx.getTenantId(), ctx.getSelfId(), deviceId); + } } } return deviceState; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java index 41dffe2fd8..4ae634902f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java @@ -28,6 +28,7 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -48,7 +49,7 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; configDirective = "tbExternalNodeRabbitMqConfig", iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZlcnNpb249IjEuMSIgeT0iMHB4IiB4PSIwcHgiIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPjxwYXRoIHN0cm9rZS13aWR0aD0iLjg0OTU2IiBkPSJtODYwLjQ3IDQxNi4zMmgtMjYyLjAxYy0xMi45MTMgMC0yMy42MTgtMTAuNzA0LTIzLjYxOC0yMy42MTh2LTI3Mi43MWMwLTIwLjMwNS0xNi4yMjctMzYuMjc2LTM2LjI3Ni0zNi4yNzZoLTkzLjc5MmMtMjAuMzA1IDAtMzYuMjc2IDE2LjIyNy0zNi4yNzYgMzYuMjc2djI3MC44NGMtMC4yNTQ4NyAxNC4xMDMtMTEuNDY5IDI1LjU3Mi0yNS43NDIgMjUuNTcybC04NS42MzYgMC42Nzk2NWMtMTQuMTAzIDAtMjUuNTcyLTExLjQ2OS0yNS41NzItMjUuNTcybDAuNjc5NjUtMjcxLjUyYzAtMjAuMzA1LTE2LjIyNy0zNi4yNzYtMzYuMjc2LTM2LjI3NmgtOTMuNTM3Yy0yMC4zMDUgMC0zNi4yNzYgMTYuMjI3LTM2LjI3NiAzNi4yNzZ2NzYzLjg0YzAgMTguMDk2IDE0Ljc4MiAzMi40NTMgMzIuNDUzIDMyLjQ1M2g3MjIuODFjMTguMDk2IDAgMzIuNDUzLTE0Ljc4MiAzMi40NTMtMzIuNDUzdi00MzUuMzFjLTEuMTg5NC0xOC4xODEtMTUuMjkyLTMyLjE5OC0zMy4zODgtMzIuMTk4em0tMTIyLjY4IDI4Ny4wN2MwIDIzLjYxOC0xOC44NiA0Mi40NzgtNDIuNDc4IDQyLjQ3OGgtNzMuOTk3Yy0yMy42MTggMC00Mi40NzgtMTguODYtNDIuNDc4LTQyLjQ3OHYtNzQuMjUyYzAtMjMuNjE4IDE4Ljg2LTQyLjQ3OCA0Mi40NzgtNDIuNDc4aDczLjk5N2MyMy42MTggMCA0Mi40NzggMTguODYgNDIuNDc4IDQyLjQ3OHoiLz48L3N2Zz4=" ) -public class TbRabbitMqNode implements TbNode { +public class TbRabbitMqNode extends TbAbstractExternalNode { private static final Charset UTF8 = Charset.forName("UTF-8"); @@ -61,6 +62,7 @@ public class TbRabbitMqNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + super.init(ctx); this.config = TbNodeUtils.convert(configuration, TbRabbitMqNodeConfiguration.class); ConnectionFactory factory = new ConnectionFactory(); factory.setHost(this.config.getHost()); @@ -83,11 +85,9 @@ public class TbRabbitMqNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { withCallback(publishMessageAsync(ctx, msg), - ctx::tellSuccess, - t -> { - TbMsg next = processException(ctx, msg, t); - ctx.tellFailure(next, t); - }); + m -> tellSuccess(ctx, m), + t -> tellFailure(ctx, processException(ctx, msg, t), t)); + ackIfNeeded(ctx, msg); } private ListenableFuture publishMessageAsync(TbContext ctx, TbMsg msg) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 6f9541f5b1..28e221b0b1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -65,6 +65,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; +import java.util.function.Consumer; @Data @Slf4j @@ -182,14 +183,16 @@ public class TbHttpClient { } } - public void processMessage(TbContext ctx, TbMsg msg) { + public void processMessage(TbContext ctx, TbMsg msg, + Consumer onSuccess, + BiConsumer onFailure) { String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg); HttpHeaders headers = prepareHeaders(msg); HttpMethod method = HttpMethod.valueOf(config.getRequestMethod()); HttpEntity entity; - if(HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method) || - HttpMethod.OPTIONS.equals(method) || HttpMethod.TRACE.equals(method) || - config.isIgnoreRequestBody()) { + if (HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method) || + HttpMethod.OPTIONS.equals(method) || HttpMethod.TRACE.equals(method) || + config.isIgnoreRequestBody()) { entity = new HttpEntity<>(headers); } else { entity = new HttpEntity<>(getData(msg), headers); @@ -198,21 +201,18 @@ public class TbHttpClient { URI uri = buildEncodedUri(endpointUrl); ListenableFuture> future = httpClient.exchange( uri, method, entity, String.class); - future.addCallback(new ListenableFutureCallback>() { + future.addCallback(new ListenableFutureCallback<>() { @Override public void onFailure(Throwable throwable) { - TbMsg next = processException(ctx, msg, throwable); - ctx.tellFailure(next, throwable); + onFailure.accept(processException(ctx, msg, throwable), throwable); } @Override public void onSuccess(ResponseEntity responseEntity) { if (responseEntity.getStatusCode().is2xxSuccessful()) { - TbMsg next = processResponse(ctx, msg, responseEntity); - ctx.tellSuccess(next); + onSuccess.accept(processResponse(ctx, msg, responseEntity)); } else { - TbMsg next = processFailureResponse(ctx, msg, responseEntity); - ctx.tellNext(next, TbRelationTypes.FAILURE); + onFailure.accept(processFailureResponse(ctx, msg, responseEntity), null); } } }); @@ -248,7 +248,7 @@ public class TbHttpClient { if (config.isTrimDoubleQuotes()) { final String dataBefore = data; - data = data.replaceAll("^\"|\"$", "");; + data = data.replaceAll("^\"|\"$", ""); log.trace("Trimming double quotes. Before trim: [{}], after trim: [{}]", dataBefore, data); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java index d2356c69f7..94b0e5d078 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java @@ -22,6 +22,7 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -43,24 +44,26 @@ import org.thingsboard.server.common.msg.TbMsg; configDirective = "tbExternalNodeRestApiCallConfig", iconUrl = "data:image/svg+xml;base64,PHN2ZyBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB2ZXJzaW9uPSIxLjEiIHk9IjBweCIgeD0iMHB4Ij48ZyB0cmFuc2Zvcm09Im1hdHJpeCguOTQ5NzUgMCAwIC45NDk3NSAxNy4xMiAyNi40OTIpIj48cGF0aCBkPSJtMTY5LjExIDEwOC41NGMtOS45MDY2IDAuMDczNC0xOS4wMTQgNi41NzI0LTIyLjAxNCAxNi40NjlsLTY5Ljk5MyAyMzEuMDhjLTMuNjkwNCAxMi4xODEgMy4yODkyIDI1LjIyIDE1LjQ2OSAyOC45MSAyLjIyNTkgMC42NzQ4MSA0LjQ5NjkgMSA2LjcyODUgMSA5Ljk3MjEgMCAxOS4xNjUtNi41MTUzIDIyLjE4Mi0xNi40NjdhNi41MjI0IDYuNTIyNCAwIDAgMCAwLjAwMiAtMC4wMDJsNjkuOTktMjMxLjA3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMCAtMC4wMDJjMy42ODU1LTEyLjE4MS0zLjI4Ny0yNS4yMjUtMTUuNDcxLTI4LjkxMi0yLjI4MjUtMC42OTE0NS00LjYxMTYtMS4wMTY5LTYuODk4NC0xem04NC45ODggMGMtOS45MDQ4IDAuMDczNC0xOS4wMTggNi41Njc1LTIyLjAxOCAxNi40NjlsLTY5Ljk4NiAyMzEuMDhjLTMuNjg5OCAxMi4xNzkgMy4yODUzIDI1LjIxNyAxNS40NjUgMjguOTA4IDIuMjI5NyAwLjY3NjQ3IDQuNTAwOCAxLjAwMiA2LjczMjQgMS4wMDIgOS45NzIxIDAgMTkuMTY1LTYuNTE1MyAyMi4xODItMTYuNDY3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMC4wMDIgLTAuMDAybDY5Ljk4OC0yMzEuMDdjMy42OTA4LTEyLjE4MS0zLjI4NTItMjUuMjIzLTE1LjQ2Ny0yOC45MTItMi4yODE0LTAuNjkyMzEtNC42MTA4LTEuMDE4OS02Ljg5ODQtMS4wMDJ6bS0yMTcuMjkgNDIuMjNjLTEyLjcyOS0wLjAwMDg3LTIzLjE4OCAxMC40NTYtMjMuMTg4IDIzLjE4NiAwLjAwMSAxMi43MjggMTAuNDU5IDIzLjE4NiAyMy4xODggMjMuMTg2IDEyLjcyNy0wLjAwMSAyMy4xODMtMTAuNDU5IDIzLjE4NC0yMy4xODYgMC4wMDA4NzYtMTIuNzI4LTEwLjQ1Ni0yMy4xODUtMjMuMTg0LTIzLjE4NnptMCAxNDYuNjRjLTEyLjcyNy0wLjAwMDg3LTIzLjE4NiAxMC40NTUtMjMuMTg4IDIzLjE4NC0wLjAwMDg3MyAxMi43MjkgMTAuNDU4IDIzLjE4OCAyMy4xODggMjMuMTg4IDEyLjcyOC0wLjAwMSAyMy4xODQtMTAuNDYgMjMuMTg0LTIzLjE4OC0wLjAwMS0xMi43MjYtMTAuNDU3LTIzLjE4My0yMy4xODQtMjMuMTg0em0yNzAuNzkgNDIuMjExYy0xMi43MjcgMC0yMy4xODQgMTAuNDU3LTIzLjE4NCAyMy4xODRzMTAuNDU1IDIzLjE4OCAyMy4xODQgMjMuMTg4aDE1NC45OGMxMi43MjkgMCAyMy4xODYtMTAuNDYgMjMuMTg2LTIzLjE4OCAwLjAwMS0xMi43MjgtMTAuNDU4LTIzLjE4NC0yMy4xODYtMjMuMTg0eiIgdHJhbnNmb3JtPSJtYXRyaXgoMS4wMzc2IDAgMCAxLjAzNzYgLTcuNTY3NiAtMTQuOTI1KSIgc3Ryb2tlLXdpZHRoPSIxLjI2OTMiLz48L2c+PC9zdmc+" ) -public class TbRestApiCallNode implements TbNode { +public class TbRestApiCallNode extends TbAbstractExternalNode { - private boolean useRedisQueueForMsgPersistence; protected TbHttpClient httpClient; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + super.init(ctx); TbRestApiCallNodeConfiguration config = TbNodeUtils.convert(configuration, TbRestApiCallNodeConfiguration.class); httpClient = new TbHttpClient(config, ctx.getSharedEventLoop()); - useRedisQueueForMsgPersistence = config.isUseRedisQueueForMsgPersistence(); - if (useRedisQueueForMsgPersistence) { + if (config.isUseRedisQueueForMsgPersistence()) { log.warn("[{}][{}] Usage of Redis Template is deprecated starting 2.5 and will have no affect", ctx.getTenantId(), ctx.getSelfId()); } } @Override public void onMsg(TbContext ctx, TbMsg msg) { - httpClient.processMessage(ctx, msg); + httpClient.processMessage(ctx, msg, + m -> tellSuccess(ctx, m), + (m, t) -> tellFailure(ctx, m, t)); + ackIfNeeded(ctx, msg); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java index e92b15780a..f6cbccd4d4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java @@ -23,6 +23,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.sms.SmsSender; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -39,13 +40,14 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; configDirective = "tbExternalNodeSendSmsConfig", icon = "sms" ) -public class TbSendSmsNode implements TbNode { +public class TbSendSmsNode extends TbAbstractExternalNode { private TbSendSmsNodeConfiguration config; private SmsSender smsSender; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + super.init(ctx); try { this.config = TbNodeUtils.convert(configuration, TbSendSmsNodeConfiguration.class); if (!this.config.isUseSystemSmsSettings()) { @@ -63,8 +65,9 @@ public class TbSendSmsNode implements TbNode { sendSms(ctx, msg); return null; }), - ok -> ctx.tellSuccess(msg), - fail -> ctx.tellFailure(msg, fail)); + ok -> tellSuccess(ctx, msg), + fail -> tellFailure(ctx, msg, fail)); + ackIfNeeded(ctx, msg); } catch (Exception ex) { ctx.tellFailure(msg, ex); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java index 14ab6cfbbf..7b6a54ae0b 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java @@ -167,7 +167,9 @@ public class TbHttpClientTest { capturedData.capture() )).thenReturn(successMsg); - httpClient.processMessage(ctx, msg); + httpClient.processMessage(ctx, msg, + m -> ctx.tellSuccess(msg), + (m, t) -> ctx.tellFailure(m, t)); Awaitility.await() .atMost(30, TimeUnit.SECONDS) diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index 86058d45ca..92981aa90f 100644 --- a/ui-ngx/angular.json +++ b/ui-ngx/angular.json @@ -82,6 +82,7 @@ ], "styles": [ "src/styles.scss", + "src/form.scss", "node_modules/jquery.terminal/css/jquery.terminal.min.css", "node_modules/tooltipster/dist/css/tooltipster.bundle.min.css", "node_modules/tooltipster/dist/css/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-shadow.min.css", diff --git a/ui-ngx/src/app/core/api/alias-controller.ts b/ui-ngx/src/app/core/api/alias-controller.ts index be08d50d69..971f238d83 100644 --- a/ui-ngx/src/app/core/api/alias-controller.ts +++ b/ui-ngx/src/app/core/api/alias-controller.ts @@ -252,10 +252,9 @@ export class AliasController implements IAliasController { private resolveDatasource(datasource: Datasource, forceFilter = false): Observable { const newDatasource = deepClone(datasource); - if (newDatasource.type === DatasourceType.device) { - newDatasource.type = DatasourceType.entity; - } - if (newDatasource.type === DatasourceType.entity || newDatasource.type === DatasourceType.entityCount + if (newDatasource.type === DatasourceType.entity + || newDatasource.type === DatasourceType.device + || newDatasource.type === DatasourceType.entityCount || newDatasource.type === DatasourceType.alarmCount) { if (newDatasource.filterId) { newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId); @@ -263,7 +262,8 @@ export class AliasController implements IAliasController { if (newDatasource.type === DatasourceType.alarmCount) { newDatasource.alarmFilter = this.entityService.resolveAlarmFilter(newDatasource.alarmFilterConfig, false); } - if (newDatasource.deviceId) { + if (newDatasource.type === DatasourceType.device) { + newDatasource.type = DatasourceType.entity; newDatasource.entityFilter = singleEntityFilterFromDeviceId(newDatasource.deviceId); if (forceFilter) { return this.entityService.findSingleEntityInfoByEntityFilter(newDatasource.entityFilter, diff --git a/ui-ngx/src/app/core/http/device.service.ts b/ui-ngx/src/app/core/http/device.service.ts index 018e202e81..dfc2d674a2 100644 --- a/ui-ngx/src/app/core/http/device.service.ts +++ b/ui-ngx/src/app/core/http/device.service.ts @@ -87,6 +87,13 @@ export class DeviceService { return this.http.post('/api/device', device, defaultHttpOptionsFromConfig(config)); } + public saveDeviceWithCredentials(device: Device, credentials: DeviceCredentials, config?: RequestConfig): Observable { + return this.http.post('/api/device-with-credentials', { + device, + credentials + }, defaultHttpOptionsFromConfig(config)); + } + public deleteDevice(deviceId: string, config?: RequestConfig) { return this.http.delete(`/api/device/${deviceId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index f82c6c3072..09a0a08d58 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -282,13 +282,9 @@ export class UtilsService { public validateDatasources(datasources: Array): Array { datasources.forEach((datasource) => { - // @ts-ignore - if (datasource.type === 'device') { - datasource.type = DatasourceType.entity; - datasource.entityType = EntityType.DEVICE; - if (datasource.deviceId) { - datasource.entityId = datasource.deviceId; - } else if (datasource.deviceAliasId) { + if (datasource.type === DatasourceType.device) { + if (datasource.deviceAliasId) { + datasource.type = DatasourceType.entity; datasource.entityAliasId = datasource.deviceAliasId; } if (datasource.deviceName) { diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 4018fd491f..c310693b03 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -20,7 +20,7 @@ import { finalize, share } from 'rxjs/operators'; import { Datasource, DatasourceData, FormattedData, ReplaceInfo } from '@app/shared/models/widget.models'; import { EntityId } from '@shared/models/id/entity-id'; import { NULL_UUID } from '@shared/models/id/has-uuid'; -import { EntityType, baseDetailsPageByEntityType } from '@shared/models/entity-type.models'; +import { baseDetailsPageByEntityType, EntityType } from '@shared/models/entity-type.models'; import { HttpErrorResponse } from '@angular/common/http'; import { TranslateService } from '@ngx-translate/core'; import { serverErrorCodesTranslations } from '@shared/models/constants'; @@ -126,15 +126,6 @@ export function isString(value: any): boolean { return typeof value === 'string'; } -export function isEmpty(obj: any): boolean { - for (const key of Object.keys(obj)) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - return false; - } - } - return true; -} - export function isLiteralObject(value: any) { return (!!value) && (value.constructor === Object); } @@ -320,9 +311,29 @@ export function extractType(target: any, keysOfProps: (keyof T return _.pick(target, keysOfProps); } -export function isEqual(a: any, b: any): boolean { - return _.isEqual(a, b); -} +export const isEqual = (a: any, b: any): boolean => _.isEqual(a, b); + +export const isEmpty = (a: any): boolean => _.isEmpty(a); + +export const isEqualIgnoreUndefined = (a: any, b: any): boolean => { + if (a === b) { + return true; + } + if (isDefinedAndNotNull(a) && isDefinedAndNotNull(b)) { + return isEqual(a, b); + } else { + return (isUndefinedOrNull(a) || !a) && (isUndefinedOrNull(b) || !b); + } +}; + +export const isArraysEqualIgnoreUndefined = (a: any[], b: any[]): boolean => { + const res = isEqualIgnoreUndefined(a, b); + if (!res) { + return (isUndefinedOrNull(a) || !a?.length) && (isUndefinedOrNull(b) || !b?.length); + } else { + return res; + } +}; export function mergeDeep(target: T, ...sources: T[]): T { return _.merge(target, ...sources); diff --git a/ui-ngx/src/app/core/ws/websocket.service.ts b/ui-ngx/src/app/core/ws/websocket.service.ts index 51545a6d54..2d2162267d 100644 --- a/ui-ngx/src/app/core/ws/websocket.service.ts +++ b/ui-ngx/src/app/core/ws/websocket.service.ts @@ -97,7 +97,9 @@ export abstract class WebsocketService implements WsServ this.dataStream.next(this.cmdWrapper.preparePublishCommands(MAX_PUBLISH_COMMANDS)); this.checkToClose(); } - this.tryOpenSocket(); + if (this.subscribersCount > 0) { + this.tryOpenSocket(); + } } private checkToClose() { diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 4c03b402ab..b7c382f6a0 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -179,6 +179,7 @@ import * as ProtobufContentComponent from '@shared/components/protobuf-content.c import * as SlackConversationAutocompleteComponent from '@shared/components/slack-conversation-autocomplete.component'; import * as StringItemsListComponent from '@shared/components/string-items-list.component'; import * as ToggleHeaderComponent from '@shared/components/toggle-header.component'; +import * as ToggleSelectComponent from '@shared/components/toggle-select.component'; import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component'; import * as EntitiesTableComponent from '@home/components/entity/entities-table.component'; @@ -478,6 +479,7 @@ class ModulesMap implements IModulesMap { '@shared/components/slack-conversation-autocomplete.component': SlackConversationAutocompleteComponent, '@shared/components/string-items-list.component': StringItemsListComponent, '@shared/components/toggle-header.component': ToggleHeaderComponent, + '@shared/components/toggle-select.component': ToggleSelectComponent, '@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent, '@home/components/entity/entities-table.component': EntitiesTableComponent, diff --git a/ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts b/ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts index 12f27f2dcc..357db25e67 100644 --- a/ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts +++ b/ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts @@ -25,6 +25,7 @@ import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardResolver } from '@app/modules/home/pages/dashboard/dashboard-routing.module'; import { UtilsService } from '@core/services/utils.service'; import { Widget } from '@app/shared/models/widget.models'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; @Injectable() export class WidgetEditorDashboardResolver implements Resolve { @@ -59,6 +60,7 @@ const routes: Routes = [ { path: 'dashboard/:dashboardId', component: DashboardPageComponent, + canDeactivate: [ConfirmOnExitGuard], data: { breadcrumb: { skip: true @@ -75,6 +77,7 @@ const routes: Routes = [ { path: 'widget-editor', component: DashboardPageComponent, + canDeactivate: [ConfirmOnExitGuard], data: { breadcrumb: { skip: true diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html index 8f43f27964..8d178aaa2c 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html @@ -26,10 +26,14 @@ #userAutocomplete="matAutocomplete" [displayWith]="displayUserFn" (optionSelected)="selected($event)"> - + account_circle {{ assigneeNotSetText | translate }} + + account_circle + {{ assignedToCurrentUserText | translate }} + ('AlarmAssigneePanelData'); @@ -59,15 +60,19 @@ export interface AlarmAssigneePanelData { }) export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDestroy { + assigneeOptions = AlarmAssigneeOption; + private dirty = false; alarmId: string; assigneeId?: string; + assigneeOption?: AlarmAssigneeOption = null; assigneeNotSetText = 'alarm.unassigned'; + assignedToCurrentUserText = ''; - reassigned: boolean = false; + reassigned = false; selectUserFormGroup: FormGroup; @@ -77,6 +82,14 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe searchText = ''; + get displayAssigneeNotSet(): boolean { + return !!this.assigneeId; + } + + get displayAssignedToCurrentUser(): boolean { + return false; + } + private destroy$ = new Subject(); constructor(@Inject(ALARM_ASSIGNEE_PANEL_DATA) public data: AlarmAssigneePanelData, @@ -124,8 +137,8 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe selected(event: MatAutocompleteSelectedEvent): void { this.clear(); - const user: User = event.option.value; - if (user) { + if (event.option.value?.id) { + const user: User = event.option.value; this.assign(user); } else { this.unassign(); diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts index 1526616833..cd8ce2ec33 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts @@ -36,11 +36,14 @@ import { emptyPageData } from '@shared/models/page/page-data'; import { OverlayRef } from '@angular/cdk/overlay'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { UtilsService } from '@core/services/utils.service'; +import { AlarmAssigneeOption } from '@shared/models/alarm.models'; export const ALARM_ASSIGNEE_SELECT_PANEL_DATA = new InjectionToken('AlarmAssigneeSelectPanelData'); export interface AlarmAssigneeSelectPanelData { assigneeId?: string; + assigneeOption?: AlarmAssigneeOption; + userMode?: boolean; } @Component({ @@ -50,11 +53,15 @@ export interface AlarmAssigneeSelectPanelData { }) export class AlarmAssigneeSelectPanelComponent implements OnInit, AfterViewInit, OnDestroy { + assigneeOptions = AlarmAssigneeOption; + private dirty = false; assigneeId?: string; + assigneeOption?: AlarmAssigneeOption; assigneeNotSetText = 'alarm.assignee-not-set'; + assignedToCurrentUserText = this.data.userMode ? 'alarm.assigned-to-me' : 'alarm.assigned-to-current-user'; selectUserFormGroup: FormGroup; @@ -67,6 +74,15 @@ export class AlarmAssigneeSelectPanelComponent implements OnInit, AfterViewInit userSelected = false; result?: UserEmailInfo; + optionResult?: AlarmAssigneeOption; + + get displayAssigneeNotSet(): boolean { + return this.assigneeOption !== AlarmAssigneeOption.noAssignee; + } + + get displayAssignedToCurrentUser(): boolean { + return this.assigneeOption !== AlarmAssigneeOption.currentUser; + } private destroy$ = new Subject(); @@ -77,6 +93,7 @@ export class AlarmAssigneeSelectPanelComponent implements OnInit, AfterViewInit private fb: FormBuilder, private utilsService: UtilsService) { this.assigneeId = data.assigneeId; + this.assigneeOption = data.assigneeOption; this.selectUserFormGroup = this.fb.group({ user: [null] }); @@ -112,7 +129,11 @@ export class AlarmAssigneeSelectPanelComponent implements OnInit, AfterViewInit selected(event: MatAutocompleteSelectedEvent): void { this.clear(); this.userSelected = true; - this.result = event.option.value; + if (event.option.value?.id) { + this.result = event.option.value; + } else { + this.optionResult = event.option.value; + } this.overlayRef.dispose(); } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html index 2ad7229365..c07a4cb3c6 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html @@ -16,16 +16,16 @@ --> - - alarm.assignee + subscriptSizing="dynamic" [appearance]="inline ? 'outline' : 'fill'"> + alarm.assignee - {{ getUserInitials() }} - account_circle - arrow_drop_down + account_circle + arrow_drop_down diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.ts index 4d1f6021be..50918bfd0d 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.ts @@ -30,6 +30,8 @@ import { AlarmAssigneeSelectPanelComponent, AlarmAssigneeSelectPanelData } from '@home/components/alarm/alarm-assignee-select-panel.component'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { AlarmAssigneeOption } from '@shared/models/alarm.models'; @Component({ selector: 'tb-alarm-assignee-select', @@ -47,8 +49,17 @@ export class AlarmAssigneeSelectComponent implements OnInit, ControlValueAccesso @Input() disabled: boolean; + @coerceBoolean() + @Input() + inline = false; + + @coerceBoolean() + @Input() + userMode = false; + assigneeFormGroup: UntypedFormGroup; assignee?: User | UserEmailInfo; + assigneeOption?: AlarmAssigneeOption; private propagateChange = (_: any) => {}; @@ -82,7 +93,15 @@ export class AlarmAssigneeSelectComponent implements OnInit, ControlValueAccesso } } - writeValue(userId?: UserId): void { + writeValue(value?: UserId | AlarmAssigneeOption): void { + let userId: UserId; + if (value && (value as UserId).id) { + userId = value as UserId; + this.assigneeOption = null; + } else { + userId = null; + this.assigneeOption = value ? value as AlarmAssigneeOption : AlarmAssigneeOption.noAssignee; + } const userObservable = userId ? this.userService.getUser(userId.id, {ignoreErrors: true}).pipe( catchError(() => of(null)) ) : of(null); @@ -92,15 +111,31 @@ export class AlarmAssigneeSelectComponent implements OnInit, ControlValueAccesso }), map((user) => this.getAssignee(user)) ).subscribe((assignee) => { - this.assigneeFormGroup.get('assignee').patchValue(assignee, {emitEvent: false}); + if (assignee) { + this.assigneeFormGroup.get('assignee').patchValue(assignee, {emitEvent: false}); + } else { + if (!this.assigneeOption) { + this.assigneeOption = AlarmAssigneeOption.noAssignee; + } + assignee = this.getAssigneeOption(this.assigneeOption); + this.assigneeFormGroup.get('assignee').patchValue(assignee, {emitEvent: false}); + } }); } - private getAssignee(user?: User| UserEmailInfo): string { + private getAssignee(user?: User| UserEmailInfo): string | null { if (user) { return this.getUserDisplayName(user); } else { + return null; + } + } + + private getAssigneeOption(assigneeOption: AlarmAssigneeOption): string { + if (assigneeOption === AlarmAssigneeOption.noAssignee) { return this.translateService.instant('alarm.assignee-not-set'); + } else { + return this.translateService.instant(this.userMode ? 'alarm.assigned-to-me' : 'alarm.assigned-to-current-user'); } } @@ -169,7 +204,9 @@ export class AlarmAssigneeSelectComponent implements OnInit, ControlValueAccesso { provide: ALARM_ASSIGNEE_SELECT_PANEL_DATA, useValue: { - assigneeId: this.assignee?.id?.id + assigneeId: this.assignee?.id?.id, + assigneeOption: this.assigneeOption, + userMode: this.userMode } as AlarmAssigneeSelectPanelData }, { @@ -183,8 +220,14 @@ export class AlarmAssigneeSelectComponent implements OnInit, ControlValueAccesso component.onDestroy(() => { if (component.instance.userSelected) { this.assignee = component.instance.result; - this.assigneeFormGroup.get('assignee').patchValue(this.getAssignee(this.assignee), {emitEvent: false}); - this.propagateChange(this.assignee?.id); + this.assigneeOption = component.instance.optionResult; + if (this.assignee) { + this.assigneeFormGroup.get('assignee').patchValue(this.getAssignee(this.assignee), {emitEvent: false}); + this.propagateChange(this.assignee?.id); + } else if (this.assigneeOption) { + this.assigneeFormGroup.get('assignee').patchValue(this.getAssigneeOption(this.assigneeOption), {emitEvent: false}); + this.propagateChange(this.assigneeOption); + } } }); } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss index 82f10650cf..947eeb01d0 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss @@ -19,20 +19,14 @@ justify-content: center; align-items: center; border-radius: 50%; - width: 28px; - height: 28px; min-width: 28px; min-height: 28px; color: white; font-size: 13px; font-weight: 700; - margin-left: 12px; - margin-right: 20px; } .unassigned-icon { - width: 28px; - height: 28px; font-size: 28px; color: rgba(0, 0, 0, 0.38); overflow: visible; @@ -40,3 +34,20 @@ margin-right: 20px; padding: 0; } + +.user-avatar, .unassigned-icon { + width: 28px; + height: 28px; + margin-left: 12px; + margin-right: 20px; + &.inline { + margin-left: 0; + margin-right: 8px; + } +} + +.drop-down-icon { + &.inline { + margin-right: -12px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html index a2a0c158a5..c5ed4fe52a 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html @@ -33,6 +33,13 @@
+ + -
- - +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss index 9777decdb0..c31f1d5791 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss @@ -14,9 +14,17 @@ * limitations under the License. */ :host { - .widget-preview-section { + .widget-preview-background { position: absolute; top: 72px; + left: 0; + right: 0; + bottom: 0; + background: #fff; + } + .widget-preview-section { + position: absolute; + top: 0; left: 16px; right: 16px; bottom: 16px; diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html b/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html index be71d3724e..768cea9252 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html @@ -26,13 +26,14 @@ matTooltip="{{ 'device.generate-client-id' | translate }}" matTooltipPosition="above" (click)="generate('clientId')" - *ngIf="!deviceCredentialsMqttFormGroup.get('clientId').value; else copyClientId"> + *ngIf="!deviceCredentialsMqttFormGroup.get('clientId').value && !disabled; else copyClientId"> autorenew + *ngIf="!deviceCredentialsMqttFormGroup.get('userName').value && !disabled; else copyUserName"> autorenew @@ -85,7 +86,7 @@ matTooltip="{{ 'device.generate-password' | translate }}" matTooltipPosition="above" (click)="generate('password')" - *ngIf="!deviceCredentialsMqttFormGroup.get('password').value; else copyPassword"> + *ngIf="!deviceCredentialsMqttFormGroup.get('password').value && !disabled; else copyPassword"> autorenew diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.html b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.html index 303c46ef70..fd666e1398 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.html +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.html @@ -16,14 +16,14 @@ -->
- - device.credentials-type - - +
+
device.credentials-type
+ + {{ credentialTypeNamesMap.get(credentialsType) }} - - - + + +
@@ -36,13 +36,14 @@ matTooltip="{{ 'device.generate-access-token' | translate }}" matTooltipPosition="above" (click)="generate('credentialsId')" - *ngIf="!deviceCredentialsFormGroup.get('credentialsId').value; else copyAccessToken"> + *ngIf="!deviceCredentialsFormGroup.get('credentialsId').value && !disabled; else copyAccessToken"> autorenew DeviceCredentialsComponent), multi: true, }], - styleUrls: [] + styleUrls: ['./device-credentials.component.scss'] }) export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, Validator, OnDestroy { @@ -73,9 +74,13 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, } } + @Input() + @coerceBoolean() + initAccessToken = false; + private destroy$ = new Subject(); - deviceCredentialsFormGroup: UntypedFormGroup; + deviceCredentialsFormGroup: FormGroup; deviceCredentialsType = DeviceCredentialsType; @@ -83,9 +88,10 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, credentialTypeNamesMap = credentialTypeNames; - private propagateChange = (v: any) => {}; + private propagateChange = null; + private propagateChangePending = false; - constructor(public fb: UntypedFormBuilder) { + constructor(public fb: FormBuilder) { this.deviceCredentialsFormGroup = this.fb.group({ credentialsType: [DeviceCredentialsType.ACCESS_TOKEN], credentialsId: [null], @@ -98,8 +104,8 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, }); this.deviceCredentialsFormGroup.get('credentialsType').valueChanges.pipe( takeUntil(this.destroy$) - ).subscribe(() => { - this.credentialsTypeChanged(); + ).subscribe((value) => { + this.credentialsTypeChanged(value); }); } @@ -107,6 +113,10 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, if (this.disabled) { this.deviceCredentialsFormGroup.disable({emitEvent: false}); } + if (this.initAccessToken && !this.deviceCredentialsFormGroup.get('credentialsId').value && + this.deviceCredentialsFormGroup.get('credentialsType').value === DeviceCredentialsType.ACCESS_TOKEN) { + this.deviceCredentialsFormGroup.get('credentialsId').patchValue(generateSecret(20)); + } } ngOnDestroy() { @@ -128,11 +138,21 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, updateView() { const deviceCredentialsValue = this.deviceCredentialsFormGroup.value; - this.propagateChange(deviceCredentialsValue); + if (this.propagateChange) { + this.propagateChange(deviceCredentialsValue); + } else { + this.propagateChangePending = true; + } } registerOnChange(fn: any): void { this.propagateChange = fn; + if (this.propagateChangePending) { + this.propagateChangePending = false; + setTimeout(() => { + this.updateView(); + }, 0); + } } registerOnTouched(fn: any): void {} @@ -144,11 +164,10 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, } else { this.deviceCredentialsFormGroup.enable({emitEvent: false}); this.updateValidators(); - this.deviceCredentialsFormGroup.updateValueAndValidity(); } } - public validate(c: UntypedFormControl) { + public validate(c: FormControl) { return this.deviceCredentialsFormGroup.valid ? null : { deviceCredentials: { valid: false, @@ -156,12 +175,15 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, }; } - credentialsTypeChanged(): void { + credentialsTypeChanged(type: DeviceCredentialsType): void { this.deviceCredentialsFormGroup.patchValue({ credentialsId: null, credentialsValue: null }); this.updateValidators(); + if (type === DeviceCredentialsType.ACCESS_TOKEN && this.initAccessToken) { + this.deviceCredentialsFormGroup.get('credentialsId').patchValue(generateSecret(20)); + } } updateValidators(): void { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss index 23917793d2..ad664e3156 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss @@ -16,6 +16,7 @@ :host { display: block; .clear-alarm-rule { + max-width: 100%; border: 2px groove rgba(0, 0, 0, .45); border-radius: 4px; padding: 8px; diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html index 2a118f6300..c88d169d82 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html @@ -43,6 +43,11 @@ (click)="editDeviceProfile($event)"> edit + + + + +
+
+
alarm.filter
+ +
+ +
+ + + + +
+
widget-config.card-appearance
+
+ + {{ 'widget-config.card-title' | translate }} + + + + +
+
+ + {{ 'widget-config.card-icon' | translate }} + +
+ + + + + +
+
+
+
widget-config.show-card-buttons
+ + {{ 'action.search' | translate }} + {{ 'alarm.alarm-filter' | translate }} + {{ 'widgets.table.columns-to-display' | translate }} + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.text-color' | translate }}
+
+ + + +
+
+
+
{{ 'widget-config.background-color' | translate }}
+
+ + + +
+
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.ts new file mode 100644 index 0000000000..33abc0670c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.ts @@ -0,0 +1,160 @@ +/// +/// Copyright © 2016-2023 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 } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup } 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, Datasource, WidgetConfig } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { isUndefined } from '@core/utils'; +import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component'; + +@Component({ + selector: 'tb-alarms-table-basic-config', + templateUrl: './alarms-table-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class AlarmsTableBasicConfigComponent extends BasicWidgetConfigComponent { + + public get alarmSource(): Datasource { + const datasources: Datasource[] = this.alarmsTableWidgetConfigForm.get('datasources').value; + if (datasources && datasources.length) { + return datasources[0]; + } else { + return null; + } + } + + alarmsTableWidgetConfigForm: UntypedFormGroup; + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.alarmsTableWidgetConfigForm; + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + this.alarmsTableWidgetConfigForm = this.fb.group({ + timewindowConfig: [getTimewindowConfig(configData.config), []], + alarmFilterConfig: [configData.config.alarmFilterConfig, []], + datasources: [[configData.config.alarmSource], []], + columns: [this.getColumns(configData.config.alarmSource), []], + showTitle: [configData.config.showTitle, []], + title: [configData.config.settings?.alarmsTitle, []], + showTitleIcon: [configData.config.showTitleIcon, []], + titleIcon: [configData.config.titleIcon, []], + iconColor: [configData.config.iconColor, []], + cardButtons: [this.getCardButtons(configData.config), []], + color: [configData.config.color, []], + backgroundColor: [configData.config.backgroundColor, []], + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow; + this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow; + this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow; + this.widgetConfig.config.alarmFilterConfig = config.alarmFilterConfig; + this.widgetConfig.config.alarmSource = config.datasources[0]; + this.setColumns(config.columns, this.widgetConfig.config.alarmSource); + this.widgetConfig.config.actions = config.actions; + this.widgetConfig.config.showTitle = config.showTitle; + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + this.widgetConfig.config.settings.alarmsTitle = config.title; + this.widgetConfig.config.showTitleIcon = config.showTitleIcon; + this.widgetConfig.config.titleIcon = config.titleIcon; + this.widgetConfig.config.iconColor = config.iconColor; + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.color = config.color; + this.widgetConfig.config.backgroundColor = config.backgroundColor; + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['showTitle', 'showTitleIcon']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const showTitle: boolean = this.alarmsTableWidgetConfigForm.get('showTitle').value; + const showTitleIcon: boolean = this.alarmsTableWidgetConfigForm.get('showTitleIcon').value; + if (showTitle) { + this.alarmsTableWidgetConfigForm.get('title').enable(); + this.alarmsTableWidgetConfigForm.get('showTitleIcon').enable({emitEvent: false}); + if (showTitleIcon) { + this.alarmsTableWidgetConfigForm.get('titleIcon').enable(); + this.alarmsTableWidgetConfigForm.get('iconColor').enable(); + } else { + this.alarmsTableWidgetConfigForm.get('titleIcon').disable(); + this.alarmsTableWidgetConfigForm.get('iconColor').disable(); + } + } else { + this.alarmsTableWidgetConfigForm.get('title').disable(); + this.alarmsTableWidgetConfigForm.get('showTitleIcon').disable({emitEvent: false}); + this.alarmsTableWidgetConfigForm.get('titleIcon').disable(); + this.alarmsTableWidgetConfigForm.get('iconColor').disable(); + } + this.alarmsTableWidgetConfigForm.get('title').updateValueAndValidity({emitEvent}); + this.alarmsTableWidgetConfigForm.get('showTitleIcon').updateValueAndValidity({emitEvent: false}); + this.alarmsTableWidgetConfigForm.get('titleIcon').updateValueAndValidity({emitEvent}); + this.alarmsTableWidgetConfigForm.get('iconColor').updateValueAndValidity({emitEvent}); + } + + private getColumns(alarmSource?: Datasource): DataKey[] { + if (alarmSource) { + return alarmSource.dataKeys || []; + } + return []; + } + + private setColumns(columns: DataKey[], alarmSource?: Datasource) { + if (alarmSource) { + alarmSource.dataKeys = columns; + } + } + + private getCardButtons(config: WidgetConfig): string[] { + const buttons: string[] = []; + if (isUndefined(config.settings?.enableSearch) || config.settings?.enableSearch) { + buttons.push('search'); + } + if (isUndefined(config.settings?.enableFilter) || config.settings?.enableFilter) { + buttons.push('filter'); + } + if (isUndefined(config.settings?.enableSelectColumnDisplay) || config.settings?.enableSelectColumnDisplay) { + buttons.push('columnsToDisplay'); + } + if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { + buttons.push('fullscreen'); + } + return buttons; + } + + private setCardButtons(buttons: string[], config: WidgetConfig) { + config.settings.enableSearch = buttons.includes('search'); + config.settings.enableFilter = buttons.includes('filter'); + config.settings.enableSelectColumnDisplay = buttons.includes('columnsToDisplay'); + config.enableFullscreen = buttons.includes('fullscreen'); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts index 73ebfb37c7..1ef7550c95 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts @@ -30,24 +30,39 @@ import { } from '@home/components/widget/config/basic/cards/entities-table-basic-config.component'; import { DataKeysPanelComponent } from '@home/components/widget/config/basic/common/data-keys-panel.component'; import { DataKeyRowComponent } from '@home/components/widget/config/basic/common/data-key-row.component'; +import { + TimeseriesTableBasicConfigComponent +} from '@home/components/widget/config/basic/cards/timeseries-table-basic-config.component'; +import { FlotBasicConfigComponent } from '@home/components/widget/config/basic/chart/flot-basic-config.component'; +import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module'; +import { + AlarmsTableBasicConfigComponent +} from '@home/components/widget/config/basic/alarm/alarms-table-basic-config.component'; @NgModule({ declarations: [ WidgetActionsPanelComponent, SimpleCardBasicConfigComponent, EntitiesTableBasicConfigComponent, + TimeseriesTableBasicConfigComponent, + FlotBasicConfigComponent, + AlarmsTableBasicConfigComponent, DataKeyRowComponent, DataKeysPanelComponent ], imports: [ CommonModule, SharedModule, + WidgetSettingsModule, WidgetConfigComponentsModule ], exports: [ WidgetActionsPanelComponent, SimpleCardBasicConfigComponent, EntitiesTableBasicConfigComponent, + TimeseriesTableBasicConfigComponent, + FlotBasicConfigComponent, + AlarmsTableBasicConfigComponent, DataKeyRowComponent, DataKeysPanelComponent ] @@ -57,5 +72,8 @@ export class BasicWidgetConfigModule { export const basicWidgetConfigComponentsMap: {[key: string]: Type} = { 'tb-simple-card-basic-config': SimpleCardBasicConfigComponent, - 'tb-entities-table-basic-config': EntitiesTableBasicConfigComponent + 'tb-entities-table-basic-config': EntitiesTableBasicConfigComponent, + 'tb-timeseries-table-basic-config': TimeseriesTableBasicConfigComponent, + 'tb-flot-basic-config': FlotBasicConfigComponent, + 'tb-alarms-table-basic-config': AlarmsTableBasicConfigComponent }; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html index ee76bdf472..5cecf6f62c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html @@ -28,6 +28,7 @@ -
-
widget-config.appearance
-
- +
+
widget-config.card-appearance
+
+ {{ 'widget-config.card-title' | translate }}
-
+
{{ 'widget-config.card-icon' | translate }} @@ -61,7 +62,15 @@
-
+
+
widget-config.show-card-buttons
+ + {{ 'action.search' | translate }} + {{ 'widgets.table.columns-to-display' | translate }} + {{ 'fullscreen.fullscreen' | translate }} + +
+
{{ 'widget-config.text-color' | translate }}
@@ -70,8 +79,8 @@
-
-
{{ 'widget-config.background' | translate }}
+
+
{{ 'widget-config.background-color' | translate }}
-
-
widget-config.appearance
-
-
widgets.simple-card.label
+
+
widget-config.appearance
+
+
widgets.simple-card.label
-
+
widgets.simple-card.label-position
@@ -49,19 +49,25 @@
-
+
widget-config.units-short
-
+
widget-config.decimals-short
- +
-
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
{{ 'widget-config.text-color' | translate }}
@@ -70,7 +76,7 @@
-
+
{{ 'widget-config.background' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts index b59170fb89..26d7b5ba61 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts @@ -23,10 +23,12 @@ import { WidgetConfigComponentData } from '@home/models/widget-component.models' import { Datasource, datasourcesHasAggregation, - datasourcesHasOnlyComparisonAggregation, + datasourcesHasOnlyComparisonAggregation, 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 } from '@home/components/widget/config/timewindow-config-panel.component'; +import { isUndefined } from '@core/utils'; @Component({ selector: 'tb-simple-card-basic-config', @@ -63,16 +65,13 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent { protected onConfigSet(configData: WidgetConfigComponentData) { this.simpleCardWidgetConfigForm = this.fb.group({ - timewindowConfig: [{ - useDashboardTimewindow: configData.config.useDashboardTimewindow, - displayTimewindow: configData.config.useDashboardTimewindow, - timewindow: configData.config.timewindow - }, []], + timewindowConfig: [getTimewindowConfig(configData.config), []], datasources: [configData.config.datasources, []], label: [this.getDataKeyLabel(configData.config.datasources), []], labelPosition: [configData.config.settings?.labelPosition, []], units: [configData.config.units, []], decimals: [configData.config.decimals, []], + cardButtons: [this.getCardButtons(configData.config), []], color: [configData.config.color, []], backgroundColor: [configData.config.backgroundColor, []], actions: [configData.config.actions || {}, []] @@ -88,9 +87,10 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent { this.widgetConfig.config.actions = config.actions; this.widgetConfig.config.units = config.units; this.widgetConfig.config.decimals = config.decimals; + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + this.setCardButtons(config.cardButtons, this.widgetConfig.config); this.widgetConfig.config.color = config.color; this.widgetConfig.config.backgroundColor = config.backgroundColor; - this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; this.widgetConfig.config.settings.labelPosition = config.labelPosition; return this.widgetConfig; } @@ -114,4 +114,16 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent { } } + private getCardButtons(config: WidgetConfig): string[] { + const buttons: string[] = []; + if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { + buttons.push('fullscreen'); + } + return buttons; + } + + private setCardButtons(buttons: string[], config: WidgetConfig) { + config.enableFullscreen = buttons.includes('fullscreen'); + } + } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html new file mode 100644 index 0000000000..c7906b63ad --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html @@ -0,0 +1,95 @@ + + + + + + + + +
+
widget-config.card-appearance
+
+ + {{ 'widget-config.card-title' | translate }} + + + + +
+
+ + {{ 'widget-config.card-icon' | translate }} + +
+ + + + + +
+
+
+
widget-config.show-card-buttons
+ + {{ 'action.search' | translate }} + {{ 'widgets.table.columns-to-display' | translate }} + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.text-color' | translate }}
+
+ + + +
+
+
+
{{ 'widget-config.background-color' | translate }}
+
+ + + +
+
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.ts new file mode 100644 index 0000000000..ac1b12167c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.ts @@ -0,0 +1,173 @@ +/// +/// Copyright © 2016-2023 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 } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup } 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, Datasource, WidgetConfig } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { deepClone, isUndefined } from '@core/utils'; +import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component'; + +@Component({ + selector: 'tb-timeseries-table-basic-config', + templateUrl: './timeseries-table-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class TimeseriesTableBasicConfigComponent extends BasicWidgetConfigComponent { + + public get datasource(): Datasource { + const datasources: Datasource[] = this.timeseriesTableWidgetConfigForm.get('datasources').value; + if (datasources && datasources.length) { + return datasources[0]; + } else { + return null; + } + } + + timeseriesTableWidgetConfigForm: UntypedFormGroup; + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.timeseriesTableWidgetConfigForm; + } + + protected setupDefaults(configData: WidgetConfigComponentData) { + this.setupDefaultDatasource(configData, + [{ name: 'temperature', label: 'Temperature', type: DataKeyType.timeseries, units: '°C', decimals: 0 }]); + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + this.timeseriesTableWidgetConfigForm = this.fb.group({ + timewindowConfig: [getTimewindowConfig(configData.config), []], + datasources: [configData.config.datasources, []], + columns: [this.getColumns(configData.config.datasources), []], + showTitle: [configData.config.showTitle, []], + title: [configData.config.title, []], + showTitleIcon: [configData.config.showTitleIcon, []], + titleIcon: [configData.config.titleIcon, []], + iconColor: [configData.config.iconColor, []], + cardButtons: [this.getCardButtons(configData.config), []], + color: [configData.config.color, []], + backgroundColor: [configData.config.backgroundColor, []], + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow; + this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow; + this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow; + this.widgetConfig.config.datasources = config.datasources; + this.setColumns(config.columns, this.widgetConfig.config.datasources); + this.widgetConfig.config.actions = config.actions; + this.widgetConfig.config.showTitle = config.showTitle; + this.widgetConfig.config.title = config.title; + this.widgetConfig.config.showTitleIcon = config.showTitleIcon; + this.widgetConfig.config.titleIcon = config.titleIcon; + this.widgetConfig.config.iconColor = config.iconColor; + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.color = config.color; + this.widgetConfig.config.backgroundColor = config.backgroundColor; + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['showTitle', 'showTitleIcon']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const showTitle: boolean = this.timeseriesTableWidgetConfigForm.get('showTitle').value; + const showTitleIcon: boolean = this.timeseriesTableWidgetConfigForm.get('showTitleIcon').value; + if (showTitle) { + this.timeseriesTableWidgetConfigForm.get('title').enable(); + this.timeseriesTableWidgetConfigForm.get('showTitleIcon').enable({emitEvent: false}); + if (showTitleIcon) { + this.timeseriesTableWidgetConfigForm.get('titleIcon').enable(); + this.timeseriesTableWidgetConfigForm.get('iconColor').enable(); + } else { + this.timeseriesTableWidgetConfigForm.get('titleIcon').disable(); + this.timeseriesTableWidgetConfigForm.get('iconColor').disable(); + } + } else { + this.timeseriesTableWidgetConfigForm.get('title').disable(); + this.timeseriesTableWidgetConfigForm.get('showTitleIcon').disable({emitEvent: false}); + this.timeseriesTableWidgetConfigForm.get('titleIcon').disable(); + this.timeseriesTableWidgetConfigForm.get('iconColor').disable(); + } + this.timeseriesTableWidgetConfigForm.get('title').updateValueAndValidity({emitEvent}); + this.timeseriesTableWidgetConfigForm.get('showTitleIcon').updateValueAndValidity({emitEvent: false}); + this.timeseriesTableWidgetConfigForm.get('titleIcon').updateValueAndValidity({emitEvent}); + this.timeseriesTableWidgetConfigForm.get('iconColor').updateValueAndValidity({emitEvent}); + } + + private getColumns(datasources?: Datasource[]): DataKey[] { + if (datasources && datasources.length) { + const dataKeys = deepClone(datasources[0].dataKeys) || []; + dataKeys.forEach(k => { + (k as any).latest = false; + }); + const latestDataKeys = deepClone(datasources[0].latestDataKeys) || []; + latestDataKeys.forEach(k => { + (k as any).latest = true; + }); + return dataKeys.concat(latestDataKeys); + } + return []; + } + + private setColumns(columns: DataKey[], datasources?: Datasource[]) { + if (datasources && datasources.length) { + const dataKeys = deepClone(columns.filter(c => !(c as any).latest)); + dataKeys.forEach(k => delete (k as any).latest); + const latestDataKeys = deepClone(columns.filter(c => (c as any).latest)); + latestDataKeys.forEach(k => delete (k as any).latest); + datasources[0].dataKeys = dataKeys; + datasources[0].latestDataKeys = latestDataKeys; + } + } + + private getCardButtons(config: WidgetConfig): string[] { + const buttons: string[] = []; + if (isUndefined(config.settings?.enableSearch) || config.settings?.enableSearch) { + buttons.push('search'); + } + if (isUndefined(config.settings?.enableSelectColumnDisplay) || config.settings?.enableSelectColumnDisplay) { + buttons.push('columnsToDisplay'); + } + if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { + buttons.push('fullscreen'); + } + return buttons; + } + + private setCardButtons(buttons: string[], config: WidgetConfig) { + config.settings.enableSearch = buttons.includes('search'); + config.settings.enableSelectColumnDisplay = buttons.includes('columnsToDisplay'); + config.enableFullscreen = buttons.includes('fullscreen'); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html new file mode 100644 index 0000000000..b9e1ab6f20 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html @@ -0,0 +1,122 @@ + + + + + + + + +
+
widget-config.card-appearance
+
+ + {{ 'widget-config.card-title' | translate }} + + + + +
+
+ + {{ 'widget-config.card-icon' | translate }} + +
+ + + + + +
+
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.text-color' | translate }}
+
+ + + +
+
+
+
{{ 'widget-config.background-color' | translate }}
+
+ + + +
+
+
+
+
widgets.chart.chart-appearance
+
+ + {{ 'widgets.chart.vertical-grid-lines' | translate }} + +
+
+ + {{ 'widgets.chart.horizontal-grid-lines' | translate }} + +
+
+ + + + + {{ 'widget-config.legend' | translate }} + + + + + + + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.ts new file mode 100644 index 0000000000..9d3dc4f1dd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.ts @@ -0,0 +1,168 @@ +/// +/// Copyright © 2016-2023 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 } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup } 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, Datasource, WidgetConfig } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { isUndefined } from '@core/utils'; +import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component'; + +@Component({ + selector: 'tb-flot-basic-config', + templateUrl: './flot-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class FlotBasicConfigComponent extends BasicWidgetConfigComponent { + + public get datasource(): Datasource { + const datasources: Datasource[] = this.flotWidgetConfigForm.get('datasources').value; + if (datasources && datasources.length) { + return datasources[0]; + } else { + return null; + } + } + + flotWidgetConfigForm: UntypedFormGroup; + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.flotWidgetConfigForm; + } + + protected setupDefaults(configData: WidgetConfigComponentData) { + this.setupDefaultDatasource(configData, + [{ name: 'temperature', label: 'Temperature', type: DataKeyType.timeseries, units: '°C', decimals: 0 }]); + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + this.flotWidgetConfigForm = this.fb.group({ + timewindowConfig: [getTimewindowConfig(configData.config), []], + datasources: [configData.config.datasources, []], + series: [this.getSeries(configData.config.datasources), []], + showTitle: [configData.config.showTitle, []], + title: [configData.config.title, []], + showTitleIcon: [configData.config.showTitleIcon, []], + titleIcon: [configData.config.titleIcon, []], + iconColor: [configData.config.iconColor, []], + cardButtons: [this.getCardButtons(configData.config), []], + color: [configData.config.color, []], + backgroundColor: [configData.config.backgroundColor, []], + verticalLines: [configData.config.settings?.grid?.verticalLines, []], + horizontalLines: [configData.config.settings?.grid?.horizontalLines, []], + showLegend: [configData.config.settings?.showLegend, []], + legendConfig: [configData.config.settings?.legendConfig, []], + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow; + this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow; + this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow; + this.widgetConfig.config.datasources = config.datasources; + this.setSeries(config.series, this.widgetConfig.config.datasources); + this.widgetConfig.config.actions = config.actions; + this.widgetConfig.config.showTitle = config.showTitle; + this.widgetConfig.config.title = config.title; + this.widgetConfig.config.showTitleIcon = config.showTitleIcon; + this.widgetConfig.config.titleIcon = config.titleIcon; + this.widgetConfig.config.iconColor = config.iconColor; + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.color = config.color; + this.widgetConfig.config.backgroundColor = config.backgroundColor; + this.widgetConfig.config.settings.grid = this.widgetConfig.config.settings.grid || {}; + this.widgetConfig.config.settings.grid.verticalLines = config.verticalLines; + this.widgetConfig.config.settings.grid.horizontalLines = config.horizontalLines; + this.widgetConfig.config.settings.showLegend = config.showLegend; + this.widgetConfig.config.settings.legendConfig = config.legendConfig; + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['showTitle', 'showTitleIcon', 'showLegend']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const showTitle: boolean = this.flotWidgetConfigForm.get('showTitle').value; + const showTitleIcon: boolean = this.flotWidgetConfigForm.get('showTitleIcon').value; + const showLegend: boolean = this.flotWidgetConfigForm.get('showLegend').value; + if (showTitle) { + this.flotWidgetConfigForm.get('title').enable(); + this.flotWidgetConfigForm.get('showTitleIcon').enable({emitEvent: false}); + if (showTitleIcon) { + this.flotWidgetConfigForm.get('titleIcon').enable(); + this.flotWidgetConfigForm.get('iconColor').enable(); + } else { + this.flotWidgetConfigForm.get('titleIcon').disable(); + this.flotWidgetConfigForm.get('iconColor').disable(); + } + } else { + this.flotWidgetConfigForm.get('title').disable(); + this.flotWidgetConfigForm.get('showTitleIcon').disable({emitEvent: false}); + this.flotWidgetConfigForm.get('titleIcon').disable(); + this.flotWidgetConfigForm.get('iconColor').disable(); + } + if (showLegend) { + this.flotWidgetConfigForm.get('legendConfig').enable(); + } else { + this.flotWidgetConfigForm.get('legendConfig').disable(); + } + this.flotWidgetConfigForm.get('title').updateValueAndValidity({emitEvent}); + this.flotWidgetConfigForm.get('showTitleIcon').updateValueAndValidity({emitEvent: false}); + this.flotWidgetConfigForm.get('titleIcon').updateValueAndValidity({emitEvent}); + this.flotWidgetConfigForm.get('iconColor').updateValueAndValidity({emitEvent}); + this.flotWidgetConfigForm.get('legendConfig').updateValueAndValidity({emitEvent}); + } + + private getSeries(datasources?: Datasource[]): DataKey[] { + if (datasources && datasources.length) { + return datasources[0].dataKeys || []; + } + return []; + } + + private setSeries(series: DataKey[], datasources?: Datasource[]) { + if (datasources && datasources.length) { + datasources[0].dataKeys = series; + } + } + + private getCardButtons(config: WidgetConfig): string[] { + const buttons: string[] = []; + if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { + buttons.push('fullscreen'); + } + return buttons; + } + + private setCardButtons(buttons: string[], config: WidgetConfig) { + config.enableFullscreen = buttons.includes('fullscreen'); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html index 0ae6ffb05e..b639d08512 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html @@ -16,6 +16,12 @@ -->
+ + + {{ 'datakey.timeseries' | translate }} + {{ 'datakey.latest' | translate }} + +
-
+
-
- +
+
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss index 5b3d4f1a80..22c9a9db8e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss @@ -43,6 +43,10 @@ } } + .tb-source-field { + width: 140px; + } + .tb-color-field, .tb-units-field, .tb-decimals-field { width: 60px; display: flex; @@ -51,3 +55,19 @@ align-items: center; } } + +.tb-data-keys-table-row-buttons { + display: flex; + flex-direction: row; + button.mat-mdc-icon-button.mat-mdc-button-base { + padding: 7px; + width: 38px; + height: 38px; + .mat-icon { + color: rgba(0, 0, 0, 0.38); + } + &.tb-hidden { + visibility: hidden; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts index d434ef74e3..7cdf277631 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts @@ -18,10 +18,12 @@ import { ChangeDetectorRef, Component, ElementRef, + EventEmitter, forwardRef, Input, OnChanges, OnInit, + Output, SimpleChanges, ViewChild, ViewEncapsulation @@ -37,7 +39,14 @@ import { } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; -import { DataKey, DatasourceType, JsonSettingsSchema, Widget, widgetType } from '@shared/models/widget.models'; +import { + DataKey, + DataKeyConfigMode, + DatasourceType, + JsonSettingsSchema, + Widget, + widgetType +} from '@shared/models/widget.models'; import { DataKeysPanelComponent } from '@home/components/widget/config/basic/common/data-keys-panel.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { AggregationType } from '@shared/models/time/time.models'; @@ -104,6 +113,9 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan @Input() deviceId: string; + @Output() + keyRemoved = new EventEmitter(); + keyFormControl: UntypedFormControl; keyRowFormGroup: UntypedFormGroup; @@ -133,6 +145,14 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan return this.dataKeysPanelComponent.hideDataKeyColor; } + get hideUnits(): boolean { + return this.dataKeysPanelComponent.hideUnits; + } + + get hideDecimals(): boolean { + return this.dataKeysPanelComponent.hideDecimals; + } + get widgetType(): widgetType { return this.widgetConfigComponent.widgetType; } @@ -141,6 +161,10 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan return this.widgetConfigComponent.widgetConfigCallbacks; } + get hasAdditionalLatestDataKeys(): boolean { + return this.dataKeysPanelComponent.hasAdditionalLatestDataKeys; + } + get widget(): Widget { return this.widgetConfigComponent.widget; } @@ -153,7 +177,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan return this.widgetConfigComponent.aliasController; } - get datakeySettingsSchema(): JsonSettingsSchema { + get dataKeySettingsSchema(): JsonSettingsSchema { return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema; } @@ -161,6 +185,14 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan return this.widgetConfigComponent.modelValue?.dataKeySettingsDirective; } + get latestDataKeySettingsSchema(): JsonSettingsSchema { + return this.widgetConfigComponent.modelValue?.latestDataKeySettingsSchema; + } + + get latestDataKeySettingsDirective(): string { + return this.widgetConfigComponent.modelValue?.latestDataKeySettingsDirective; + } + get isEntityDatasource(): boolean { return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType); } @@ -169,6 +201,22 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan return this.modelValue.type && ![ DataKeyType.alarm, DataKeyType.entityField, DataKeyType.count ].includes(this.modelValue.type); } + get keySettingsTitle(): string { + return this.dataKeysPanelComponent.keySettingsTitle; + } + + get removeKeyTitle(): string { + return this.dataKeysPanelComponent.removeKeyTitle; + } + + get dragEnabled(): boolean { + return this.dataKeysPanelComponent.dragEnabled; + } + + get isLatestDataKeys(): boolean { + return this.hasAdditionalLatestDataKeys && this.keyRowFormGroup.get('latest').value === true; + } + private propagateChange = (_val: any) => {}; constructor(private fb: UntypedFormBuilder, @@ -188,6 +236,12 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan units: [null, []], decimals: [null, []], }); + if (this.hasAdditionalLatestDataKeys) { + this.keyRowFormGroup.addControl('latest', this.fb.control(false)); + this.keyRowFormGroup.valueChanges.subscribe( + () => this.clearKeySearchCache() + ); + } this.keyRowFormGroup.valueChanges.subscribe( () => this.updateModel() ); @@ -262,6 +316,11 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan decimals: value?.decimals }, {emitEvent: false} ); + if (this.hasAdditionalLatestDataKeys) { + this.keyRowFormGroup.patchValue({ + latest: (value as any)?.latest + }, {emitEvent: false}); + } this.cd.markForCheck(); } @@ -291,15 +350,16 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan } } - editKey() { + editKey(advanced = false) { this.dialog.open(DataKeyConfigDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { dataKey: deepClone(this.modelValue), - dataKeySettingsSchema: this.datakeySettingsSchema, - dataKeySettingsDirective: this.dataKeySettingsDirective, + dataKeyConfigMode: advanced ? DataKeyConfigMode.advanced : DataKeyConfigMode.general, + dataKeySettingsSchema: this.isLatestDataKeys ? this.latestDataKeySettingsSchema : this.dataKeySettingsSchema, + dataKeySettingsDirective: this.isLatestDataKeys ? this.latestDataKeySettingsDirective : this.dataKeySettingsDirective, dashboard: this.dashboard, aliasController: this.aliasController, widget: this.widget, @@ -374,7 +434,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan } else if (this.datasourceType === DatasourceType.entity && this.entityAliasId || this.datasourceType === DatasourceType.device && this.deviceId) { const dataKeyTypes = [DataKeyType.timeseries]; - if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) { + if (this.isLatestDataKeys || this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) { dataKeyTypes.push(DataKeyType.attribute); dataKeyTypes.push(DataKeyType.entityField); if (this.widgetType === widgetType.alarm) { @@ -403,7 +463,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan } private addKeyFromChipValue(chip: DataKey) { - this.modelValue = this.callbacks.generateDataKey(chip.name, chip.type, this.datakeySettingsSchema); + this.modelValue = this.callbacks.generateDataKey(chip.name, chip.type, this.dataKeySettingsSchema); if (!this.keyRowFormGroup.get('label').value) { this.keyRowFormGroup.get('label').patchValue(this.modelValue.label, {emitEvent: false}); } 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 754f0052c9..f528e66c5e 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,38 +15,37 @@ limitations under the License. --> -
-
{{ panelTitle }}
+
+
{{ panelTitle }}
+
datakey.source
datakey.key
datakey.label
datakey.color
-
widget-config.units-short
-
widget-config.decimals-short
+
widget-config.units-short
+
widget-config.decimals-short
-
-
+
+ [entityAliasId]="entityAliasId" + (keyRemoved)="removeKey($index)">
-
+
+ + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html index 534f12493d..51f0574d4b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html @@ -15,38 +15,62 @@ limitations under the License. --> -
-
- widgets.table.common-table-settings - - widgets.table.alarms-table-title - - -
-
- - {{ 'widgets.table.enable-alarms-selection' | translate }} - - - {{ 'widgets.table.enable-alarms-search' | translate }} - - - {{ 'widgets.table.enable-select-column-display' | translate }} - - - {{ 'widgets.table.enable-alarm-filter' | translate }} - -
-
- - {{ 'widgets.table.enable-sticky-header' | translate }} - - - {{ 'widgets.table.enable-sticky-action' | translate }} - -
-
- + +
+
widgets.table.table-header
+
+
{{ 'widgets.table.alarms-table-title' | translate }}
+ + + +
+
+ + {{ 'widgets.table.enable-sticky-header' | translate }} + +
+
+
widgets.table.header-buttons
+ + {{ 'widgets.table.enable-alarms-search' | translate }} + + + {{ 'widgets.table.enable-select-column-display' | translate }} + + + {{ 'widgets.table.enable-alarm-filter' | translate }} + +
+
+
+
widgets.table.columns
+ + {{ 'widgets.table.enable-alarms-selection' | translate }} + +
+ + {{ 'widgets.table.enable-sticky-action' | translate }} + +
+
+
widgets.table.table-buttons
+ + {{ 'widgets.table.display-alarm-activity' | translate }} + + + {{ 'widgets.table.display-alarm-details' | translate }} + + + {{ 'widgets.table.allow-alarms-assign' | translate }} + + + {{ 'widgets.table.allow-alarms-ack' | translate }} + + + {{ 'widgets.table.allow-alarms-clear' | translate }} + +
+ widgets.table.hidden-cell-button-display-mode @@ -57,48 +81,34 @@ -
- - {{ 'widgets.table.display-alarm-activity' | translate }} - - - {{ 'widgets.table.display-alarm-details' | translate }} - - - {{ 'widgets.table.allow-alarms-ack' | translate }} - - - {{ 'widgets.table.allow-alarms-clear' | translate }} - - - {{ 'widgets.table.allow-alarms-assign' | translate }} - -
- - {{ 'widgets.table.display-pagination' | translate }} - - - widgets.table.default-page-size - - -
- - widgets.table.default-sort-order - + + widgets.table.default-sort-order + + +
+
+
widgets.table.pagination
+ + {{ 'widgets.table.display-pagination' | translate }} + +
+
widgets.table.default-page-size
+ + -
- -
- widgets.table.row-style + + +
+
widgets.table.rows
- - - + + {{ 'widgets.table.use-row-style-function' | translate }} - + widget-config.advanced-settings @@ -112,5 +122,5 @@ -
- + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html index d57f56cb59..02d51ee325 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html @@ -15,21 +15,56 @@ limitations under the License. --> -
- - widgets.table.custom-title - - - - widgets.table.column-width - - -
- widgets.table.cell-style + +
+
widgets.table.column-settings
+
+
{{ 'widgets.table.custom-title' | translate }}
+ + + +
+
+
{{ 'widgets.table.column-width' | translate }}
+ + + +
+
+
{{ 'widgets.table.default-column-visibility' | translate }}
+ + + + {{ 'widgets.table.column-visibility-visible' | translate }} + + + {{ 'widgets.table.column-visibility-hidden' | translate }} + + + {{ 'widgets.table.column-visibility-hidden-mobile' | translate }} + + + +
+
+
{{ 'widgets.table.column-selection-to-display' | translate }}
+ + + + {{ 'widgets.table.column-selection-to-display-enabled' | translate }} + + + {{ 'widgets.table.column-selection-to-display-disabled' | translate }} + + + +
+
+
- + - {{ 'widgets.table.use-cell-style-function' | translate }} @@ -48,13 +83,12 @@ -
-
- widgets.table.cell-content + +
- + - {{ 'widgets.table.use-cell-content-function' | translate }} @@ -73,30 +107,5 @@ -
- - widgets.table.default-column-visibility - - - {{ 'widgets.table.column-visibility-visible' | translate }} - - - {{ 'widgets.table.column-visibility-hidden' | translate }} - - - {{ 'widgets.table.column-visibility-hidden-mobile' | translate }} - - - - - widgets.table.column-selection-to-display - - - {{ 'widgets.table.column-selection-to-display-enabled' | translate }} - - - {{ 'widgets.table.column-selection-to-display-disabled' | translate }} - - - -
+ + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html index c0680dfeb9..b7ec55faf9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html @@ -15,32 +15,59 @@ limitations under the License. --> -
-
- widgets.table.common-table-settings - - widgets.table.entities-table-title - - -
-
- - {{ 'widgets.table.enable-search' | translate }} - - - {{ 'widgets.table.enable-select-column-display' | translate }} - -
-
- - {{ 'widgets.table.enable-sticky-header' | translate }} - - - {{ 'widgets.table.enable-sticky-action' | translate }} - -
-
- + +
+
widgets.table.table-header
+
+
{{ 'widgets.table.entities-table-title' | translate }}
+ + + +
+
+ + {{ 'widgets.table.enable-sticky-header' | translate }} + +
+
+
widgets.table.header-buttons
+ + {{ 'widgets.table.enable-search' | translate }} + + + {{ 'widgets.table.enable-select-column-display' | translate }} + +
+
+
+
widgets.table.columns
+
+ + {{ 'widgets.table.display-entity-name' | translate }} + + + + +
+
+ + {{ 'widgets.table.display-entity-label' | translate }} + + + + +
+
+ + {{ 'widgets.table.display-entity-type' | translate }} + +
+
+ + {{ 'widgets.table.enable-sticky-action' | translate }} + +
+ widgets.table.hidden-cell-button-display-mode @@ -51,54 +78,34 @@ -
-
- - {{ 'widgets.table.display-entity-name' | translate }} - - - widgets.table.entity-name-column-title - - -
-
- - {{ 'widgets.table.display-entity-label' | translate }} - - - widgets.table.entity-label-column-title - - -
- - {{ 'widgets.table.display-entity-type' | translate }} - -
- - {{ 'widgets.table.display-pagination' | translate }} - - - widgets.table.default-page-size - - -
- - widgets.table.default-sort-order - + + widgets.table.default-sort-order + + +
+
+
widgets.table.pagination
+ + {{ 'widgets.table.display-pagination' | translate }} + +
+
widgets.table.default-page-size
+ + -
- -
- widgets.table.row-style + + +
+
widgets.table.rows
- - - + + {{ 'widgets.table.use-row-style-function' | translate }} - + widget-config.advanced-settings @@ -112,5 +119,5 @@ -
- + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.ts index d91baebb53..af32954e62 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.ts @@ -33,7 +33,7 @@ export interface LabelWidgetLabel { @Component({ selector: 'tb-label-widget-label', templateUrl: './label-widget-label.component.html', - styleUrls: ['./label-widget-label.component.scss', './../widget-settings.scss'], + styleUrls: ['./label-widget-label.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.html index 1ee182f923..dc201c1cc6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.html @@ -15,9 +15,9 @@ limitations under the License. --> -
-
widgets.simple-card.label
-
+
+
widgets.simple-card.label
+
widgets.simple-card.label-position
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html index 2dbedf96bd..6c79eed622 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html @@ -15,18 +15,49 @@ limitations under the License. --> -
-
- widgets.table.cell-style + +
+
widgets.table.column-settings
+
+
{{ 'widgets.table.default-column-visibility' | translate }}
+ + + + {{ 'widgets.table.column-visibility-visible' | translate }} + + + {{ 'widgets.table.column-visibility-hidden' | translate }} + + + {{ 'widgets.table.column-visibility-hidden-mobile' | translate }} + + + +
+
+
{{ 'widgets.table.column-selection-to-display' | translate }}
+ + + + {{ 'widgets.table.column-selection-to-display-enabled' | translate }} + + + {{ 'widgets.table.column-selection-to-display-disabled' | translate }} + + + +
+
+
- - - + + {{ 'widgets.table.use-cell-style-function' | translate }} - + widget-config.advanced-settings @@ -40,18 +71,17 @@ -
-
- widgets.table.cell-content +
+
- - - + + {{ 'widgets.table.use-cell-content-function' | translate }} - + widget-config.advanced-settings @@ -65,30 +95,5 @@ - - - widgets.table.default-column-visibility - - - {{ 'widgets.table.column-visibility-visible' | translate }} - - - {{ 'widgets.table.column-visibility-hidden' | translate }} - - - {{ 'widgets.table.column-visibility-hidden-mobile' | translate }} - - - - - widgets.table.column-selection-to-display - - - {{ 'widgets.table.column-selection-to-display-enabled' | translate }} - - - {{ 'widgets.table.column-selection-to-display-disabled' | translate }} - - - - +
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html index 8fb1629b13..97cbe28ae9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html @@ -15,25 +15,58 @@ limitations under the License. --> -
- - {{ 'widgets.table.show-latest-data-column' | translate }} - - - widgets.table.latest-data-column-order - - -
- widgets.table.cell-style + +
+
widgets.table.column-settings
+ + {{ 'widgets.table.show-latest-data-column' | translate }} + +
+
widgets.table.latest-data-column-order
+ + + +
+
+
{{ 'widgets.table.default-column-visibility' | translate }}
+ + + + {{ 'widgets.table.column-visibility-visible' | translate }} + + + {{ 'widgets.table.column-visibility-hidden' | translate }} + + + {{ 'widgets.table.column-visibility-hidden-mobile' | translate }} + + + +
+
+
{{ 'widgets.table.column-selection-to-display' | translate }}
+ + + + {{ 'widgets.table.column-selection-to-display-enabled' | translate }} + + + {{ 'widgets.table.column-selection-to-display-disabled' | translate }} + + + +
+
+
- - - + + {{ 'widgets.table.use-cell-style-function' | translate }} - + widget-config.advanced-settings @@ -47,18 +80,17 @@ -
-
- widgets.table.cell-content +
+
- - - + + {{ 'widgets.table.use-cell-content-function' | translate }} - + widget-config.advanced-settings @@ -72,30 +104,6 @@ - - - widgets.table.default-column-visibility - - - {{ 'widgets.table.column-visibility-visible' | translate }} - - - {{ 'widgets.table.column-visibility-hidden' | translate }} - - - {{ 'widgets.table.column-visibility-hidden-mobile' | translate }} - - - - - widgets.table.column-selection-to-display - - - {{ 'widgets.table.column-selection-to-display-enabled' | translate }} - - - {{ 'widgets.table.column-selection-to-display-disabled' | translate }} - - - - +
+ + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html index ed4428fe45..e45c235ad1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html @@ -15,28 +15,31 @@ limitations under the License. --> -
-
- widgets.table.common-table-settings -
-
- - {{ 'widgets.table.enable-search' | translate }} - - - {{ 'widgets.table.enable-select-column-display' | translate }} - -
-
- - {{ 'widgets.table.enable-sticky-header' | translate }} - - - {{ 'widgets.table.enable-sticky-action' | translate }} - -
-
- + +
+
widgets.table.table-header
+ + {{ 'widgets.table.enable-sticky-header' | translate }} + + + {{ 'widgets.table.enable-search' | translate }} + + + {{ 'widgets.table.enable-select-column-display' | translate }} + +
+
+
widgets.table.columns
+ + {{ 'widgets.table.display-timestamp' | translate }} + + + {{ 'widgets.table.display-milliseconds' | translate }} + + + {{ 'widgets.table.enable-sticky-action' | translate }} + + widgets.table.hidden-cell-button-display-mode @@ -47,41 +50,39 @@ -
- - {{ 'widgets.table.display-timestamp' | translate }} - - - {{ 'widgets.table.display-milliseconds' | translate }} - -
- - {{ 'widgets.table.display-pagination' | translate }} - - - widgets.table.default-page-size - - -
- - {{ 'widgets.table.use-entity-label-tab-name' | translate }} - - - {{ 'widgets.table.hide-empty-lines' | translate }} - -
-
-
- widgets.table.row-style +
+
+
widgets.table.pagination
+ + {{ 'widgets.table.display-pagination' | translate }} + +
+
widgets.table.default-page-size
+ + + +
+
+
+
widgets.table.table-tabs
+ + {{ 'widgets.table.use-entity-label-tab-name' | translate }} + +
+
+
widgets.table.rows
+ + {{ 'widgets.table.hide-empty-lines' | translate }} + - - - + + {{ 'widgets.table.use-row-style-function' | translate }} - + widget-config.advanced-settings @@ -95,5 +96,5 @@ - - +
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html index 2bebfeef8a..f3176f9834 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html @@ -15,81 +15,85 @@ limitations under the License. --> -
-
- widgets.chart.common-settings - + +
+ {{ 'widgets.chart.data-is-hidden-by-default' | translate }} - - + + {{ 'widgets.chart.disable-data-hiding' | translate }} - - + + {{ 'widgets.chart.remove-from-legend' | translate }} - - + + {{ 'widgets.chart.exclude-from-stacking' | translate }} - -
-
- widgets.chart.line-settings + +
+
- - - + + {{ 'widgets.chart.show-line' | translate }} - + widget-config.advanced-settings -
- - widgets.chart.line-width - +
+
{{ 'widgets.chart.line-width' | translate }}
+ + + px - - {{ 'widgets.chart.fill-line' | translate }} - - - widgets.chart.fill-line-opacity - +
+ + {{ 'widgets.chart.fill-line' | translate }} + +
+
{{ 'widgets.chart.fill-line-opacity' | translate }}
+ + -
+
- -
- widgets.chart.points-settings + +
- - - + + {{ 'widgets.chart.show-points' | translate }} - + widget-config.advanced-settings -
-
- - widgets.chart.points-line-width - - - - widgets.chart.points-radius - - -
- - widgets.chart.point-shape +
+
{{ 'widgets.chart.points-line-width' | translate }}
+ + + px + +
+
+
{{ 'widgets.chart.points-radius' | translate }}
+ + + px + +
+
+
{{ 'widgets.chart.point-shape' | translate }}
+ {{ 'widgets.chart.point-shape-circle' | translate }} @@ -111,19 +115,19 @@ - - -
+
+ + -
-
- widgets.chart.tooltip-settings + +
+
widgets.chart.tooltip-settings
-
-
- widgets.chart.yaxis-settings - + +
+
widgets.chart.vertical-axis
+ {{ 'widgets.chart.show-separate-axis' | translate }} - - widgets.chart.axis-title - - -
- - widgets.chart.min-scale-value - +
+
widgets.chart.axis-title
+ + - - widgets.chart.max-scale-value - +
+
+
widgets.chart.min-scale-value
+ + -
- - widgets.chart.axis-position - - - {{ 'widgets.chart.axis-position-left' | translate }} - - - {{ 'widgets.chart.axis-position-right' | translate }} - - - -
- widgets.chart.yaxis-tick-labels-settings -
- - widgets.chart.tick-step-size - +
+
+
widgets.chart.max-scale-value
+ + + +
+
+
{{ 'widgets.chart.axis-position' | translate }}
+ + + + {{ 'widgets.chart.axis-position-left' | translate }} + + + {{ 'widgets.chart.axis-position-right' | translate }} + + + +
+
+
widgets.chart.ticks
+
+
widget-config.decimals-short
+ + - - widgets.chart.number-of-decimals - +
+
+
widgets.chart.tick-step-size
+ + - +
-
- -
- widgets.chart.thresholds -
-
-
- - -
-
-
- widgets.chart.no-thresholds -
-
- +
+
+
+
+
widgets.chart.thresholds
+ +
+
+
+ +
-
-
- widgets.chart.comparison-settings - + +
+
widgets.chart.comparison-settings
+ - - + {{ 'widgets.chart.show-values-for-comparison' | translate }} - - widget-config.advanced-settings - -
- - widgets.chart.comparison-values-label - +
+
widgets.chart.comparison-values-label
+ + - - -
+
+
+
{{ 'widgets.chart.comparison-line-color' | translate }}
+
+ + + +
+
-
- + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts index b2c88e5fcb..f254eaeb2e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts @@ -225,6 +225,7 @@ export class FlotKeySettingsComponent extends PageComponent implements OnInit, C this.flotKeySettingsFormGroup.disable({emitEvent: false}); } else { this.flotKeySettingsFormGroup.enable({emitEvent: false}); + this.updateValidators(false); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.html index 61f1587306..cdff37f492 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.html @@ -15,34 +15,37 @@ limitations under the License. --> -
-
- widgets.chart.threshold-settings - + +
+
widgets.chart.threshold-settings
+ - {{ 'widgets.chart.use-as-threshold' | translate }} - - widget-config.advanced-settings - -
- - widgets.chart.threshold-line-width - +
+
widgets.chart.threshold-line-width
+ + + px - - -
+
+
+
{{ 'widgets.chart.threshold-color' | translate }}
+
+ + + +
+
-
-
+ + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html index 353e716ad9..6f08b5f3f7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html @@ -15,44 +15,34 @@ limitations under the License. --> - - -
- -
-
{{ thresholdText() }}
-
-
-
-
-
- - -
-
- -
- -
- -
- - widgets.chart.line-width - - - - -
-
-
-
-
+
+ + +
+
{{ thresholdText() }}
+ + + + +
+
+ + +
+
widgets.chart.line-width
+ + + px + +
+
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss index 53eeafb799..150d516894 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss @@ -13,28 +13,60 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -:host { - display: block; - .mat-expansion-panel { + +.tb-flot-threshold { + display: flex; + flex-direction: row; + align-items: start; + gap: 4px; + .mat-expansion-panel.tb-settings.tb-flot-threshold-settings { box-shadow: none; - &.flot-threshold { - border: 1px groove rgba(0, 0, 0, .25); - .mat-expansion-panel-header { - padding: 0 24px 0 8px; - &.mat-expanded { - height: 48px; + border-radius: 6px; + border: 1px solid rgba(0, 0, 0, 0.12); + .mat-expansion-panel-header { + height: 56px; + border-radius: 0; + display: flex; + flex-direction: row; + align-items: stretch; + .tb-threshold-header { + flex: 1; + display: flex; + flex-direction: row; + gap: 16px; + align-items: center; + padding-left: 16px; + .mat-divider-vertical { + height: 100%; } } + .tb-threshold-text { + flex: 1; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.15px; + } + .mat-expansion-indicator { + margin-right: 22px; + margin-left: 22px; + margin-top: 12px; + } } - } -} - -:host ::ng-deep { - .mat-expansion-panel { - &.flot-threshold { - .mat-expansion-panel-body { - padding: 0 8px 8px; + > .mat-expansion-panel-content { + > .mat-expansion-panel-body { + padding: 16px !important; } } + &.mat-expanded { + .mat-expansion-panel-header { + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + } + } + } + > .mdc-icon-button { + margin-top: 4px; + color: rgba(0, 0, 0, 0.54); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.ts index 9b997ed424..eb55c12f86 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.ts @@ -15,8 +15,14 @@ /// import { ValueSourceProperty } from '@home/components/widget/lib/settings/common/value-source.component'; -import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Component, EventEmitter, forwardRef, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -28,14 +34,15 @@ import { TbFlotKeyThreshold } from '@home/components/widget/lib/flot-widget.mode @Component({ selector: 'tb-flot-threshold', templateUrl: './flot-threshold.component.html', - styleUrls: ['./flot-threshold.component.scss', './../widget-settings.scss'], + styleUrls: ['./flot-threshold.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FlotThresholdComponent), multi: true } - ] + ], + encapsulation: ViewEncapsulation.None }) export class FlotThresholdComponent extends PageComponent implements OnInit, ControlValueAccessor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html index 5422c034f6..defc26856a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html @@ -15,30 +15,33 @@ limitations under the License. --> -
-
- widgets.chart.common-settings - + +
+
widgets.chart.common-settings
+ {{ 'widgets.chart.enable-stacking-mode' | translate }} - -
- - widgets.chart.line-shadow-size - + + + {{ 'widgets.chart.enable-selection-mode' | translate }} + + + {{ 'widgets.chart.display-smooth-lines' | translate }} + +
+
widgets.chart.line-shadow-size
+ + - - {{ 'widgets.chart.display-smooth-lines' | translate }} - -
-
- - widgets.chart.default-bar-width - +
+
+
widgets.chart.default-bar-width
+ + - - widgets.chart.bar-alignment +
+
+
{{ 'widgets.chart.bar-alignment' | translate }}
+ {{ 'widgets.chart.bar-alignment-left' | translate }} @@ -51,211 +54,255 @@ -
-
- - widgets.chart.default-font-size - + +
+
widgets.chart.thresholds-line-width
+ + - - -
- - widgets.chart.thresholds-line-width - - - -
- widget-config.legend + +
+
{{ 'widgets.chart.default-font' | translate }}
+
+ + + px + + + + +
+
+ +
+
widget-config.legend
- + - - {{ 'widget-config.display-legend' | translate }} + + {{ 'widget-config.legend' | translate }} - + widget-config.advanced-settings - + + -
-
- widgets.chart.tooltip-settings + +
+
widgets.chart.axis
+
+
widgets.chart.vertical-axis
+
+
widgets.chart.axis-title
+ + + +
+
+
widgets.chart.min-scale-value
+ + + +
+
+
widgets.chart.max-scale-value
+ + + +
+
+
widgets.chart.ticks
+ + + + + {{ 'widgets.chart.ticks' | translate }} + + + + widget-config.advanced-settings + + + +
+
{{ 'widget-config.color' | translate }}
+
+ + + +
+
+
+
widget-config.decimals-short
+ + + +
+
+
widgets.chart.tick-step-size
+ + + +
+ + +
+
+
+
+
+
widgets.chart.horizontal-axis
+
+
widgets.chart.axis-title
+ + + +
+
+
widgets.chart.ticks
+ + + + + {{ 'widgets.chart.ticks' | translate }} + + + + widget-config.advanced-settings + + + +
+
{{ 'widget-config.color' | translate }}
+
+ + + +
+
+
+
+
+
+
+
+
widgets.chart.chart-background
+
+ + {{ 'widgets.chart.vertical-grid-lines' | translate }} + +
+
+ + {{ 'widgets.chart.horizontal-grid-lines' | translate }} + +
+
+
{{ 'widgets.chart.grid-lines-color' | translate }}
+
+ + + +
+
+
+
{{ 'widgets.chart.border' | translate }}
+
+ + + px + + + + +
+
+
+
{{ 'widgets.chart.background-color' | translate }}
+
+ + + +
+
+
+
+
widgets.chart.tooltip
- - - + + - {{ 'widgets.chart.show-tooltip' | translate }} + {{ 'widgets.chart.tooltip' | translate }} - + widget-config.advanced-settings -
- +
+ {{ 'widgets.chart.hover-individual-points' | translate }} - +
+
+ {{ 'widgets.chart.show-cumulative-values' | translate }} - +
+
+ {{ 'widgets.chart.hide-zero-false-values' | translate }} - - -
+
+ + -
-
- widgets.chart.grid-settings - - {{ 'widgets.chart.show-vertical-lines' | translate }} - - - {{ 'widgets.chart.show-horizontal-lines' | translate }} - - - widgets.chart.grid-outline-border-width - - - - - - - - -
-
- widgets.chart.xaxis-settings - - widgets.chart.axis-title - - -
- widgets.chart.xaxis-tick-labels-settings - - - - - {{ 'widgets.chart.show-tick-labels' | translate }} - - - - widget-config.advanced-settings - - - - - - - -
-
-
- widgets.chart.yaxis-settings - - widgets.chart.axis-title - - -
- - widgets.chart.min-scale-value - - - - widgets.chart.max-scale-value - - -
-
- widgets.chart.yaxis-tick-labels-settings - - - - - {{ 'widgets.chart.show-tick-labels' | translate }} - - - - widget-config.advanced-settings - - - - - -
- - widgets.chart.tick-step-size - - - - widgets.chart.number-of-decimals - - -
- - -
-
-
-
-
- widgets.chart.comparison-settings + +
+
widgets.chart.comparison-settings
- - - + + {{ 'widgets.chart.enable-comparison' | translate }} - + widget-config.advanced-settings -
- - widgets.chart.time-for-comparison +
+
{{ 'widgets.chart.time-for-comparison' | translate }}
+ {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} @@ -277,18 +324,29 @@ - - widgets.chart.custom-interval-value - +
+
+
widgets.chart.custom-interval-value
+ + -
- widgets.chart.comparison-x-axis-settings - - widgets.chart.axis-title - +
+
+
widgets.chart.comparison-x-axis-settings
+
+
widgets.chart.axis-title
+ + - - widgets.chart.axis-position +
+
+ + {{ 'widgets.chart.show-tick-labels' | translate }} + +
+
+
{{ 'widgets.chart.axis-position' | translate }}
+ {{ 'widgets.chart.axis-position-top' | translate }} @@ -298,31 +356,28 @@ - - {{ 'widgets.chart.show-tick-labels' | translate }} - -
- + + - -
- widgets.chart.custom-legend-settings + +
+
widgets.chart.custom-legend-settings
- + - + {{ 'widgets.chart.enable-custom-legend' | translate }} - + widget-config.advanced-settings -
- widgets.chart.label-keys-list +
+
widgets.chart.label-keys-list
@@ -348,8 +403,8 @@
-
+
-
- + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts index 98c4acd62b..46454937f6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts @@ -45,6 +45,7 @@ import { defaultLegendConfig, widgetType } from '@shared/models/widget.models'; export const flotDefaultSettings = (chartType: ChartType): Partial => { const settings: Partial = { stack: false, + enableSelection: true, fontColor: '#545454', fontSize: 10, showTooltip: true, @@ -148,6 +149,7 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit // Common settings stack: [false, []], + enableSelection: [true, []], fontSize: [10, [Validators.min(0)]], fontColor: ['#545454', []], @@ -282,6 +284,7 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit this.flotSettingsFormGroup.disable({emitEvent: false}); } else { this.flotSettingsFormGroup.enable({emitEvent: false}); + this.updateValidators(false); } } @@ -377,9 +380,11 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit } else { this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent}); } + this.flotSettingsFormGroup.get('xaxisSecond').enable({emitEvent: false}); } else { this.flotSettingsFormGroup.get('timeForComparison').disable({emitEvent: false}); this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent}); + this.flotSettingsFormGroup.get('xaxisSecond').disable({emitEvent: false}); } if (customLegendEnabled) { this.flotSettingsFormGroup.get('dataKeysListForLabels').enable({emitEvent}); @@ -390,6 +395,7 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit this.flotSettingsFormGroup.get('legendConfig').updateValueAndValidity({emitEvent: false}); this.flotSettingsFormGroup.get('timeForComparison').updateValueAndValidity({emitEvent: false}); this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').updateValueAndValidity({emitEvent: false}); + this.flotSettingsFormGroup.get('xaxisSecond').updateValueAndValidity({emitEvent: false}); this.flotSettingsFormGroup.get('dataKeysListForLabels').updateValueAndValidity({emitEvent: false}); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.ts index 0cd2cad7c7..0cc661cb44 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.ts @@ -47,7 +47,7 @@ export function labelDataKeyValidator(control: AbstractControl): ValidationError @Component({ selector: 'tb-label-data-key', templateUrl: './label-data-key.component.html', - styleUrls: ['./label-data-key.component.scss', './../widget-settings.scss'], + styleUrls: ['./label-data-key.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html index f7e2234afd..10bfa058da 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html @@ -15,20 +15,21 @@ limitations under the License. --> -
-
- - legend.direction - + +
+
{{ 'legend.direction' | translate }}
+ + {{ legendDirectionTranslations.get(legendDirection[direction]) | translate }} - - legend.position - +
+
+
{{ 'legend.position' | translate }}
+ + @@ -37,28 +38,17 @@
-
- - {{ 'legend.show-min' | translate }} - - - {{ 'legend.show-max' | translate }} - -
-
- - {{ 'legend.show-avg' | translate }} - - - {{ 'legend.show-total' | translate }} - -
-
- - {{ 'legend.show-latest' | translate }} - - - {{ 'legend.sort-legend' | translate }} - +
+
legend.show-values
+ + {{ 'legend.min-option' | translate }} + {{ 'legend.max-option' | translate }} + {{ 'legend.average-option' | translate }} + {{ 'legend.total-option' | translate }} + {{ 'legend.latest-option' | translate }} +
- + + {{ 'legend.sort-legend' | translate }} + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts index c8de7872e8..15b2bbe316 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts @@ -15,7 +15,7 @@ /// import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { isDefined } from '@core/utils'; import { LegendConfig, @@ -30,7 +30,7 @@ import { Subscription } from 'rxjs'; @Component({ selector: 'tb-legend-config', templateUrl: './legend-config.component.html', - styleUrls: [], + styleUrls: ['./../widget-settings.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, @@ -62,12 +62,8 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc this.legendConfigForm = this.fb.group({ direction: [null, []], position: [null, []], - sortDataKeys: [null, []], - showMin: [null, []], - showMax: [null, []], - showAvg: [null, []], - showTotal: [null, []], - showLatest: [null, []] + showValues: [[], []], + sortDataKeys: [null, []] }); this.legendSettingsFormDirectionChanges$ = this.legendConfigForm.get('direction').valueChanges .subscribe((direction: LegendDirection) => { @@ -121,18 +117,49 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc this.legendConfigForm.patchValue({ direction: legendConfig.direction, position: legendConfig.position, - sortDataKeys: isDefined(legendConfig.sortDataKeys) ? legendConfig.sortDataKeys : false, - showMin: isDefined(legendConfig.showMin) ? legendConfig.showMin : false, - showMax: isDefined(legendConfig.showMax) ? legendConfig.showMax : false, - showAvg: isDefined(legendConfig.showAvg) ? legendConfig.showAvg : false, - showTotal: isDefined(legendConfig.showTotal) ? legendConfig.showTotal : false, - showLatest: isDefined(legendConfig.showLatest) ? legendConfig.showLatest : false + showValues: this.getShowValues(legendConfig), + sortDataKeys: isDefined(legendConfig.sortDataKeys) ? legendConfig.sortDataKeys : false }, {emitEvent: false}); } this.onDirectionChanged(legendConfig.direction); } private legendConfigUpdated() { - this.propagateChange(this.legendConfigForm.value); + const configValue = this.legendConfigForm.value; + const legendConfig: Partial = { + direction: configValue.direction, + position: configValue.position, + sortDataKeys: configValue.sortDataKeys + }; + this.setShowValues(configValue.showValues, legendConfig); + this.propagateChange(legendConfig); + } + + private getShowValues(legendConfig: LegendConfig): string[] { + const showValues: string[] = []; + if (isDefined(legendConfig.showMin) && legendConfig.showMin) { + showValues.push('min'); + } + if (isDefined(legendConfig.showMax) && legendConfig.showMax) { + showValues.push('max'); + } + if (isDefined(legendConfig.showAvg) && legendConfig.showAvg) { + showValues.push('average'); + } + if (isDefined(legendConfig.showTotal) && legendConfig.showTotal) { + showValues.push('total'); + } + if (isDefined(legendConfig.showLatest) && legendConfig.showLatest) { + showValues.push('latest'); + } + return showValues; + } + + private setShowValues(showValues: string[], legendConfig: Partial) { + legendConfig.showMin = showValues.includes('min'); + legendConfig.showMax = showValues.includes('max'); + legendConfig.showAvg = showValues.includes('average'); + legendConfig.showTotal = showValues.includes('total'); + legendConfig.showLatest = showValues.includes('latest'); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html index 46e3f87664..3cc771e94f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html @@ -15,27 +15,27 @@ limitations under the License. --> -
- - widgets.value-source.value-source - - + +
+ + {{ 'widgets.value-source.predefined-value' | translate }} - - + + {{ 'widgets.value-source.entity-attribute' | translate }} - - - - - widgets.value-source.value - - -
- - {{ 'widgets.value-source.source-entity-alias' | translate }} - + +
+
+
widgets.value-source.value
+ + + +
+
+
widgets.value-source.source-entity-alias
+ + @@ -53,9 +53,11 @@ - - {{ 'widgets.value-source.source-entity-attribute' | translate }} - +
+
widgets.value-source.source-entity-attribute
+ + @@ -73,5 +75,5 @@ -
- +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts index 6417ae4bcb..603b12bcb5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts @@ -14,8 +14,8 @@ /// limitations under the License. /// -import { Component, ElementRef, forwardRef, HostBinding, Input, OnInit, ViewChild } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -39,7 +39,7 @@ export interface ValueSourceProperty { @Component({ selector: 'tb-value-source', templateUrl: './value-source.component.html', - styleUrls: [], + styleUrls: ['./../widget-settings.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, @@ -50,8 +50,6 @@ export interface ValueSourceProperty { }) export class ValueSourceComponent extends PageComponent implements OnInit, ControlValueAccessor { - @HostBinding('style.display') display = 'block'; - @ViewChild('entityAliasInput') entityAliasInput: ElementRef; @ViewChild('keyInput') keyInput: ElementRef; @@ -212,15 +210,13 @@ export class ValueSourceComponent extends PageComponent implements OnInit, Contr private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array): Observable> { return this.aliasController.getAliasInfo(entityAliasId).pipe( - mergeMap((aliasInfo) => { - return this.entityService.getEntityKeysByEntityFilter( + mergeMap((aliasInfo) => this.entityService.getEntityKeysByEntityFilter( aliasInfo.entityFilter, dataKeyTypes, [], {ignoreLoading: true, ignoreErrors: true} ).pipe( catchError(() => of([])) - ); - }), + )), catchError(() => of([] as Array)) ); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.ts index 9baad9677c..8bd92dca8a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.ts @@ -50,7 +50,7 @@ export function fixedColorLevelValidator(control: AbstractControl): ValidationEr @Component({ selector: 'tb-fixed-color-level', templateUrl: './fixed-color-level.component.html', - styleUrls: ['./fixed-color-level.component.scss', './../widget-settings.scss'], + styleUrls: ['./fixed-color-level.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.ts index 33f44f0111..e5c1ec9fbc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.ts @@ -31,7 +31,7 @@ export interface GaugeHighlight { @Component({ selector: 'tb-gauge-highlight', templateUrl: './gauge-highlight.component.html', - styleUrls: ['./gauge-highlight.component.scss', './../widget-settings.scss'], + styleUrls: ['./gauge-highlight.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts index ef334d6ee2..51665a3afa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts @@ -27,7 +27,7 @@ import { IAliasController } from '@core/api/widget-api.models'; @Component({ selector: 'tb-tick-value', templateUrl: './tick-value.component.html', - styleUrls: ['./tick-value.component.scss', './../widget-settings.scss'], + styleUrls: ['./tick-value.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.ts index d6e6d675ad..636fd9f0fe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.ts @@ -56,7 +56,7 @@ export const gpioItemValidator = (hasColor: boolean): ValidatorFn => (control: A @Component({ selector: 'tb-gpio-item', templateUrl: './gpio-item.component.html', - styleUrls: ['./gpio-item.component.scss', './../widget-settings.scss'], + styleUrls: ['./gpio-item.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.ts index a546537bf7..1d304aef64 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.ts @@ -46,7 +46,7 @@ export const dataKeySelectOptionValidator = (control: AbstractControl) => { @Component({ selector: 'tb-datakey-select-option', templateUrl: './datakey-select-option.component.html', - styleUrls: ['./datakey-select-option.component.scss', './../widget-settings.scss'], + styleUrls: ['./datakey-select-option.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.scss index 5452b03f3b..1971b02b6c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.scss @@ -16,6 +16,10 @@ @import '../../../../../../../scss/constants'; :host { + display: flex; + flex-direction: column; + gap: 16px; + .tb-widget-settings { .fields-group { padding: 0 16px 8px; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index 9342a2dc4c..78f6203b1b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -388,9 +388,11 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI this.sources.push(source); } } - this.prepareDisplayedColumn(); - this.sources[this.sourceIndex].displayedColumns = - this.displayedColumns[this.sourceIndex].filter(value => value.display).map(value => value.def); + if (this.sources.length) { + this.prepareDisplayedColumn(); + this.sources[this.sourceIndex].displayedColumns = + this.displayedColumns[this.sourceIndex].filter(value => value.display).map(value => value.def); + } this.updateActiveEntityInfo(); } @@ -398,48 +400,50 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI if ($event) { $event.stopPropagation(); } - const target = $event.target || $event.currentTarget; - const config = new OverlayConfig(); - config.backdropClass = 'cdk-overlay-transparent-backdrop'; - config.hasBackdrop = true; - const connectedPosition: ConnectedPosition = { - originX: 'end', - originY: 'bottom', - overlayX: 'end', - overlayY: 'top' - }; - config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) - .withPositions([connectedPosition]); - - const overlayRef = this.overlay.create(config); - overlayRef.backdropClick().subscribe(() => { - overlayRef.dispose(); - }); - const source = this.sources[this.sourceIndex]; - - this.prepareDisplayedColumn(); - - const providers: StaticProvider[] = [ - { - provide: DISPLAY_COLUMNS_PANEL_DATA, - useValue: { - columns: this.displayedColumns[this.sourceIndex], - columnsUpdated: (newColumns) => { - source.displayedColumns = newColumns.filter(value => value.display).map(value => value.def); - this.clearCache(); + if (this.sources.length) { + const target = $event.target || $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const source = this.sources[this.sourceIndex]; + + this.prepareDisplayedColumn(); + + const providers: StaticProvider[] = [ + { + provide: DISPLAY_COLUMNS_PANEL_DATA, + useValue: { + columns: this.displayedColumns[this.sourceIndex], + columnsUpdated: (newColumns) => { + source.displayedColumns = newColumns.filter(value => value.display).map(value => value.def); + this.clearCache(); + } } + }, + { + provide: OverlayRef, + useValue: overlayRef } - }, - { - provide: OverlayRef, - useValue: overlayRef - } - ]; + ]; - const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); - overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent, - this.viewContainerRef, injector)); - this.ctx.detectChanges(); + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); + overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent, + this.viewContainerRef, injector)); + this.ctx.detectChanges(); + } } private prepareDisplayedColumn() { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 1766b79009..c1b150acdf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -17,9 +17,9 @@ -->
- - + +
@@ -31,22 +31,24 @@
-
-
widget-config.card-title
+
+
widget-config.card-title
{{ 'widget-config.display-title' | translate }} -
- - widget-config.title - +
+
widget-config.title
+ + - - widget-config.title-tooltip - +
+
+
widget-config.title-tooltip
+ +
-
+
{{ 'widget-config.display-icon' | translate }} @@ -55,7 +57,7 @@ [color]="widgetSettings.get('iconColor').value" formControlName="titleIcon"> - + @@ -80,10 +82,10 @@
-
-
widget-config.card-style
-
-
{{ 'widget-config.text' | translate }}
+
+
widget-config.card-style
+
+
{{ 'widget-config.text-color' | translate }}
-
-
{{ 'widget-config.background' | translate }}
+
+
{{ 'widget-config.background-color' | translate }}
-
+
{{ 'widget-config.padding' | translate }}
- +
-
+
{{ 'widget-config.margin' | translate }}
- +
+
+
{{ 'widget-config.border-radius' | translate }}
+ + + +
{{ 'widget-config.drop-shadow' | translate }} @@ -124,7 +132,6 @@ @@ -135,15 +142,15 @@
-
-
widget-config.card-buttons
+
+
widget-config.card-buttons
{{ 'widget-config.enable-fullscreen' | translate }}
-
+
-
+
{{ 'widget-config.mobile-hide' | translate }}
-
+
{{ 'widget-config.desktop-hide' | translate }}
-
-
+
+
widget-config.order
- +
-
+
widget-config.height
- +
@@ -205,8 +212,16 @@ formControlName="timewindowConfig"> -
- +
+
+
alarm.filter
+ +
+
-
-
widget-config.target-device
+
widget-config.target-device
-
widget-config.alarm-source
+ [formGroup]="dataSettings" class="tb-form-panel" > +
widget-config.alarm-source
-
-
widget-config.limits
-
+
+
widget-config.limits
+
widget-config.data-page-size
- +
-
-
widget-config.data-settings
-
+
+
widget-config.data-settings
+
widget-config.units
-
+
widget-config.decimals
- +
-
-
widget-config.no-data-display-message
+
+
widget-config.no-data-display-message
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 999e7e55ff..1f418224dd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -224,6 +224,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe color: [null, []], padding: [null, []], margin: [null, []], + borderRadius: [null, []], widgetStyle: [null, []], widgetCss: [null, []], titleStyle: [null, []], @@ -484,6 +485,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe color: config.color, padding: config.padding, margin: config.margin, + borderRadius: config.borderRadius, widgetStyle: isDefined(config.widgetStyle) ? config.widgetStyle : {}, widgetCss: isDefined(config.widgetCss) ? config.widgetCss : '', titleStyle: isDefined(config.titleStyle) ? config.titleStyle : { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html index 870cb08513..9b6c683953 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html @@ -18,6 +18,7 @@ -
- + check @@ -54,133 +57,49 @@ {{ 'device.label-max-length' | translate }} -
- - - device.wizard.existing-device-profile - - - device.wizard.new-device-profile - - -
- - - - device-profile.new-device-profile-name - - - {{ 'device-profile.new-device-profile-name-required' | translate }} - - -
-
- - -
-
- - -
-
-
- + + +
+ {{ 'device.is-gateway' | translate }} - - + + {{ 'device.overwrite-activity-time' | translate }} - +
- + + + device.description - + - -
- {{ 'device-profile.transport-configuration' | translate }} - device-profile.transport-type - - - {{deviceTransportTypeTranslations.get(type) | translate}} - - - - {{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}} - - - {{ 'device-profile.transport-type-required' | translate }} - - - - -
-
- -
- {{'device-profile.alarm-rules-with-count' | translate: - {count: alarmRulesFormGroup.get('alarms').value ? - alarmRulesFormGroup.get('alarms').value.length : 0} }} - - -
-
- -
- {{ 'device-profile.device-provisioning' | translate }} - - -
-
- + {{ 'device.credentials' | translate }}
- {{ 'device.wizard.add-credentials' | translate }}
- - {{ 'customer.customer' | translate }} -
- - -
-
-
-
+
+
@@ -192,7 +111,7 @@ (click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}
-
+
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss index 0fe18467fd..2f35b3e60d 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss @@ -18,49 +18,62 @@ :host { height: 100%; display: grid; + grid-template-rows: min-content 4px auto min-content; - .dialog-actions-row { - padding: 8px; + .toggle-group { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 16px; + margin-bottom: 16px; + } + + @media #{$mat-sm} { + min-width: 470px; + } + + @media #{$mat-gt-sm} { + min-width: 650px; } } -:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { - @media #{$mat-lt-sm} { - .mat-mdc-dialog-content { - max-height: 75vh; +:host-context(.mat-mdc-dialog-container) { + .tb-dialog-actions { + padding: 0; + grid-row: 4; + + .dialog-actions-row { + padding: 8px; + display: flex; + gap: 8px; + justify-content: flex-end; + flex: 1; } } - .invisible{ - visibility: hidden; + .mat-mdc-dialog-content { + grid-row: 3; + padding: 0; } + } :host ::ng-deep { .mat-mdc-dialog-content { - display: flex; - flex-direction: column; - height: 100%; - padding: 0 !important; - .mat-stepper-horizontal { display: flex; height: 100%; overflow: hidden; .mat-horizontal-stepper-wrapper { - flex: 1 1 100%; + width: 100%; } .mat-horizontal-content-container { - height: 680px; + height: 500px; max-height: 100%; - width: 100%;; overflow-y: auto; scrollbar-gutter: stable; - @media #{$mat-gt-sm} { - min-width: 500px; - } } } } diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts index 77916d379c..65e7df1e90 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts @@ -14,201 +14,83 @@ /// limitations under the License. /// -import { Component, Inject, OnDestroy, SkipSelf, ViewChild } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Component, ViewChild } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; -import { - createDeviceProfileConfiguration, - createDeviceProfileTransportConfiguration, - DeviceProfile, - DeviceProfileInfo, - DeviceProfileType, - DeviceProvisionConfiguration, - DeviceProvisionType, - DeviceTransportType, - deviceTransportTypeHintMap, - deviceTransportTypeTranslationMap -} from '@shared/models/device.models'; -import { MatStepper } from '@angular/material/stepper'; -import { AddEntityDialogData } from '@home/models/entity/entity-component.models'; +import { Device, DeviceProfileInfo, DeviceTransportType } from '@shared/models/device.models'; +import { MatStepper, StepperOrientation } from '@angular/material/stepper'; import { BaseData, HasId } from '@shared/models/base-data'; import { EntityType } from '@shared/models/entity-type.models'; -import { DeviceProfileService } from '@core/http/device-profile.service'; -import { EntityId } from '@shared/models/id/entity-id'; -import { Observable, of, Subscription, throwError } from 'rxjs'; -import { catchError, map, mergeMap, tap } from 'rxjs/operators'; +import { Observable, throwError } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; import { DeviceService } from '@core/http/device.service'; -import { ErrorStateMatcher } from '@angular/material/core'; import { StepperSelectionEvent } from '@angular/cdk/stepper'; -import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; +import { BreakpointObserver } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; -import { RuleChainId } from '@shared/models/id/rule-chain-id'; -import { ServiceType } from '@shared/models/queue.models'; import { deepTrim } from '@core/utils'; +import { CustomerId } from '@shared/models/id/customer-id'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'tb-device-wizard', templateUrl: './device-wizard-dialog.component.html', - providers: [], styleUrls: ['./device-wizard-dialog.component.scss'] }) -export class DeviceWizardDialogComponent extends - DialogComponent implements OnDestroy, ErrorStateMatcher { +export class DeviceWizardDialogComponent extends DialogComponent { @ViewChild('addDeviceWizardStepper', {static: true}) addDeviceWizardStepper: MatStepper; - selectedIndex = 0; - - showNext = true; - - createProfile = false; - - entityType = EntityType; - - deviceTransportTypes = Object.values(DeviceTransportType); - - deviceTransportTypeTranslations = deviceTransportTypeTranslationMap; + stepperOrientation: Observable; - deviceTransportTypeHints = deviceTransportTypeHintMap; + stepperLabelPosition: Observable<'bottom' | 'end'>; - deviceWizardFormGroup: UntypedFormGroup; - - transportConfigFormGroup: UntypedFormGroup; - - alarmRulesFormGroup: UntypedFormGroup; + selectedIndex = 0; - provisionConfigFormGroup: UntypedFormGroup; + credentialsOptionalStep = true; - credentialsFormGroup: UntypedFormGroup; + showNext = true; - customerFormGroup: UntypedFormGroup; + entityType = EntityType; - labelPosition: MatStepper['labelPosition'] = 'end'; + deviceWizardFormGroup: FormGroup; - serviceType = ServiceType.TB_RULE_ENGINE; + credentialsFormGroup: FormGroup; - private subscriptions: Subscription[] = []; private currentDeviceProfileTransportType = DeviceTransportType.DEFAULT; constructor(protected store: Store, protected router: Router, - @Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData>, - @SkipSelf() private errorStateMatcher: ErrorStateMatcher, public dialogRef: MatDialogRef, - private deviceProfileService: DeviceProfileService, private deviceService: DeviceService, private breakpointObserver: BreakpointObserver, - private fb: UntypedFormBuilder) { + private fb: FormBuilder) { super(store, router, dialogRef); + + this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-sm']) + .pipe(map(({matches}) => matches ? 'horizontal' : 'vertical')); + + this.stepperLabelPosition = this.breakpointObserver.observe(MediaBreakpoints['gt-sm']) + .pipe(map(({matches}) => matches ? 'end' : 'bottom')); + this.deviceWizardFormGroup = this.fb.group({ name: ['', [Validators.required, Validators.maxLength(255)]], label: ['', Validators.maxLength(255)], gateway: [false], overwriteActivityTime: [false], - addProfileType: [0], + customerId: [null], deviceProfileId: [null, Validators.required], - newDeviceProfileTitle: [{value: null, disabled: true}], - defaultRuleChainId: [{value: null, disabled: true}], - defaultQueueName: [{value: null, disabled: true}], description: [''] } ); - this.subscriptions.push(this.deviceWizardFormGroup.get('addProfileType').valueChanges.subscribe( - (addProfileType: number) => { - if (addProfileType === 0) { - this.deviceWizardFormGroup.get('deviceProfileId').setValidators([Validators.required]); - this.deviceWizardFormGroup.get('deviceProfileId').enable(); - this.deviceWizardFormGroup.get('newDeviceProfileTitle').setValidators(null); - this.deviceWizardFormGroup.get('newDeviceProfileTitle').disable(); - this.deviceWizardFormGroup.get('defaultRuleChainId').disable(); - this.deviceWizardFormGroup.get('defaultQueueName').disable(); - this.deviceWizardFormGroup.updateValueAndValidity(); - this.createProfile = false; - } else { - this.deviceWizardFormGroup.get('deviceProfileId').setValidators(null); - this.deviceWizardFormGroup.get('deviceProfileId').disable(); - this.deviceWizardFormGroup.get('newDeviceProfileTitle').setValidators([Validators.required]); - this.deviceWizardFormGroup.get('newDeviceProfileTitle').enable(); - this.deviceWizardFormGroup.get('defaultRuleChainId').enable(); - this.deviceWizardFormGroup.get('defaultQueueName').enable(); - - this.deviceWizardFormGroup.updateValueAndValidity(); - this.createProfile = true; - } - } - )); - - this.transportConfigFormGroup = this.fb.group( - { - transportType: [DeviceTransportType.DEFAULT, Validators.required], - transportConfiguration: [createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT), Validators.required] - } - ); - - this.subscriptions.push(this.transportConfigFormGroup.get('transportType').valueChanges.subscribe((transportType) => { - this.deviceProfileTransportTypeChanged(transportType); - })); - - this.alarmRulesFormGroup = this.fb.group({ - alarms: [null] - } - ); - - this.provisionConfigFormGroup = this.fb.group( - { - provisionConfiguration: [{ - type: DeviceProvisionType.DISABLED - } as DeviceProvisionConfiguration, [Validators.required]] - } - ); - this.credentialsFormGroup = this.fb.group({ - setCredential: [false], - credential: [{value: null, disabled: true}] - } - ); - - this.subscriptions.push(this.credentialsFormGroup.get('setCredential').valueChanges.subscribe((value) => { - if (value) { - this.credentialsFormGroup.get('credential').enable(); - } else { - this.credentialsFormGroup.get('credential').disable(); - } - })); - - this.customerFormGroup = this.fb.group({ - customerId: [null] + credential: [] } ); - - this.labelPosition = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']) ? 'end' : 'bottom'; - - this.subscriptions.push(this.breakpointObserver - .observe(MediaBreakpoints['gt-sm']) - .subscribe((state: BreakpointState) => { - if (state.matches) { - this.labelPosition = 'end'; - } else { - this.labelPosition = 'bottom'; - } - } - )); - } - - ngOnDestroy() { - super.ngOnDestroy(); - this.subscriptions.forEach(s => s.unsubscribe()); - } - - isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { - const originalErrorState = this.errorStateMatcher.isErrorState(control, form); - const customErrorState = !!(control && control.invalid); - return originalErrorState || customErrorState; } cancel(): void { @@ -224,24 +106,11 @@ export class DeviceWizardDialogComponent extends } getFormLabel(index: number): string { - if (index > 0) { - if (!this.createProfile) { - index += 3; - } - } switch (index) { case 0: return 'device.wizard.device-details'; case 1: - return 'device-profile.transport-configuration'; - case 2: - return 'device-profile.alarm-rules'; - case 3: - return 'device-profile.device-provisioning'; - case 4: return 'device.credentials'; - case 5: - return 'customer.customer'; } } @@ -249,88 +118,30 @@ export class DeviceWizardDialogComponent extends return this.addDeviceWizardStepper?._steps?.length - 1; } - private deviceProfileTransportTypeChanged(deviceTransportType: DeviceTransportType): void { - this.transportConfigFormGroup.patchValue( - {transportConfiguration: createDeviceProfileTransportConfiguration(deviceTransportType)}); - const setCredentialBox = this.credentialsFormGroup.get('setCredential'); - if (deviceTransportType === DeviceTransportType.LWM2M) { - setCredentialBox.patchValue(true); - setCredentialBox.disable(); - } else { - setCredentialBox.patchValue(false); - setCredentialBox.enable(); - } - } - add(): void { if (this.allValid()) { - this.createDeviceProfile().pipe( - mergeMap(profileId => this.createDevice(profileId)), - mergeMap(device => this.saveCredentials(device)) - ).subscribe( - (created) => { - this.dialogRef.close(created); - } + this.createDevice().subscribe( + () => this.dialogRef.close(true) ); } } get deviceTransportType(): DeviceTransportType { - if (this.deviceWizardFormGroup.get('addProfileType').value) { - return this.transportConfigFormGroup.get('transportType').value; - } else { - return this.currentDeviceProfileTransportType; - } + return this.currentDeviceProfileTransportType; } deviceProfileChanged(deviceProfile: DeviceProfileInfo) { if (deviceProfile) { this.currentDeviceProfileTransportType = deviceProfile.transportType; + this.credentialsOptionalStep = this.currentDeviceProfileTransportType !== DeviceTransportType.LWM2M; } } - private createDeviceProfile(): Observable { - if (this.deviceWizardFormGroup.get('addProfileType').value) { - const deviceProvisionConfiguration: DeviceProvisionConfiguration = this.provisionConfigFormGroup.get('provisionConfiguration').value; - const provisionDeviceKey = deviceProvisionConfiguration.provisionDeviceKey; - delete deviceProvisionConfiguration.provisionDeviceKey; - const deviceProfile: DeviceProfile = { - name: this.deviceWizardFormGroup.get('newDeviceProfileTitle').value, - type: DeviceProfileType.DEFAULT, - defaultQueueName: this.deviceWizardFormGroup.get('defaultQueueName').value, - transportType: this.transportConfigFormGroup.get('transportType').value, - provisionType: deviceProvisionConfiguration.type, - provisionDeviceKey, - profileData: { - configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT), - transportConfiguration: this.transportConfigFormGroup.get('transportConfiguration').value, - alarms: this.alarmRulesFormGroup.get('alarms').value, - provisionConfiguration: deviceProvisionConfiguration - } - }; - if (this.deviceWizardFormGroup.get('defaultRuleChainId').value) { - deviceProfile.defaultRuleChainId = new RuleChainId(this.deviceWizardFormGroup.get('defaultRuleChainId').value); - } - return this.deviceProfileService.saveDeviceProfile(deepTrim(deviceProfile)).pipe( - tap((profile) => { - this.currentDeviceProfileTransportType = profile.transportType; - this.deviceWizardFormGroup.patchValue({ - deviceProfileId: profile.id, - addProfileType: 0 - }); - }), - map(profile => profile.id) - ); - } else { - return of(this.deviceWizardFormGroup.get('deviceProfileId').value); - } - } - - private createDevice(profileId): Observable> { - const device = { + private createDevice(): Observable> { + const device: Device = { name: this.deviceWizardFormGroup.get('name').value, label: this.deviceWizardFormGroup.get('label').value, - deviceProfileId: profileId, + deviceProfileId: this.deviceWizardFormGroup.get('deviceProfileId').value, additionalInfo: { gateway: this.deviceWizardFormGroup.get('gateway').value, overwriteActivityTime: this.deviceWizardFormGroup.get('overwriteActivityTime').value, @@ -338,13 +149,22 @@ export class DeviceWizardDialogComponent extends }, customerId: null }; - if (this.customerFormGroup.get('customerId').value) { - device.customerId = { - entityType: EntityType.CUSTOMER, - id: this.customerFormGroup.get('customerId').value - }; + if (this.deviceWizardFormGroup.get('customerId').value) { + device.customerId = new CustomerId(this.deviceWizardFormGroup.get('customerId').value); + } + if (this.addDeviceWizardStepper.steps.last.completed || this.addDeviceWizardStepper.selectedIndex > 0) { + return this.deviceService.saveDeviceWithCredentials(deepTrim(device), deepTrim(this.credentialsFormGroup.value.credential)).pipe( + catchError((e: HttpErrorResponse) => { + if (e.error.message.includes('Device credentials')) { + this.addDeviceWizardStepper.selectedIndex = 1; + } else { + this.addDeviceWizardStepper.selectedIndex = 0; + } + return throwError(() => e); + }) + ); } - return this.data.entitiesTableConfig.saveEntity(deepTrim(device)).pipe( + return this.deviceService.saveDevice(deepTrim(device)).pipe( catchError(e => { this.addDeviceWizardStepper.selectedIndex = 0; return throwError(e); @@ -352,31 +172,8 @@ export class DeviceWizardDialogComponent extends ); } - private saveCredentials(device: BaseData): Observable { - if (this.credentialsFormGroup.get('setCredential').value) { - return this.deviceService.getDeviceCredentials(device.id.id).pipe( - mergeMap( - (deviceCredentials) => { - const deviceCredentialsValue = {...deviceCredentials, ...this.credentialsFormGroup.value.credential}; - return this.deviceService.saveDeviceCredentials(deviceCredentialsValue).pipe( - catchError(e => { - this.addDeviceWizardStepper.selectedIndex = 1; - return this.deviceService.deleteDevice(device.id.id).pipe( - mergeMap(() => { - return throwError(e); - } - )); - }) - ); - } - ), - map(() => true)); - } - return of(true); - } - allValid(): boolean { - if (this.addDeviceWizardStepper.steps.find((item, index) => { + return !this.addDeviceWizardStepper.steps.find((item, index) => { if (item.stepControl.invalid) { item.interacted = true; this.addDeviceWizardStepper.selectedIndex = index; @@ -384,19 +181,11 @@ export class DeviceWizardDialogComponent extends } else { return false; } - } )) { - return false; - } else { - return true; - } + }); } changeStep($event: StepperSelectionEvent): void { this.selectedIndex = $event.selectedIndex; - if (this.selectedIndex === this.maxStepperIndex) { - this.showNext = false; - } else { - this.showNext = true; - } + this.showNext = this.selectedIndex !== this.maxStepperIndex; } } diff --git a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts index 2c79ef7b68..e9787260bb 100644 --- a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts @@ -328,6 +328,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { backgroundColor: string; padding: string; margin: string; + borderRadius: string; title: string; customTranslatedTitle: string; @@ -427,6 +428,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { this.backgroundColor = this.widget.config.backgroundColor || '#fff'; this.padding = this.widget.config.padding || '8px'; this.margin = this.widget.config.margin || '0px'; + this.borderRadius = this.widget.config.borderRadius; this.title = isDefined(this.widgetContext.widgetTitle) && this.widgetContext.widgetTitle.length ? this.widgetContext.widgetTitle : this.widget.config.title; @@ -478,7 +480,8 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { color: this.color, backgroundColor: this.backgroundColor, padding: this.padding, - margin: this.margin}; + margin: this.margin, + borderRadius: this.borderRadius}; if (this.widget.config.widgetStyle) { this.style = {...this.style, ...this.widget.config.widgetStyle}; } diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html index bc5ffd3ae5..7cc06bc4af 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html @@ -37,7 +37,7 @@
- + @@ -59,7 +59,7 @@
-
@@ -146,7 +146,7 @@ admin.oauth2.no-mobile-apps
-
@@ -203,7 +203,7 @@
admin.oauth2.providers
- diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts index 34f35611b4..d274f6728e 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts @@ -570,4 +570,8 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha trackByParams(index: number): number { return index; } + + trackByItem(i, item) { + return item; + } } diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts b/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts index d2c02955c2..3f94a23a30 100644 --- a/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts @@ -251,6 +251,7 @@ const routes: Routes = [ { path: ':dashboardId', component: DashboardPageComponent, + canDeactivate: [ConfirmOnExitGuard], data: { breadcrumb: { labelFunction: dashboardBreadcumbLabelFunction, diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts index 89f66df545..6645dc6bd3 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts @@ -32,6 +32,7 @@ import { UserDashboardAction } from '@shared/models/user-settings.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; @Injectable() export class DashboardResolver implements Resolve { @@ -85,6 +86,7 @@ const routes: Routes = [ { path: ':dashboardId', component: DashboardPageComponent, + canDeactivate: [ConfirmOnExitGuard], data: { breadcrumb: { labelFunction: dashboardBreadcumbLabelFunction, diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts index 01e5584a57..5aa9b0b317 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts @@ -17,10 +17,10 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, - UntypedFormBuilder, - UntypedFormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, ValidationErrors, Validator, Validators @@ -104,9 +104,7 @@ export class DeviceTransportConfigurationComponent implements ControlValueAccess if (configuration) { delete configuration.type; } - setTimeout(() => { - this.deviceTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); - }, 0); + this.deviceTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); } validate(): ValidationErrors | null { diff --git a/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.ts index 29ac39ee6a..26b70396d9 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.ts @@ -127,6 +127,9 @@ export class SnmpDeviceTransportConfigurationComponent implements ControlValueAc this.snmpDeviceTransportConfigurationFormGroup.disable({emitEvent: false}); } else { this.snmpDeviceTransportConfigurationFormGroup.enable({emitEvent: false}); + this.updateDisabledFormValue( + this.snmpDeviceTransportConfigurationFormGroup.get('protocolVersion').value || SnmpDeviceProtocolVersion.V2C + ); } } diff --git a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html index ff32bf68df..d7f964542e 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html @@ -15,49 +15,47 @@ limitations under the License. --> -
- -

{{ 'device.device-credentials' | translate }}

- - -
- - -
-
-
-
- - -
-
- -
- - - {{ 'device.loading-device-credentials' | translate }} - -
-
-
-
- - -
-
+ +

{{ 'device.device-credentials' | translate }}

+ + +
+ + +
+
+
+ + +
+
+ +
+ + + {{ 'device.loading-device-credentials' | translate }} + +
+
+
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.scss new file mode 100644 index 0000000000..f2c168e150 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.scss @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2023 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"; + +:host { + height: 100%; + display: grid; + grid-template-rows: min-content 4px auto min-content; + + @media #{$mat-gt-xs} { + min-width: 420px; + } +} + +:host-context(.mat-mdc-dialog-container) { + .tb-dialog-actions { + grid-row: 4; + display: flex; + gap: 8px; + justify-content: flex-end; + flex: 1; + } + + .mat-mdc-dialog-content { + grid-row: 3; + padding: 24px 24px 4px; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts index 41986b2b03..cb4a795c9e 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts @@ -37,7 +37,7 @@ export interface DeviceCredentialsDialogData { selector: 'tb-device-credentials-dialog', templateUrl: './device-credentials-dialog.component.html', providers: [{provide: ErrorStateMatcher, useExisting: DeviceCredentialsDialogComponent}], - styleUrls: [] + styleUrls: ['./device-credentials-dialog.component.scss'] }) export class DeviceCredentialsDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.html b/ui-ngx/src/app/modules/home/pages/device/device.component.html index e619fa9561..244b1c000b 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.html @@ -133,14 +133,14 @@ required>
-
- +
+ {{ 'device.is-gateway' | translate }} - - + + {{ 'device.overwrite-activity-time' | translate }} - +
device.description diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.scss b/ui-ngx/src/app/modules/home/pages/device/device.component.scss index 66df772d2d..526b1daa58 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.scss +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.scss @@ -14,5 +14,11 @@ * limitations under the License. */ :host { - + .toggle-group { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 16px; + margin-bottom: 16px; + } } diff --git a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts index 83064c27ff..b246bd8d7c 100644 --- a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts @@ -458,10 +458,7 @@ export class DevicesTableConfigResolver implements Resolve>, boolean>(DeviceWizardDialogComponent, { disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - entitiesTableConfig: this.config - } + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] }).afterClosed().subscribe( (res) => { if (res) { diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts b/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts index 9ec7d15aa0..d454e7bc83 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts @@ -241,6 +241,7 @@ const routes: Routes = [ { path: ':dashboardId', component: DashboardPageComponent, + canDeactivate: [ConfirmOnExitGuard], data: { breadcrumb: { labelFunction: dashboardBreadcumbLabelFunction, diff --git a/ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw b/ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw index 9205fddf71..6f9215b4f9 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw +++ b/ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw @@ -48,7 +48,7 @@ "padding": "16px", "settings": { "useMarkdownTextFunction": false, - "markdownTextPattern": "
\n
\n
{{ 'widgets.activity.title' | translate }}
\n \n \n
\n \n \n \n \n \n \n \n \n
", + "markdownTextPattern": "
\n
\n
{{ 'widgets.activity.title' | translate }}
\n \n {{ 'device.devices' | translate }}\n {{ 'widgets.transport-messages.title' | translate }}\n \n
\n \n \n \n \n \n \n \n \n
", "applyDefaultMarkdownStyle": false, "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n" }, @@ -1101,4 +1101,4 @@ }, "externalId": null, "name": "Tenant Administrator Home Page" -} \ No newline at end of file +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts index 4846cd063d..57ee325e90 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts @@ -162,7 +162,6 @@ export class SentNotificationDialogComponent extends } this.notificationRequestForm.get('useTemplate').setValue(useTemplate, {onlySelf : true}); } - this.refreshAllowDeliveryMethod(); } @@ -337,19 +336,23 @@ export class SentNotificationDialogComponent extends refreshAllowDeliveryMethod() { this.notificationService.getAvailableDeliveryMethods({ignoreLoading: true}).subscribe(allowMethods => { this.allowNotificationDeliveryMethods = allowMethods; - this.updateDeliveryMethodsDisableState(); - this.showRefresh = (this.notificationDeliveryMethods.length !== allowMethods.length); + if (!this.notificationRequestForm.get('useTemplate').value) { + this.updateDeliveryMethodsDisableState(); + this.showRefresh = (this.notificationDeliveryMethods.length !== allowMethods.length); + } }); } private updateDeliveryMethodsDisableState() { - this.notificationDeliveryMethods.forEach(method => { - if (this.allowNotificationDeliveryMethods.includes(method)) { - this.getDeliveryMethodsTemplatesControl(method).enable({emitEvent: true}); - } else { - this.getDeliveryMethodsTemplatesControl(method).disable({emitEvent: true}); - this.getDeliveryMethodsTemplatesControl(method).setValue(false, {emitEvent: true}); //used for notify again - } - }); + if (this.allowNotificationDeliveryMethods) { + this.notificationDeliveryMethods.forEach(method => { + if (this.allowNotificationDeliveryMethods.includes(method)) { + this.getDeliveryMethodsTemplatesControl(method).enable({emitEvent: true}); + } else { + this.getDeliveryMethodsTemplatesControl(method).disable({emitEvent: true}); + this.getDeliveryMethodsTemplatesControl(method).setValue(false, {emitEvent: true}); //used for notify again + } + }); + } } } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss index bb79851da4..b109b4753d 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss @@ -182,6 +182,7 @@ .fc-node { border-radius: 8px; &.fc-selected { + z-index: 2; &:not(.fc-edit) { margin: -3px; border: solid 3px #f00; diff --git a/ui-ngx/src/app/shared/components/color-input.component.html b/ui-ngx/src/app/shared/components/color-input.component.html index 492a2419eb..2a283f7d93 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.html +++ b/ui-ngx/src/app/shared/components/color-input.component.html @@ -20,10 +20,10 @@ {{icon}} {{label}} -
+
- + -
{{alarmSeverityTranslations.get(notification.info.alarmSeverity) | translate}} diff --git a/ui-ngx/src/app/shared/components/notification/notification.component.ts b/ui-ngx/src/app/shared/components/notification/notification.component.ts index ad543c43c1..6e1ff508a7 100644 --- a/ui-ngx/src/app/shared/components/notification/notification.component.ts +++ b/ui-ngx/src/app/shared/components/notification/notification.component.ts @@ -139,7 +139,7 @@ export class NotificationComponent implements OnInit { } notificationColor(): string { - if (this.notification.type === NotificationType.ALARM) { + if (this.notification.type === NotificationType.ALARM && !this.notification.info.cleared) { return AlarmSeverityNotificationColors.get(this.notification.info.alarmSeverity); } return 'transparent'; diff --git a/ui-ngx/src/app/shared/components/public-api.ts b/ui-ngx/src/app/shared/components/public-api.ts index 9ec226769a..32c709ecb0 100644 --- a/ui-ngx/src/app/shared/components/public-api.ts +++ b/ui-ngx/src/app/shared/components/public-api.ts @@ -24,3 +24,4 @@ export * from './slack-conversation-autocomplete.component'; export * from './notification/template-autocomplete.component'; export * from './resource/resource-autocomplete.component'; export * from './toggle-header.component'; +export * from './toggle-select.component'; diff --git a/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.html b/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.html index 2b9c227c30..1d699b5857 100644 --- a/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.html +++ b/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.html @@ -15,13 +15,17 @@ limitations under the License. --> - - - {{ruleChain.name}} - - + + settings_ethernet + + + {{ruleChain.name}} + + + diff --git a/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.scss b/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.scss index c538da5725..c69661ad76 100644 --- a/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.scss +++ b/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.scss @@ -19,8 +19,16 @@ padding: 0 6px; .tb-rule-chain-select { display: flex; - height: 48px; min-height: 100%; pointer-events: all; } } + +:host ::ng-deep { + .mat-mdc-form-field.tb-rule-select .mdc-text-field { + .mat-mdc-form-field-infix { + min-height: 48px; + padding: 12px 0; + } + } +} diff --git a/ui-ngx/src/app/shared/components/tb-error.component.ts b/ui-ngx/src/app/shared/components/tb-error.component.ts index 08959e2949..5ddd2d7896 100644 --- a/ui-ngx/src/app/shared/components/tb-error.component.ts +++ b/ui-ngx/src/app/shared/components/tb-error.component.ts @@ -16,11 +16,12 @@ import { Component, Input } from '@angular/core'; import { animate, state, style, transition, trigger } from '@angular/animations'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-error', template: ` -
+
{{message}} @@ -51,6 +52,10 @@ export class TbErrorComponent { state: any; message; + @Input() + @coerceBoolean() + noMargin = false; + @Input() set error(value) { if (value && !this.message) { diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.html b/ui-ngx/src/app/shared/components/toggle-header.component.html index e36cdbd592..d7ed76de90 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.html +++ b/ui-ngx/src/app/shared/components/toggle-header.component.html @@ -18,13 +18,14 @@ - {{ option.name }} + {{ option.name }} - + {{ option.name }} diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.scss b/ui-ngx/src/app/shared/components/toggle-header.component.scss index a9542012d4..6a6785c11b 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.scss +++ b/ui-ngx/src/app/shared/components/toggle-header.component.scss @@ -80,6 +80,33 @@ } } } + &.tb-disabled { + pointer-events: none; + background: rgba(0, 0, 0, 0.03); + + .mat-button-toggle.mat-button-toggle-appearance-standard { + color: rgba(0, 0, 0, 0.28); + + &.mat-button-toggle-checked { + .mat-button-toggle-button { + background: transparent; + color: rgba(0, 0, 0, 0.38); + border-color: rgba(0, 0, 0, 0.38); + } + } + } + &.tb-fill { + .mat-button-toggle.mat-button-toggle-appearance-standard { + &.mat-button-toggle-checked { + .mat-button-toggle-button { + background: rgba(0, 0, 0, 0.12); + color: rgba(0, 0, 0, 0.38); + border: transparent; + } + } + } + } + } } @media #{$mat-md-lg} { .mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header:not(.tb-ignore-md-lg) { diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.ts b/ui-ngx/src/app/shared/components/toggle-header.component.ts index a7f37f53a4..35daad0e3f 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.ts +++ b/ui-ngx/src/app/shared/components/toggle-header.component.ts @@ -16,29 +16,26 @@ import { AfterContentInit, - AfterViewInit, ChangeDetectorRef, Component, - ContentChildren, EventEmitter, + ContentChildren, + Directive, + ElementRef, + EventEmitter, Input, - OnInit, Output, - QueryList, - ViewChild + OnDestroy, + OnInit, + Output, + QueryList } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { AdminService } from '@core/http/admin.service'; -import { UpdateMessage } from '@shared/models/settings.models'; -import { getCurrentAuthUser } from '@core/auth/auth.selectors'; -import { Authority } from '@shared/models/authority.enum'; -import { of, Subscription } from 'rxjs'; -import { MatStepper } from '@angular/material/stepper'; -import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button-toggle'; +import { Subject, Subscription } from 'rxjs'; import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; import { coerceBoolean } from '@shared/decorators/coercion'; -import { BreadCrumb } from '@shared/components/breadcrumb'; +import { startWith, takeUntil } from 'rxjs/operators'; export interface ToggleHeaderOption { name: string; @@ -47,12 +44,72 @@ export interface ToggleHeaderOption { export type ToggleHeaderAppearance = 'fill' | 'fill-invert' | 'stroked'; +@Directive( + { + // eslint-disable-next-line @angular-eslint/directive-selector + selector: 'tb-toggle-option', + } +) +// eslint-disable-next-line @angular-eslint/directive-class-suffix +export class ToggleOption { + + @Input() value: any; + + get viewValue(): string { + return (this._element?.nativeElement.textContent || '').trim(); + } + + constructor( + private _element: ElementRef + ) {} +} + +@Directive() +export abstract class _ToggleBase extends PageComponent implements AfterContentInit, OnDestroy { + + @ContentChildren(ToggleOption) toggleOptions: QueryList; + + @Input() + options: ToggleHeaderOption[] = []; + + private _destroyed = new Subject(); + + protected constructor(protected store: Store) { + super(store); + } + + ngAfterContentInit(): void { + this.toggleOptions.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => { + this.syncToggleHeaderOptions(); + }); + } + + ngOnDestroy() { + this._destroyed.next(); + this._destroyed.complete(); + } + + private syncToggleHeaderOptions() { + if (this.toggleOptions?.length) { + this.options.length = 0; + this.toggleOptions.forEach(option => { + this.options.push( + { name: option.viewValue, + value: option.value + } + ); + }); + } + } + +} + @Component({ selector: 'tb-toggle-header', templateUrl: './toggle-header.component.html', styleUrls: ['./toggle-header.component.scss'] }) -export class ToggleHeaderComponent extends PageComponent implements OnInit { +export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterContentInit, OnDestroy { @Input() value: any; @@ -60,9 +117,6 @@ export class ToggleHeaderComponent extends PageComponent implements OnInit { @Output() valueChange = new EventEmitter(); - @Input() - options: ToggleHeaderOption[]; - @Input() name: string; @@ -77,6 +131,10 @@ export class ToggleHeaderComponent extends PageComponent implements OnInit { @Input() appearance: ToggleHeaderAppearance = 'stroked'; + @Input() + @coerceBoolean() + disabled = false; + isMdLg: boolean; private observeBreakpointSubscription: Subscription; diff --git a/ui-ngx/src/app/shared/components/toggle-select.component.html b/ui-ngx/src/app/shared/components/toggle-select.component.html new file mode 100644 index 0000000000..a5ce7778b5 --- /dev/null +++ b/ui-ngx/src/app/shared/components/toggle-select.component.html @@ -0,0 +1,26 @@ + + + diff --git a/ui-ngx/src/app/shared/components/toggle-select.component.ts b/ui-ngx/src/app/shared/components/toggle-select.component.ts new file mode 100644 index 0000000000..8338e04f6a --- /dev/null +++ b/ui-ngx/src/app/shared/components/toggle-select.component.ts @@ -0,0 +1,72 @@ +/// +/// Copyright © 2016-2023 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 } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { _ToggleBase, ToggleHeaderAppearance } from '@shared/components/toggle-header.component'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-toggle-select', + templateUrl: './toggle-select.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ToggleSelectComponent), + multi: true + } + ] +}) +export class ToggleSelectComponent extends _ToggleBase implements ControlValueAccessor { + + @Input() + @coerceBoolean() + disabled: boolean; + + @Input() + appearance: ToggleHeaderAppearance = 'stroked'; + + modelValue: any; + + private propagateChange = null; + + constructor(protected store: Store) { + super(store); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: any): void { + this.modelValue = value; + } + + updateModel(value: any) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/shared/decorators/coercion.ts b/ui-ngx/src/app/shared/decorators/coercion.ts index c60cda730d..d1d00828f1 100644 --- a/ui-ngx/src/app/shared/decorators/coercion.ts +++ b/ui-ngx/src/app/shared/decorators/coercion.ts @@ -22,87 +22,128 @@ import { coerceStringArray as coerceStringArrayAngular } from '@angular/cdk/coercion'; -export const coerceBoolean = () => (target: any, key: string): void => { - const getter = function() { - return this['__' + key]; - }; - - const setter = function(next: any) { - this['__' + key] = coerceBooleanProperty(next); - }; - - Object.defineProperty(target, key, { - get: getter, - set: setter, - enumerable: true, - configurable: true, - }); +export const coerceBoolean = () => (target: any, key: string, propertyDescriptor?: PropertyDescriptor): void => { + if (!!propertyDescriptor && !!propertyDescriptor.set) { + const original = propertyDescriptor.set; + + propertyDescriptor.set = function(next) { + original.apply(this, [coerceBooleanProperty(next)]); + }; + } else { + const getter = function() { + return this['__' + key]; + }; + + const setter = function(next: any) { + this['__' + key] = coerceBooleanProperty(next); + }; + + Object.defineProperty(target, key, { + get: getter, + set: setter, + enumerable: true, + configurable: true, + }); + } }; -export const coerceNumber = () => (target: any, key: string): void => { - const getter = function(): number { - return this['__' + key]; - }; - - const setter = function(next: any) { - this['__' + key] = coerceNumberProperty(next); - }; - - Object.defineProperty(target, key, { - get: getter, - set: setter, - enumerable: true, - configurable: true, - }); +export const coerceNumber = () => (target: any, key: string, propertyDescriptor?: PropertyDescriptor): void => { + if (!!propertyDescriptor && !!propertyDescriptor.set) { + const original = propertyDescriptor.set; + + propertyDescriptor.set = function(next) { + original.apply(this, [coerceNumberProperty(next)]); + }; + } else { + const getter = function() { + return this['__' + key]; + }; + + const setter = function(next: any) { + this['__' + key] = coerceNumberProperty(next); + }; + + Object.defineProperty(target, key, { + get: getter, + set: setter, + enumerable: true, + configurable: true, + }); + } }; -export const coerceCssPixelValue = () => (target: any, key: string): void => { - const getter = function(): string { - return this['__' + key]; - }; - - const setter = function(next: any) { - this['__' + key] = coerceCssPixelValueAngular(next); - }; - - Object.defineProperty(target, key, { - get: getter, - set: setter, - enumerable: true, - configurable: true, - }); +export const coerceCssPixelValue = () => (target: any, key: string, propertyDescriptor?: PropertyDescriptor): void => { + if (!!propertyDescriptor && !!propertyDescriptor.set) { + const original = propertyDescriptor.set; + + propertyDescriptor.set = function(next) { + original.apply(this, [coerceCssPixelValueAngular(next)]); + }; + } else { + const getter = function() { + return this['__' + key]; + }; + + const setter = function(next: any) { + this['__' + key] = coerceCssPixelValueAngular(next); + }; + + Object.defineProperty(target, key, { + get: getter, + set: setter, + enumerable: true, + configurable: true, + }); + } }; -export const coerceArray = () => (target: any, key: string): void => { - const getter = function(): any[] { - return this['__' + key]; - }; - - const setter = function(next: any) { - this['__' + key] = coerceArrayAngular(next); - }; - - Object.defineProperty(target, key, { - get: getter, - set: setter, - enumerable: true, - configurable: true, - }); +export const coerceArray = () => (target: any, key: string, propertyDescriptor?: PropertyDescriptor): void => { + if (!!propertyDescriptor && !!propertyDescriptor.set) { + const original = propertyDescriptor.set; + + propertyDescriptor.set = function(next) { + original.apply(this, [coerceArrayAngular(next)]); + }; + } else { + const getter = function() { + return this['__' + key]; + }; + + const setter = function(next: any) { + this['__' + key] = coerceArrayAngular(next); + }; + + Object.defineProperty(target, key, { + get: getter, + set: setter, + enumerable: true, + configurable: true, + }); + } }; -export const coerceStringArray = (separator?: string | RegExp) => (target: any, key: string): void => { - const getter = function(): string[] { - return this['__' + key]; - }; - - const setter = function(next: any) { - this['__' + key] = coerceStringArrayAngular(next, separator); - }; - - Object.defineProperty(target, key, { - get: getter, - set: setter, - enumerable: true, - configurable: true, - }); +export const coerceStringArray = (separator?: string | RegExp) => + (target: any, key: string, propertyDescriptor?: PropertyDescriptor): void => { + if (!!propertyDescriptor && !!propertyDescriptor.set) { + const original = propertyDescriptor.set; + + propertyDescriptor.set = function(next) { + original.apply(this, [coerceStringArrayAngular(next, separator)]); + }; + } else { + const getter = function() { + return this['__' + key]; + }; + + const setter = function(next: any) { + this['__' + key] = coerceStringArrayAngular(next, separator); + }; + + Object.defineProperty(target, key, { + get: getter, + set: setter, + enumerable: true, + configurable: true, + }); + } }; diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts index 202b25d539..49f780beeb 100644 --- a/ui-ngx/src/app/shared/models/alarm.models.ts +++ b/ui-ngx/src/app/shared/models/alarm.models.ts @@ -145,6 +145,11 @@ export interface AlarmAssignee { email: string; } +export enum AlarmAssigneeOption { + noAssignee = 'noAssignee', + currentUser = 'currentUser' +} + export interface AlarmDataInfo extends AlarmInfo { actionCellButtons?: TableCellButtonActionDescriptor[]; hasActions?: boolean; diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index f7802b3a8b..b338a8031b 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -705,7 +705,7 @@ export interface Device extends BaseData, ExportableEntity { tenantId?: TenantId; customerId?: CustomerId; name: string; - type: string; + type?: string; label: string; firmwareId?: OtaPackageId; softwareId?: OtaPackageId; diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts index 2cef7c6cc2..4d8a9cffcd 100644 --- a/ui-ngx/src/app/shared/models/notification.models.ts +++ b/ui-ngx/src/app/shared/models/notification.models.ts @@ -48,6 +48,8 @@ export interface NotificationInfo { alarmStatus?: AlarmStatus; alarmType?: string; stateEntityId?: EntityId; + acknowledged?: boolean; + cleared?: boolean; } export interface NotificationRequest extends Omit, 'label'> { diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index 7c978fb4c9..5e4e7b3527 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -22,7 +22,15 @@ import { EntityInfo } from '@shared/models/entity.models'; import { EntityType } from '@shared/models/entity-type.models'; import { DataKey, Datasource, DatasourceType } from '@shared/models/widget.models'; import { PageData } from '@shared/models/page/page-data'; -import { isDefined, isEqual } from '@core/utils'; +import { + isArraysEqualIgnoreUndefined, + isDefined, + isDefinedAndNotNull, + isEmpty, + isEqual, + isEqualIgnoreUndefined, + isUndefinedOrNull +} from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; import { AlarmInfo, AlarmSearchStatus, AlarmSeverity } from '../alarm.models'; import { Filter } from '@material-ui/icons'; @@ -721,6 +729,36 @@ export interface AlarmFilterConfig extends AlarmFilter { assignedToCurrentUser?: boolean; } +export const alarmFilterConfigEquals = (filter1?: AlarmFilterConfig, filter2?: AlarmFilterConfig): boolean => { + if (filter1 === filter2) { + return true; + } + if ((isUndefinedOrNull(filter1) || isEmpty(filter1)) && (isUndefinedOrNull(filter2) || isEmpty(filter2))) { + return true; + } else if (isDefinedAndNotNull(filter1) && isDefinedAndNotNull(filter2)) { + if (!isArraysEqualIgnoreUndefined(filter1.typeList, filter2.typeList)) { + return false; + } + if (!isArraysEqualIgnoreUndefined(filter1.statusList, filter2.statusList)) { + return false; + } + if (!isArraysEqualIgnoreUndefined(filter1.severityList, filter2.severityList)) { + return false; + } + if (!isEqualIgnoreUndefined(filter1.assigneeId, filter2.assigneeId)) { + return false; + } + if (!isEqualIgnoreUndefined(filter1.searchPropagatedAlarms, filter2.searchPropagatedAlarms)) { + return false; + } + if (!isEqualIgnoreUndefined(filter1.assignedToCurrentUser, filter2.assignedToCurrentUser)) { + return false; + } + return true; + } + return false; +}; + export type AlarmCountQuery = EntityCountQuery & AlarmFilter; export type AlarmDataPageLink = EntityDataPageLink & AlarmFilter; diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index fd2b4f52fa..235df14bb0 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -452,6 +452,15 @@ export const calculateIntervalStartTime = (interval: QuickTimeInterval, currentD case QuickTimeInterval.PREVIOUS_MONTH: currentDate.subtract(1, 'months'); return currentDate.startOf('month'); + case QuickTimeInterval.PREVIOUS_QUARTER: + currentDate.subtract(1, 'quarter'); + return currentDate.startOf('quarter'); + case QuickTimeInterval.PREVIOUS_HALF_YEAR: + if (currentDate.get('quarter') < 3) { + return currentDate.startOf('year').subtract(2, 'quarters'); + } else { + return currentDate.startOf('year'); + } case QuickTimeInterval.PREVIOUS_YEAR: currentDate.subtract(1, 'years'); return currentDate.startOf('year'); diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index bfe9021926..de4c5332f7 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -318,6 +318,11 @@ export interface DataKey extends KeyInfo { _hash?: number; } +export enum DataKeyConfigMode { + general = 'general', + advanced = 'advanced' +} + export enum DatasourceType { function = 'function', device = 'device', @@ -632,6 +637,7 @@ export interface WidgetConfig { backgroundColor?: string; padding?: string; margin?: string; + borderRadius?: string; widgetStyle?: {[klass: string]: any}; widgetCss?: string; titleStyle?: {[klass: string]: any}; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 2a1bcfab87..cf8b271171 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -191,8 +191,9 @@ import { import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.component'; import { ResourceAutocompleteComponent } from '@shared/components/resource/resource-autocomplete.component'; import { ShortNumberPipe } from '@shared/pipe/short-number.pipe'; -import { ToggleHeaderComponent } from '@shared/components/toggle-header.component'; +import { ToggleHeaderComponent, ToggleOption } from '@shared/components/toggle-header.component'; import { RuleChainSelectComponent } from '@shared/components/rule-chain/rule-chain-select.component'; +import { ToggleSelectComponent } from '@shared/components/toggle-select.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -364,6 +365,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ColorPickerComponent, ResourceAutocompleteComponent, ToggleHeaderComponent, + ToggleOption, + ToggleSelectComponent, RuleChainSelectComponent ], imports: [ @@ -592,6 +595,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ColorPickerComponent, ResourceAutocompleteComponent, ToggleHeaderComponent, + ToggleOption, + ToggleSelectComponent, RuleChainSelectComponent ] }) diff --git a/ui-ngx/src/assets/locale/locale.constant-ca_ES.json b/ui-ngx/src/assets/locale/locale.constant-ca_ES.json index 8c07387adc..349d13da2c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ca_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-ca_ES.json @@ -1376,13 +1376,8 @@ "device-configuration": "Configuració del dispositiu", "transport-configuration": "Configuració del transport", "wizard": { - "device-wizard": "Assistent de dispositiu", "device-details": "Detalls del dispositiu", - "new-device-profile": "Crear un nou perfil de dispositiu", - "existing-device-profile": "Seleccionar un perfil existent", - "specific-configuration": "Configuració específica", - "customer-to-assign-device": "Client al que assignar el dispositiu", - "add-credentials": "Afegir credencial" + "customer-to-assign-device": "Client al que assignar el dispositiu" }, "unassign-devices-from-edge-title": "Està segur de que desitja desassignar {count, plural, =1 {1 dispositivo} other {# dispositivos} }?", "unassign-devices-from-edge-text": "Després de la confirmació, tots els dispositius seleccionats quedaran sense assignar i la vora no podrà accedir a ells." @@ -1404,8 +1399,6 @@ "delete": "Esborrar perfil de dispositiu", "copyId": "Copiar ID de perfil", "name-max-length": "El nom ha de ser inferior a 256", - "new-device-profile-name": "Nom del perfil", - "new-device-profile-name-required": "Cal nom de perfil.", "name": "Nom", "name-required": "Cal nom.", "type": "Tipus de perfil", diff --git a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json index 229a4d7eee..52873b4d70 100644 --- a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json +++ b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json @@ -1018,13 +1018,8 @@ "device-configuration": "Konfigurace zařízení", "transport-configuration": "Konfigurace přenosu", "wizard": { - "device-wizard": "Průvodce zařízením", "device-details": "Detail zařízení", - "new-device-profile": "Vytvořit nový profil zařízení", - "existing-device-profile": "Vybrat existující profil zařízení", - "specific-configuration": "Specifická konfigurace", - "customer-to-assign-device": "Přiřadit zařízení zákazníkovi", - "add-credentials": "Přidat přístupový údaj" + "customer-to-assign-device": "Přiřadit zařízení zákazníkovi" }, "unassign-devices-from-edge-title": "Jste se jisti, že chcete odebrat { count, plural, =1 {1 zařízení} other {# zařízení} }?", "unassign-devices-from-edge-text": "Po potvrzení budou všechna vybraná zařízení odebrána a nebudou pro edge dostupná." @@ -1045,8 +1040,6 @@ "set-default": "Učinit profil zařízení defaultním", "delete": "Smazat profil zařízení", "copyId": "Kopírovat Id profilu zařízení", - "new-device-profile-name": "Název profilu zařízení", - "new-device-profile-name-required": "Název profilu zařízení je povinný.", "name": "Název", "name-required": "Název je povinný.", "type": "Typ profilu", diff --git a/ui-ngx/src/assets/locale/locale.constant-da_DK.json b/ui-ngx/src/assets/locale/locale.constant-da_DK.json index 0f1dd146b1..2c1df70902 100644 --- a/ui-ngx/src/assets/locale/locale.constant-da_DK.json +++ b/ui-ngx/src/assets/locale/locale.constant-da_DK.json @@ -1095,13 +1095,8 @@ "device-configuration": "Enhedskonfiguration", "transport-configuration": "Transportkonfiguration", "wizard": { - "device-wizard": "Enhedsguide", "device-details": "Enhedsoplysninger", - "new-device-profile": "Opret ny enhedsprofil", - "existing-device-profile": "Vælg eksisterende enhedsprofil", - "specific-configuration": "Specifik konfiguration", - "customer-to-assign-device": "Kunden skal tildele enheden", - "add-credential": "Tilføj brugeroplysninger" + "customer-to-assign-device": "Kunden skal tildele enheden" } }, "device-profile": { @@ -1120,8 +1115,6 @@ "set-default": "Gør enhedsprofil standard", "delete": "Slet enhedsprofil", "copyId": "Kopiér enhedsprofil-id", - "new-device-profile-name": "Enhedsprofilnavn", - "new-device-profile-name-required": "Enhedsprofilnavn er påkrævet.", "name": "Navn", "name-required": "Navn er påkrævet.", "type": "Profiltype", 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 60e218674f..c5ec1fca40 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -67,7 +67,8 @@ "more": "More", "less": "Less", "skip": "Skip", - "send": "Send" + "send": "Send", + "reset": "Reset" }, "aggregation": { "aggregation": "Aggregation", @@ -1130,6 +1131,7 @@ }, "datakey": { "settings": "Settings", + "general": "General", "advanced": "Advanced", "key": "Key", "label": "Label", @@ -1187,7 +1189,9 @@ "delta-calculation-result": "Delta calculation result", "delta-calculation-result-previous-value": "Previous value", "delta-calculation-result-delta-absolute": "Delta (absolute)", - "delta-calculation-result-delta-percent": "Delta (percent)" + "delta-calculation-result-delta-percent": "Delta (percent)", + "source": "Source", + "latest": "Latest" }, "datasource": { "type": "Datasource type", @@ -1370,13 +1374,8 @@ "device-configuration": "Device configuration", "transport-configuration": "Transport configuration", "wizard": { - "device-wizard": "Device Wizard", "device-details": "Device details", - "new-device-profile": "Create new device profile", - "existing-device-profile": "Select existing device profile", - "specific-configuration": "Specific configuration", - "customer-to-assign-device": "Customer to assign the device", - "add-credentials": "Add credentials" + "customer-to-assign-device": "Customer to assign the device" }, "unassign-devices-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 device} other {# devices} }?", "unassign-devices-from-edge-text": "After the confirmation all selected devices will be unassigned and won't be accessible by the edge." @@ -1443,8 +1442,6 @@ "delete": "Delete device profile", "copyId": "Copy device profile Id", "name-max-length": "Name should be less than 256", - "new-device-profile-name": "Device profile name", - "new-device-profile-name-required": "Device profile name is required.", "name": "Name", "name-required": "Name is required.", "type": "Profile type", @@ -2779,8 +2776,14 @@ "left-side": "Left side layout" }, "legend": { - "direction": "Legend direction", - "position": "Legend position", + "direction": "Direction", + "position": "Position", + "show-values": "Show values", + "min-option": "Min", + "max-option": "Max", + "average-option": "Average", + "total-option": "Total", + "latest-option": "Latest", "sort-legend": "Sort datakeys in legend", "show-max": "Show max value", "show-min": "Show min value", @@ -4180,6 +4183,7 @@ "enable-fullscreen": "Enable fullscreen", "background-color": "Background color", "text-color": "Text color", + "border-radius": "Border radius", "padding": "Padding", "margin": "Margin", "widget-style": "Widget style", @@ -4242,13 +4246,15 @@ "preview": "Preview", "set": "Set", "set-message": "Set message", - "card-title": "Card title", "advanced-title-style": "Advanced title style", "card-style": "Card style", "text": "Text", "background": "Background", "advanced-widget-style": "Advanced widget style", - "card-buttons": "Card buttons" + "card-buttons": "Card buttons", + "show-card-buttons": "Show card buttons", + "card-appearance": "Card appearance", + "color": "Color" }, "widget-type": { "import": "Import widget type", @@ -4262,6 +4268,8 @@ "chart": { "common-settings": "Common settings", "enable-stacking-mode": "Enable stacking mode", + "selection": "Time range selection", + "enable-selection-mode": "Enable selection mode", "line-shadow-size": "Line shadow size", "display-smooth-lines": "Display smooth (curved) lines", "default-bar-width": "Default bar width for non-aggregated data (milliseconds)", @@ -4269,10 +4277,12 @@ "bar-alignment-left": "Left", "bar-alignment-right": "Right", "bar-alignment-center": "Center", + "default-font": "Default font", "default-font-size": "Default font size", "default-font-color": "Default font color", "thresholds-line-width": "Default line width for all thresholds", "tooltip-settings": "Tooltip settings", + "tooltip": "Tooltip", "show-tooltip": "Show tooltip", "hover-individual-points": "Hover individual points", "show-cumulative-values": "Show cumulative values in stacking mode", @@ -4347,9 +4357,10 @@ "axis-position-right": "Right", "thresholds": "Thresholds", "no-thresholds": "No thresholds configured", - "add-threshold": "Add new threshold", + "add-threshold": "Add threshold", "show-values-for-comparison": "Show historical values for comparison", "comparison-values-label": "Historical values label", + "comparison-line-color": "Comparison line color", "threshold-settings": "Threshold settings", "use-as-threshold": "Use key value as threshold", "threshold-line-width": "Threshold line width", @@ -4368,7 +4379,23 @@ "border-color": "Border color", "legend-settings": "Legend settings", "display-legend": "Display legend", - "labels-font-color": "Labels font color" + "labels-font-color": "Labels font color", + "series": "Series", + "add-series": "Add series", + "series-settings": "Series settings", + "remove-series": "Remove series", + "no-series": "No series configured", + "no-series-error": "At least one series should be specified", + "chart-appearance": "Chart appearance", + "vertical-grid-lines": "Vertical grid lines", + "horizontal-grid-lines": "Horizontal grid lines", + "chart-background": "Chart background", + "grid-lines-color": "Grid lines color", + "border": "Border", + "axis": "Axis", + "vertical-axis": "Vertical axis", + "ticks": "Ticks", + "horizontal-axis": "Horizontal axis" }, "dashboard-state": { "dashboard-state-settings": "Dashboard state settings", @@ -5231,14 +5258,23 @@ "display-alarm-activity": "Display alarm activity", "allow-alarms-assign": "Allow alarms assignment", "columns": "Columns", + "column-settings": "Column settings", "remove-column": "Remove column", "add-column": "Add column", - "no-columns": "No columns configured" + "no-columns": "No columns configured", + "columns-to-display": "Columns to display", + "table-header": "Table header", + "header-buttons": "Header buttons", + "table-buttons": "Table buttons", + "pagination": "Pagination", + "rows": "Rows", + "timeseries-column-error": "At least one timeseries column should be specified", + "table-tabs": "Table tabs" }, "value-source": { "value-source": "Value source", "predefined-value": "Predefined value", - "entity-attribute": "Value taken from entity attribute", + "entity-attribute": "Entity attribute", "value": "Value", "source-entity-alias": "Source entity alias", "source-entity-attribute": "Source entity attribute" diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json index 4689c66bed..6518e03f58 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json @@ -1325,13 +1325,8 @@ "device-configuration": "Configuración del dispositivo", "transport-configuration": "Configuración del transporte", "wizard": { - "device-wizard": "Asistente de dispositivo", "device-details": "Detalles del dispositivo", - "new-device-profile": "Crear un nuevo perfil de dispositivo", - "existing-device-profile": "Seleccionar un perfil existente", - "specific-configuration": "Configuración específica", - "customer-to-assign-device": "Cliente al que asignar el dispositivo", - "add-credentials": "Añadir credencial" + "customer-to-assign-device": "Cliente al que asignar el dispositivo" }, "unassign-devices-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, =1 {1 dispositivo} other {# dispositivos} }?", "unassign-devices-from-edge-text": "Después de la confirmación, todos los dispositivos seleccionados quedarán sin asignar y el Edge no podrá acceder a ellos." @@ -1398,8 +1393,6 @@ "delete": "Borrar perfil de dispositivo", "copyId": "Copiar ID de perfil", "name-max-length": "El nombre debe ser menor de 256", - "new-device-profile-name": "Nombre de perfil", - "new-device-profile-name-required": "Se requiere nombre de perfil.", "name": "Nombre", "name-required": "Se requiere nombre.", "type": "Tipo de perfil", diff --git a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json index 8704210933..19f92e5a7c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json +++ b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json @@ -1053,13 +1053,8 @@ "device-configuration": "Configuration du dipositif", "transport-configuration": "Configuration du transport", "wizard": { - "device-wizard": "Wizard du dispositif", "device-details": "Détails du dispositif", - "new-device-profile": "Créer un nouveau profil de dispositif", - "existing-device-profile": "Choisissez un profile de dispositif existant", - "specific-configuration": "Configuration spécifique", - "customer-to-assign-device": "Client auquel assigner le dispositif", - "add-credentials": "Ajouter identifiants" + "customer-to-assign-device": "Client auquel assigner le dispositif" } }, "device-profile": { @@ -1079,8 +1074,6 @@ "delete": "Supprimer le profil de dispositif", "copyId": "Copier l'Identifiant du profil de dispositif", "name-max-length": "La longueur du nom devrait être moins de 256", - "new-device-profile-name": "Nom du profil de dispositif", - "new-device-profile-name-required": "Nom du profil de dispositif est requis.", "name": "Nom", "name-required": "Nom est requis.", "type": "Type de profile", diff --git a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json index 1462a4f3d1..758482f578 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json +++ b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json @@ -913,13 +913,8 @@ "device-configuration": "장치 설정", "transport-configuration": "전송 설정", "wizard": { - "device-wizard": "장치 마법사", "device-details": "장치 상세 정보", - "new-device-profile": "새로운 장치 프로파일 생성", - "existing-device-profile": "기존 장치 프로파일 선택", - "specific-configuration": "특수 설정", - "customer-to-assign-device": "장치에 할당할 커스터머", - "add-credentials": "크리덴셜 추가" + "customer-to-assign-device": "장치에 할당할 커스터머" } }, "device-profile": { @@ -938,8 +933,6 @@ "set-default": "Make device profile default", "delete": "Delete device profile", "copyId": "Copy device profile Id", - "new-device-profile-name": "장치 프로파일 이름", - "new-device-profile-name-required": "Device profile name is required.", "name": "이름", "name-required": "이름을 입력하세요.", "type": "프로파일 유형", diff --git a/ui-ngx/src/assets/locale/locale.constant-sl_SI.json b/ui-ngx/src/assets/locale/locale.constant-sl_SI.json index 1200732365..8aced0ddc6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-sl_SI.json +++ b/ui-ngx/src/assets/locale/locale.constant-sl_SI.json @@ -913,13 +913,8 @@ "device-configuration": "Device configuration", "transport-configuration": "Transport configuration", "wizard": { - "device-wizard": "Device Wizard", "device-details": "Device details", - "new-device-profile": "Create new device profile", - "existing-device-profile": "Select existing device profile", - "specific-configuration": "Specific configuration", - "customer-to-assign-device": "Customer to assign the device", - "add-credentials": "Add credentials" + "customer-to-assign-device": "Customer to assign the device" } }, "device-profile": { @@ -938,8 +933,6 @@ "set-default": "Make device profile default", "delete": "Delete device profile", "copyId": "Copy device profile Id", - "new-device-profile-name": "Device profile name", - "new-device-profile-name-required": "Device profile name is required.", "name": "Name", "name-required": "Name is required.", "type": "Profile type", diff --git a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json index 1aa6900be7..b175a2d51a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json +++ b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json @@ -1021,13 +1021,8 @@ "device-configuration": "Cihaz yapılandırması", "transport-configuration": "Aktarım yapılandırması", "wizard": { - "device-wizard": "Cihaz Sihirbazı", "device-details": "Cihaz ayrıntıları", - "new-device-profile": "Yeni cihaz profili oluştur", - "existing-device-profile": "Mevcut cihaz profilini seçin", - "specific-configuration": "Özel yapılandırma", - "customer-to-assign-device": "Cihazı atamak için kullanıcı grubu", - "add-credentials": "Kimlik bilgileri ekle" + "customer-to-assign-device": "Cihazı atamak için kullanıcı grubu" }, "unassign-devices-from-edge-title": "{ count, plural, =1 {1 cihazın} other {# cihazın} } atamasını kaldırmak istediğinizden emin misiniz?", "unassign-devices-from-edge-text": "Onaydan sonra, seçilen tüm cihazların ataması kaldırılacak ve uç tarafından erişilemeyecek." @@ -1048,8 +1043,6 @@ "set-default": "Cihaz profilini varsayılan yap", "delete": "Cihaz profilini sil", "copyId": "Cihaz profili kimliğini kopyala", - "new-device-profile-name": "Cihaz profili adı", - "new-device-profile-name-required": "Cihaz profili adı gerekli.", "name": "İsim", "name-required": "İsim gerekli.", "type": "Profil türü", 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 58a214372a..b39e10e46c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -1221,13 +1221,8 @@ "device-configuration": "设备配置", "transport-configuration": "传输配置", "wizard": { - "device-wizard": "设备向导", "device-details": "设备详细信息", - "new-device-profile": "新建设备配置", - "existing-device-profile": "选择已有设备配置", - "specific-configuration": "指定配置", - "customer-to-assign-device": "客户分配设备", - "add-credentials": "添加凭据" + "customer-to-assign-device": "客户分配设备" }, "unassign-devices-from-edge-title": "确定要取消分配 { count, plural, =1 {1 个设备} other {# 个设备} } 吗?", "unassign-devices-from-edge-text": "确认后,设备将被取消分配,边缘将无法访问。" @@ -1292,8 +1287,6 @@ "delete": "删除设备配置", "copyId": "复制设备配置 ID", "name-max-length": "名称长度必须少于256个字符", - "new-device-profile-name": "设备配置名称", - "new-device-profile-name-required": "设备配置名称必填。", "name": "名称", "name-required": "名称是必需的。", "type": "配置类型", diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_TW.json b/ui-ngx/src/assets/locale/locale.constant-zh_TW.json index 2d493961aa..f2cce81824 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_TW.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_TW.json @@ -1134,13 +1134,8 @@ "device-configuration": "設備配置", "transport-configuration": "傳輸配置", "wizard": { - "device-wizard": "設備嚮導", "device-details": "設備詳情", - "new-device-profile": "建立設備協議", - "existing-device-profile": "選擇現有的設備協議", - "specific-configuration": "具體配置", - "customer-to-assign-device": "客戶指定設備", - "add-credentials": "新增驗證資訊" + "customer-to-assign-device": "客戶指定設備" }, "unassign-devices-from-edge-title": "您確定要解除邊緣設備 { count, plural, =1 {1 device} other {# devices} }的指定嗎?", "unassign-devices-from-edge-text": "確認後邊緣指定設備將解除指定及其所有相關資料將無法恢復。" @@ -1205,8 +1200,6 @@ "delete": "刪除設備協議", "copyId": "複製設備協議Id", "name-max-length": "名稱應小於256", - "new-device-profile-name": "設備協議名稱", - "new-device-profile-name-required": "需要設備協議名稱。", "name": "名稱", "name-required": "需要名稱", "type": "協議類型", diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss new file mode 100644 index 0000000000..0d66e09def --- /dev/null +++ b/ui-ngx/src/form.scss @@ -0,0 +1,264 @@ +/** + * Copyright © 2016-2023 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. + */ +.tb-default, .tb-dark { + .tb-form-panel { + box-shadow: 0 0 10px 6px rgba(11, 17, 51, 0.04); + border-radius: 4px; + padding: 16px; + gap: 16px; + display: flex; + flex-direction: column; + color: rgba(0, 0, 0, 0.87); + letter-spacing: 0.15px; + position: relative; + &.no-padding-bottom { + padding-bottom: 0; + } + &.no-padding { + padding: 0; + } + &.stroked { + box-shadow: none; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 6px; + } + &.no-border { + box-shadow: none; + border-radius: 0; + } + &.tb-slide-toggle { + padding: 0; + gap: 0; + > .tb-form-panel-title { + padding-top: 16px; + padding-left: 16px; + } + > .mat-expansion-panel { + padding: 16px; + .mat-expansion-panel-header { + height: 32px; + .mat-slide { + margin: 0; + } + } + } + } + .mat-expansion-panel { + &.tb-settings { + box-shadow: none; + .mat-content { + overflow: visible; + } + > .mat-expansion-panel-header { + font-weight: 500; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.25px; + padding: 0; + .mat-content { + flex: 0; + white-space: nowrap; + } + &.fill-width { + .mat-content { + flex: 1; + } + } + &:hover { + background: none; + } + .mat-expansion-indicator { + height: 32px; + padding: 2px; + } + } + > .mat-expansion-panel-header-description { + align-items: center; + } + > .mat-expansion-panel-content { + > .mat-expansion-panel-body { + display: flex; + flex-direction: column; + gap: 16px; + padding: 16px 0 0 !important; + } + } + .tb-json-object-panel, .tb-css-content-panel { + margin: 0 0 8px; + } + } + .mat-expansion-panel-content { + font: inherit; + } + } + .mat-slide { + margin: 0; + &.margin { + margin: 8px 0; + } + .mdc-form-field>label { + font-weight: 400; + font-size: 16px; + line-height: 24px; + margin-left: 12px; + } + } + } + + .tb-form-panel-title { + font-weight: 500; + font-size: 16px; + } + .tb-form-panel-hint { + font-size: 12px; + color: #808080; + } + .tb-form-row { + height: 100%; + padding-top: 7px; + padding-bottom: 7px; + display: flex; + flex-direction: row; + align-items: center; + gap: 16px; + padding-left: 16px; + padding-right: 12px; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 6px; + &.same-padding { + padding-right: 16px; + } + &.space-between { + justify-content: space-between; + } + .mat-divider-vertical { + height: 56px; + margin-top: -7px; + margin-bottom: -7px; + } + .mat-mdc-form-field { + width: 106px; + &.medium-width { + width: 220px; + } + } + .fixed-title-width { + min-width: 200px; + } + .mat-slide:only-child { + margin: 8px 0; + } + } + + .tb-form-row .mat-mdc-form-field, .mat-mdc-form-field.tb-inline-field { + &.mat-form-field-appearance-fill { + .mdc-text-field--filled:not(.mdc-text-field--disabled) { + &:before { + opacity: 0; + } + .mdc-line-ripple::before { + border-bottom-color: rgba(0, 0, 0, 0.12); + } + } + .mat-mdc-form-field-focus-overlay { + opacity: 0; + } + } + .mat-mdc-text-field-wrapper { + &.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) { + padding-right: 12px; + padding-left: 12px; + &:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(:hover) { + .mdc-notched-outline__leading, .mdc-notched-outline__trailing { + border-color: rgba(0, 0, 0, 0.12); + } + } + .mat-mdc-form-field-infix { + padding-top: 8px; + padding-bottom: 8px; + min-height: 40px; + width: auto; + .mdc-text-field__input, .mat-mdc-select { + font-weight: 400; + font-size: 14px; + line-height: 20px; + } + } + .mat-mdc-form-field-icon-suffix { + height: 40px; + font-size: 14px; + line-height: 40px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.38); + > button.mat-mdc-icon-button { + width: 40px; + height: 40px; + padding: 8px; + .mat-icon { + width: 20px; + height: 20px; + font-size: 20px; + } + } + > .mat-icon { + width: 20px; + height: 20px; + padding: 10px; + font-size: 20px; + } + } + } + } + &.center { + .mat-mdc-text-field-wrapper { + .mat-mdc-form-field-infix { + .mdc-text-field__input { + text-align: center; + } + } + } + } + &.number { + .mat-mdc-text-field-wrapper { + padding-right: 4px; + .mat-mdc-form-field-infix { + input.mdc-text-field__input[type=number]::-webkit-inner-spin-button, + input.mdc-text-field__input[type=number]::-webkit-outer-spin-button { + opacity: 1; + } + } + } + } + &.tb-chips { + .mat-mdc-text-field-wrapper { + &.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) { + .mat-mdc-form-field-infix { + padding-top: 4px; + padding-bottom: 4px; + + .mdc-evolution-chip-set { + min-height: 32px; + + .mdc-evolution-chip { + height: 24px; + } + } + } + } + } + } + } +} diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 760ffe88c6..127d60e3e5 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -224,6 +224,8 @@ div { line-height: var(--mdc-typography-caption-line-height, 20px); font-weight: var(--mdc-typography-caption-font-weight, 400); letter-spacing: var(--mdc-typography-caption-letter-spacing, 0.0333333333em); + color: rgba(0, 0, 0, 0.6); + white-space: normal; } .mat-caption { @@ -1184,165 +1186,4 @@ mat-label { color: inherit; } - // Widget config - - .tb-widget-config-panel { - box-shadow: 0 0 10px 6px rgba(11, 17, 51, 0.04); - border-radius: 4px; - padding: 16px; - gap: 16px; - display: flex; - flex-direction: column; - color: rgba(0, 0, 0, 0.87); - letter-spacing: 0.15px; - position: relative; - &.no-padding-bottom { - padding-bottom: 0; - } - &.stroked { - box-shadow: none; - border: 1px solid rgba(0, 0, 0, 0.12); - border-radius: 6px; - } - .mat-expansion-panel { - &.tb-settings { - box-shadow: none; - .mat-content { - overflow: visible; - } - .mat-expansion-panel-header { - font-weight: 500; - font-size: 16px; - line-height: 24px; - letter-spacing: 0.25px; - padding: 0; - .mat-content { - flex: 0; - white-space: nowrap; - } - &:hover { - background: none; - } - .mat-expansion-indicator { - height: 32px; - padding: 2px; - } - } - .mat-expansion-panel-header-description { - align-items: center; - } - > .mat-expansion-panel-content { - > .mat-expansion-panel-body { - padding: 0; - } - } - .tb-json-object-panel, .tb-css-content-panel { - margin: 0 0 8px; - } - } - .mat-expansion-panel-content { - font: inherit; - } - } - .mat-slide { - margin: 8px 0; - .mdc-form-field>label { - font-weight: 400; - font-size: 16px; - line-height: 24px; - margin-left: 12px; - } - } - } - - .tb-widget-config-panel-title { - font-weight: 500; - font-size: 16px; - } - .tb-widget-config-panel-hint { - font-size: 12px; - color: #808080; - } - .tb-widget-config-row { - height: 56px; - display: flex; - flex-direction: row; - align-items: center; - gap: 16px; - padding-left: 16px; - padding-right: 12px; - border: 1px solid rgba(0, 0, 0, 0.12); - border-radius: 6px; - &.same-padding { - padding-right: 16px; - } - &.space-between { - justify-content: space-between; - } - .mat-divider-vertical { - height: 56px; - } - .mat-mdc-form-field { - width: 80px; - } - } - - .tb-widget-config-row .mat-mdc-form-field, .mat-mdc-form-field.tb-inline-field { - &.mat-form-field-appearance-fill { - .mdc-text-field--filled:not(.mdc-text-field--disabled) { - &:before { - opacity: 0; - } - .mdc-line-ripple::before { - border-bottom-color: rgba(0, 0, 0, 0.12); - } - } - .mat-mdc-form-field-focus-overlay { - opacity: 0; - } - } - .mat-mdc-text-field-wrapper { - &.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) { - padding-right: 12px; - padding-left: 12px; - &:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(:hover) { - .mdc-notched-outline__leading, .mdc-notched-outline__trailing { - border-color: rgba(0, 0, 0, 0.12); - } - } - .mat-mdc-form-field-infix { - padding-top: 7px; - padding-bottom: 7px; - min-height: 38px; - width: auto; - .mdc-text-field__input, .mat-mdc-select { - font-weight: 400; - font-size: 14px; - line-height: 20px; - } - } - } - } - &.center { - .mat-mdc-text-field-wrapper { - .mat-mdc-form-field-infix { - .mdc-text-field__input { - text-align: center; - } - } - } - } - &.number { - .mat-mdc-text-field-wrapper { - padding-right: 4px; - .mat-mdc-form-field-infix { - input.mdc-text-field__input[type=number]::-webkit-inner-spin-button, - input.mdc-text-field__input[type=number]::-webkit-outer-spin-button { - opacity: 1; - } - } - } - } - } - } diff --git a/ui-ngx/src/typings/jquery.flot.typings.d.ts b/ui-ngx/src/typings/jquery.flot.typings.d.ts index 5bcc9b9b3e..e832a36e7a 100644 --- a/ui-ngx/src/typings/jquery.flot.typings.d.ts +++ b/ui-ngx/src/typings/jquery.flot.typings.d.ts @@ -119,6 +119,7 @@ interface JQueryPlotSelection { color?: string; shape?: JQueryPlotSelectionShape; minSize?: number; + touch?: boolean; } interface JQueryPlotSelectionRanges {