diff --git a/application/pom.xml b/application/pom.xml index 17b179c767..112cc82ff7 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT thingsboard application diff --git a/application/src/main/data/json/system/scada_symbols/filter-hp.svg b/application/src/main/data/json/system/scada_symbols/filter-hp.svg index 02328a62ef..6225d70b0e 100644 --- a/application/src/main/data/json/system/scada_symbols/filter-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/filter-hp.svg @@ -163,8 +163,8 @@ }, { "id": "criticalAnimation", - "name": "{i18n:scada.symbol.critical-state-animation}", - "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "name": "{i18n:scada.symbol.warning-critical-state-animation}", + "hint": "{i18n:scada.symbol.warning-critical-state-animation-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", diff --git a/application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg b/application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg index 003c30df02..1d7babc3b0 100644 --- a/application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg @@ -306,8 +306,8 @@ }, { "id": "criticalAnimation", - "name": "{i18n:scada.symbol.critical-state-animation}", - "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "name": "{i18n:scada.symbol.warning-critical-state-animation}", + "hint": "{i18n:scada.symbol.warning-critical-state-animation-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg b/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg index 9ee3ebda66..cff6a97eb9 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg @@ -247,8 +247,8 @@ }, { "id": "criticalAnimation", - "name": "{i18n:scada.symbol.critical-state-animation}", - "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "name": "{i18n:scada.symbol.warning-critical-state-animation}", + "hint": "{i18n:scada.symbol.warning-critical-state-animation-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-valve-hp.svg b/application/src/main/data/json/system/scada_symbols/horizontal-valve-hp.svg index 2e542cecae..754bd24a4f 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-valve-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-valve-hp.svg @@ -272,8 +272,8 @@ }, { "id": "criticalAnimation", - "name": "{i18n:scada.symbol.critical-state-animation}", - "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "name": "{i18n:scada.symbol.warning-critical-state-animation}", + "hint": "{i18n:scada.symbol.warning-critical-state-animation-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", diff --git a/application/src/main/data/json/system/scada_symbols/pool-hp.svg b/application/src/main/data/json/system/scada_symbols/pool-hp.svg index e67280d617..357563a687 100644 --- a/application/src/main/data/json/system/scada_symbols/pool-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/pool-hp.svg @@ -247,8 +247,8 @@ }, { "id": "criticalAnimation", - "name": "{i18n:scada.symbol.critical-state-animation}", - "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "name": "{i18n:scada.symbol.warning-critical-state-animation}", + "hint": "{i18n:scada.symbol.warning-critical-state-animation-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", diff --git a/application/src/main/data/json/system/scada_symbols/pump-hp.svg b/application/src/main/data/json/system/scada_symbols/pump-hp.svg index daaca4ae9c..422a8b5259 100644 --- a/application/src/main/data/json/system/scada_symbols/pump-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/pump-hp.svg @@ -206,8 +206,8 @@ }, { "id": "criticalAnimation", - "name": "{i18n:scada.symbol.critical-state-animation}", - "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "name": "{i18n:scada.symbol.warning-critical-state-animation}", + "hint": "{i18n:scada.symbol.warning-critical-state-animation-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", diff --git a/application/src/main/data/json/system/scada_symbols/sand-filter-hp.svg b/application/src/main/data/json/system/scada_symbols/sand-filter-hp.svg index 01cc98ce0f..773837608e 100644 --- a/application/src/main/data/json/system/scada_symbols/sand-filter-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/sand-filter-hp.svg @@ -279,8 +279,8 @@ }, { "id": "criticalAnimation", - "name": "{i18n:scada.symbol.critical-state-animation}", - "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "name": "{i18n:scada.symbol.warning-critical-state-animation}", + "hint": "{i18n:scada.symbol.warning-critical-state-animation-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", diff --git a/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg b/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg index 8a86100091..1f635fc810 100644 --- a/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg @@ -247,8 +247,8 @@ }, { "id": "criticalAnimation", - "name": "{i18n:scada.symbol.critical-state-animation}", - "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "name": "{i18n:scada.symbol.warning-critical-state-animation}", + "hint": "{i18n:scada.symbol.warning-critical-state-animation-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", diff --git a/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg b/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg index 4a7bbb3fdf..3e29a4e657 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg @@ -247,8 +247,8 @@ }, { "id": "criticalAnimation", - "name": "{i18n:scada.symbol.critical-state-animation}", - "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "name": "{i18n:scada.symbol.warning-critical-state-animation}", + "hint": "{i18n:scada.symbol.warning-critical-state-animation-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", diff --git a/application/src/main/data/json/system/scada_symbols/vertical-valve-hp.svg b/application/src/main/data/json/system/scada_symbols/vertical-valve-hp.svg index 200e93cb95..6c2683c123 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-valve-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-valve-hp.svg @@ -272,8 +272,8 @@ }, { "id": "criticalAnimation", - "name": "{i18n:scada.symbol.critical-state-animation}", - "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "name": "{i18n:scada.symbol.warning-critical-state-animation}", + "hint": "{i18n:scada.symbol.warning-critical-state-animation-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", diff --git a/application/src/main/data/json/system/widget_bundles/buttons.json b/application/src/main/data/json/system/widget_bundles/buttons.json index 115db85e66..facca89336 100644 --- a/application/src/main/data/json/system/widget_bundles/buttons.json +++ b/application/src/main/data/json/system/widget_bundles/buttons.json @@ -11,6 +11,7 @@ "action_button", "command_button", "toggle_button", + "two_segment_button", "power_button" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json index f61219bfe6..c3dc219f5b 100644 --- a/application/src/main/data/json/system/widget_bundles/control_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json @@ -11,6 +11,7 @@ "single_switch", "command_button", "toggle_button", + "two_segment_button", "power_button", "slider", "control_widgets.switch_control", diff --git a/application/src/main/data/json/system/widget_bundles/general_high_performance_scada_symbols.json b/application/src/main/data/json/system/widget_bundles/general_high_performance_scada_symbols.json index dd5ad38e12..ffb0fee835 100644 --- a/application/src/main/data/json/system/widget_bundles/general_high_performance_scada_symbols.json +++ b/application/src/main/data/json/system/widget_bundles/general_high_performance_scada_symbols.json @@ -5,7 +5,7 @@ "image": "tb-image:aHBfc2NhZGFfZ2VuZXJhbF9zeW1ib2xzX3N5c3RlbV9idW5kbGVfaW1hZ2UucG5n:IkdlbmVyYWwgaGlnaC1wZXJmb3JtYW5jZSBTQ0FEQSBzdW1ib2xzIiBzeXN0ZW0gYnVuZGxlIGltYWdl:SU1BR0U=;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAA8FBMVEXe3t7f39/f39/X19cAAADf39/e3t7d3d3e3t6VlZXI3/eZmZlmZmbGxsbr6+vc3Nytra3Y2Nikrri3xtZ9fX0aGho1NTW5ubnS0tKUlJS/v7+urq6zs7Pa2tqJiYm0tLR5eXnPz8/b29t8fHyhoaJxcXImJia+vr5WVlZjY2NNTU0wMDCpqamzx9zCx9GxsbGEhIRZWVlubm7Q09jJycmLmbhvg6tBQUG3ydvR0dEbPoW90+qzu8spSosNM3+nsMWZpb6TlpkAKHhhZWpfX19UVFRGRkbU1NR9jrKmpqZTbJ43VZFdZm5qampFYZh4foRgw9f8AAAACHRSTlPvvyAgAK+wr43PeosAAAVgSURBVHja7d1pV9pAGIZhuj9TMw1t02RiZawkYYlKRSm21qWb3Zf//286Y8RCgBgIlBecWwkh+sHrvIF45hyldPfOrRKWvXt37pbuLD9DpRgPsBKtwGmVVFoVCAyEWjcWsreftAdiTQp5YSU5IJaBgFgGAmIZCIhlICCWgWDSBJ9hYnEQyWbboiD8OWZayNFruSFlAzEQopCd9aSdohAh9NHFQbYeJW0UhVgMgIv+IrHMEMFCACHjCGtSS8pltS9CIBTcC0OhH+odshDJOXfBY2ZJRFHoiucJxI3kof4KXM4OWVRzpAu9E1OFxJZluXAc7ikOLx8+h8uRTEnKS4iFqx24gigkObWstuNIRBZz+yDMGQGJOWmIjACOGvBvIgIWEy5Q64O01Q7VUyuBIIpjCcc9jEPImOtj8WF8cbh/Iq7lSqKQsbkYjll0X35XH7JsF0QDoQKxbegPfX+5kxzVx5cI8uXbWkbfHkMlvPWMNr9QgNgv1zLzbAAeMvNmA3nyOunJNBDb3smGbEBJ1pHZ5mwgTx8mTQXBtRDbtpcAYg9ORH7+LFOQl2mI/8tH53zTiShB7BRkW0rX3/8kMyAbEZwd/1f9fJMYpNoPqalPue1sZ0BkiB3Hj+rn9XlAdn++nQqCFGRbQz4rTx+kqiB9P7TvY0duSHzCHCC778XHt7OYSCOOG2mIPQgRn/zzzpwgu+8BJZkKUlWQvl44a75cczImgo66/okO6nOA/IGK/yx4aqVLQ9IRm0gOSHX0dYTSc8SGgpSvn4jt/a8L4m70duqX33o2ZN1WdbaQUaez8OsIFKTCvfHVOxUNwdambuNis6Fv+rNXx6YBqTzOqFK1dehlA7Yq2VxFAZIlqVQqVQUBbPTXUxCDXEiuMMlepefoDcQecCRbOpAQl5JqtaI0A1Wr6qbSCsDGUIQmAnaVPz6WO4lFQZLs60LeFgyBrS1pD3r3SwS5tOhN8uDKhjyRggxVCEEKck0GMj4DKZSBjM9ACmUg41tRyMFZCxe1sPjyQR4jaQDSPf1+1AXw9fTkAAsvH8QPkdQHaZ220PpwgFdHFAaSE7JnRS+h6occvAOgID9eteYh4Zik/BDL2tsahOi634HTk5OjLmZesO0jf/khOn8rBXmnz6oP73BweoxZFzx7lp8yIcSKvAHI8UkLCtKCOr1QrDfldM1nzzSlPBRHYcjeOvohrZNjQCHOgKOzYoyoIVmqtobUApZONpuiGGT/ZerJfvS72+2eqfPr6/cTFKrpY6hAM95gRKxZBLIfPkYK8kp3DBx3vx6gSLyJERDFGOfm00PkFpCCzK6QYThvHANgDKlyQxRjjhDGCn8/jV8aDURlIAZiIAYyPwhjBmIgBmIgBmIgU0I4Y80mY2LpIaJ2sSzCMZwMMFFNb5EQBBoSYETbNYEJ8hpYKETUxgzEV778611h0OazgBzvJoWYuGDcQNTCj8i93sXKwCwg6yLpG6YYybiBjBGi6SNnRf4QZvKCaMxAkpFkr3dRgjCG4XgQNBpB4F273kUdknXcQAzEQAzEQAzEQAzkhkG8etIarlpOyIj/r2UgNwMytHy0rJCh5SNCEC/KXKfKXj6iBEHDG+lr5Fk+IgXhjWbWOlX28hElCFBmQ5VzLx9RgkxUECFvtCFkryOXGUhGBmIgKgPJ6CZDGkkBpo4GhGw3FSJCDsBjZVBrQojF2oBz6FiuAK0mhLTDNrhGtCVoNSGEMw7Z1jsctCph4hxyb9kxLSQCwaaAhDEASW0sU0DgSlF2V+A5Am7F7ecg1k29stPNQKhlINRamTfWLd3CSnS/dHslRqIYd2/fw7JXunX77l/CoXsjeoRJ3gAAAABJRU5ErkJggg==", "scada": true, "description": "Bundle with high-performance SCADA symbols", - "order": 9500, + "order": 9400, "name": "General high-performance SCADA symbols" }, "widgetTypeFqns": [ diff --git a/application/src/main/data/json/system/widget_bundles/high_performance_scada_fluid_system.json b/application/src/main/data/json/system/widget_bundles/high_performance_scada_fluid_system.json index c30667e6de..1e41b22446 100644 --- a/application/src/main/data/json/system/widget_bundles/high_performance_scada_fluid_system.json +++ b/application/src/main/data/json/system/widget_bundles/high_performance_scada_fluid_system.json @@ -5,7 +5,7 @@ "image": "tb-image:aHBfc2NhZGFfZmx1aWRfc3lzdGVtX2J1bmRsZV9pbWFnZS5wbmc=:IkhpZ2gtcGVyZm9ybWFuY2UgU0NBREEgZmx1aWQgc3lzdGVtIiBzeXN0ZW0gYnVuZGxlIGltYWdl:SU1BR0U=;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAABNVBMVEXe3t7f39/X19cAAADf39/e3t7f39/e3t7e3t7////I3/fr6+vHx8esrKzj4+OcrsHT0tKVlZWvr6/Kysrx8fF0dHT9/f24uLjBwcHc29v39/etra27u7ubm5vt7e2/v7/Dw8OysrKqqqq+1ev6+vrE2/Lh4eHV1dWoqKiQkJCmpqa1yt+2trbu7e2YmJi/1eyurq7B2O9ycXFlZWXT09Ojo6PMzMzIyMjm5eWBgYGenp78/Py0tLSTk5OLi4uwxdq6z+Wous6ioqKGhoZjY2OgoKBoaGheXl5WVlasv9SorrR/fn7R0dGlpaVSUlKhrr13d3evuseTpLWWoq+fn59sbGw+Pj6rv9Tv8veitci4uryAk7vf5O7P1+WQocRgeasvLy8hISG/yd2frsx/k7tvhrNAXZmZOz+XAAAACHRSTlPvICAAv7Cvv9cI9yEAAAzySURBVHja7JttT9pQGIaNe8lTejg9dDRqJEElwFxNN1lIZNko3RrRuoAvUaOf/P+/YkecPq0v0PY5lZZ48dUvV06vm54Ylt4tv1+CovNx+d3ScvE1JFLjAywE8R6rNcg9SzNEama36/R/raxLVmazsfrTrMFcmCYiNrdAb7VX6ht6u91e0+MhdkwGBNSLdDd0ZK/V0mOzYwIBxSLiJ+hhEc71+EBdAAGVIuKzTuIzwUSpyKoeZa/FE5oAAXUijo609D0UiQ+hE3UiTOhIvf5Jb0kSimwIoEIX2dTDIp9wfpOwCXGpHHyfwqiSXgSHNzy/mR1J5RtMYT29SE1HEseOmHMX+aIroT53kdVnToTvJRbZmbtINJE1+bl9tAooYpTCGIZbakhKSTFyJnLlHZZs2x4UXmRC8+vXRRHZXQwRyWKILMCJNO2GvRAiJds7VDy/XZqImVLEHaie35s/LCrSX59CPyrChhe5mV+LW1ZYhVUi+H4lAgNEHPWqVm5itzSN+87LybwcRd9nmmblZn4tTbJ1YCYVMc9XNEmuTuSW7WOWRIQNt6saRWTQcKXIblO5iMatIzZbBOPQNJLIlYHzq1REwg+ceCLObRxEEdt2cX7VikxSqc0WMc+3NMTKz/xqYbb32XQRti/j+E8uY9cwlSki4qjDtRC5m98wbUzlsYjTmcRRiBO5S4U9J2IOMQ6yiDvwshfRqg+poAg7xjhUiGQ4v09TQRFx5GMcKkQuB3amIgg7cFCkG42jAPMbSWXI7kTM4Q/tnkLFfg+XqVQq4tE3R2HmNwzvXYwmcRT7RKSI3+v1MhFxB/YriOAKi8lLCVcu0jyV8zuwbfs1RH4MzfvLR1n5iTQbrza/nS7+kTPKZn53s3u0MA4BYXoWL2DsVbz4Rt7fiza/ZRnHM7BRuVAnwjoOPAJTaSubX9vNVoR3MI6nCEyFKpLp/OL7O/I0FTUijUGG81s+N2EmtVE5540w34FYOCOeY5FqD+OYBZOp5HR+q9tDBnHAi28uT6SGccTFHJVJV91LT70I8/uQAsfn6UUOPeXzi3EkhVkWTytiN1XP7wXGkRz25+a1G/HgBUwg0Y0tMi49It3/R04gW2aLnJSUcAY06CLf1Dxa10CDLiK8yPy6V6lEPCBCF4Egslqul2p+AyBCF4keSVN+Usyvy4AKXQQCeiMVoEMXgbOnIjne3iki4ow4vX+BAF0EEScYRTPpF2JjTPWgiyDB+OHSnvDR8n4DGboIwoKxO9nfRPPbOAsIx6FWBLkOxieeYRinEmM243EQXMNcWIgfuL6J5JE3kX/tneeu0zAUgEEsH8mx3MY0o0naRIiEqK0Q6oDS3h8gJCRA4gcgJJaY7/8I2Bk9GaVJKKOMD6Gbyy3H/myfeKSFU+O/yKnxj4nw/uzBIjY0yY07Dx9eXWuHwHOn1cigPwt/3lWEznyNQRFhrNchfBONZMyn8BOZrjqJPPEp7CNcU9Ek4sNPZdRehJ+5Ar4J1dhhEQo/Fae1iB8LOAilv1OEthSh9xk0wW6wkxd5EkEbAu3ERbCpG2BrccIifCugNWvWKEL38QtE+Ay6ELAGkRXsQYQ/UQQ9jjCpi9D9FfnpIlvoSiBOUWTfJCiCReDOegs36E8Y1Fn/aBERjkeSMRXfLfKE1YJGwU1Odty8Y9Zf4vxIEToynbxAZ9MPv0uEbqp1dM84qdA7q6pQ2kbk7ds3r941icwnlfKk13eIONWXu0nYobudbYLI37oPku/9auy1aBZ5+eHdS3h1WOR6psEN3/dHw8wp9rqKzBiUmDkq6MRl4N2GBBb0VAh6j1WyqFkE3nyCj68PivRp0prBZumAIlxqwySW3lEkKtfuPpcaQVrkLpTJHBXELZsYorlHvrx6/flQjogg0RipUHwKKSvfT2rGuoj4rBT3hgqbu9lzSOEAgVs3oUcnO1NRaV8ANlz6NaZqeLEOIuVM36raziHHyr6ogKxuEoruInUPf1eeZUOCqX4UYys3idQzJOayro91LMgqtJTo8erUuT5SZKIG0AQkaCDh6RSgfijaiiygQECTNvfQhKfxsb94/ANF/KSqnlkRsTzYmTgtRSgtzR+E9JjKDRUQ+2JqFTLoJgNE0GNEGE3TAE0sq9huIiaE83YipfaN08kxM8HGMSBjeuMmIfehgEYVd2oiId2xWtEcBiVcQugU6g3HIWOywho2iCwAYT4hdwBKJrYuf1tYTF92CS3VpQSFDpgqm3fFoYju5e3G1bGJ2UaEG+XAONI8fRcZPTwQi3K+U7pfhF2XvyTXQwbXc+ZiUDnc8fcUhwPATAZJ3EakzwCRCXADv7PSaX2FGTJnAKpLFgKQJ3tFBtg188G4cA1ISIutfZtnItgh6mKCWXJQxAWE3ZTfr1Y611M7007Hq14qit4kfALIep/IoCBy/S6KjAdooppkONRNa5pVe5rlZC5nKiHhELJqIfKsfE9/wLIq65ZKjzS+hR5ZVtwDZNbUI3URHFkaqAzUzUTCSEXyDsk2DiuVx80ii/Kk7gKOWWt6ezq7f3b/eRpQqqXcI+SsqL+3R+4mF04ick2KCMaYkCJ3YYD7GRxZti7DexaEdNTPytFZ5k4IZ00imEjY1IhlPTWXkhtMleQVat4DRPu2yNX3N3IR9uL9C6ZEsEsoITzEOJ6hjx2RnwbYppf7qkxqFgkAmWVeaGYuN+tgubwDlr4rzzyjZMhgh0P3iVyTF/Tq1athJgLsIVPNe21QHFlDKOKNpICdDDEsz7YiQkbNIhogvcwLRZb9W7duPVk+mHhZUH1uAXMIDWv333qP5CiRDCUyqN98cQVq3LolNYYGyy24afuEOEeKnC23UuTRcnJPl8xlSmY3N0qbRK7tEykPLT1taYTC9JaEwcaTWCtT1201lI8SsT2+4jeWaxl4uwywA9qK3G0WCasiI/CUiAWxxbk1tUHSWsQAZJFuFr2VPuf6bds2by2Xs0f3zWW2uMKaDxuH1mBOM5zJtcfjnMe1oZXdf2V36w8A4lu3eri07CISlyd2Vw6hbHNzW7fZ2VKhjaGIIacb0XTXGly7Zmdcu3b32u5yUBLpA9N1k6czoe456eqXQolRq2SfAfIA76vZ7ZY9DZabO2fGFMod5wISVETQJOPu3bvXdqCHmiDo0MrLM+QVc0DiLKGIuCnDNosMAYlwFTU1IcHUYip0sOa8tCZ5AIj/DZHB3UGKvM5c7qo/Km1GJvnqx0yL1YaU+oybhZabq+mmWSQqD35OS6sR3bO8dKniWTqfZrmuykee1UWQpNpKJQcK4H1Vz8ozwEzXvNzUp7gic9osUQQgZ8mmCT2mLN1XWdM8J7m+imQ3sqa1VkVnv8hKVlFkq+p8tWvmu0RbupiWZytdret+xE/3sVMd8gYCj+3WwbiLXAMSOs0iWbdABc4J2aBHUu4K18GS2xbv4c7qoAjxK+v4e+iRDSsF6m4ooQEgwRE7xE1y17QK7QYcKg3nYhUPi7hQ4J468tMho3AOYJvl3TDy7Mit7mbXbrqHxfV3L+Etd4ikTwFhMrO2Aj0kenknuiblDmGLI0TUMHUmUOgQmNupiYHt5rM2IpWF24bjIYkBeWQ0iWllqT8mx4hwjidwlpcYlBtObXRNaCfiisqJKe9hYDxBkaGVR/XM9P5RIuAQvAUXv2RHXSFGbBbhQfUslm9FIaKBk/2aVk4rgNLjRESc/51pLmLnJoa9wmPsBpF6uuNJNa+cKwNTzxucMexN9e4imAQkYjiEbX0nqWao1RRai/CgbnLzbOSBAg+wXRV3MYEiGj1GBE10E0wsLuXxSHlc7/TESpRD3+BE/bNEIaRwO3uoiM8bcL18tAiwmEjcZ3hSgA8V/Wm3Z4g+lIkfEInj3otoyBjdLtybSSW3AkpE9FgRhUh3/X6fpuFNMR/FPBkqrOvD0GrxTMVWcOfm7jKovWpGjhFBTJ8k3BxNJuNJ5PDsIXX35+xxTT281+ME4cMtBQQf0h0tgioIPlTsKoIbDEQY99wHQ2M8HvbkGNsT1SXHiyCm5nCSwZ1gggV2EuEx7IWFNGSwl4g2iHSGjkYOpY4zDo94Uw2NoAN1j9N54xmNunqcqAihbjePkxUhfMugHWJGyQmLEOI2VQOn/tMWIdQV0EjkEHLqIqQXOE2h1i4/EGADP5XHLUVcIceNBg3vjd8eEIkF/ESE3yyC7whkgcH2h9Eo2y30u+fZ8cw3pJUIjbDhjWp9Qk2jIr/uH/wETQPumO7FeNJvgLf93/dKFTccQ9ZdoRmBpoVQYEPJd4J7pTrXV6SBBpF4lBBpddY3JFqdaJRwg3QEPZpNuoss4HsZk248mUwieoBgMknT+dRFaNvP7PwFIph93UUi7Xs5I92IaCMRac25v+UTlecukr+CS+fO/xVdIjUunL9M/nTOXTl/4St0s1h4GZ5DIgAAAABJRU5ErkJggg==", "scada": true, "description": "Bundle with high-performance SCADA symbols for fluid system", - "order": 9600, + "order": 9350, "name": "High-performance SCADA fluid system" }, "widgetTypeFqns": [ diff --git a/application/src/main/data/json/system/widget_types/two_segment_button.json b/application/src/main/data/json/system/widget_types/two_segment_button.json new file mode 100644 index 0000000000..d32d139dce --- /dev/null +++ b/application/src/main/data/json/system/widget_types/two_segment_button.json @@ -0,0 +1,43 @@ +{ + "fqn": "two_segment_button", + "name": "Two-segment button", + "deprecated": false, + "image": "tb-image;/api/images/system/two-segment-button.svg", + "description": "Facilitates navigation to other dashboards, states, or custom actions. Configurable settings allow for on-click action definition and conditions for button activation or deactivation. It offers various layouts and custom styling options for different stat", + "descriptor": { + "type": "latest", + "sizeX": 5, + "sizeY": 1.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true,\n maxDatasources: 1,\n maxDataKeys: 0,\n singleEntity: true,\n previewWidth: '300px',\n previewHeight: '80px',\n embedTitlePanel: true,\n overflowVisible: true,\n hideDataSettings: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-segmented-button-widget-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-segmented-button-basic-config", + "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#E8E8E8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"initialState\":{\"action\":\"DO_NOTHING\",\"defaultValue\":true,\"getAttribute\":{\"key\":\"state\",\"scope\":null},\"getTimeSeries\":{\"key\":\"state\"},\"getAlarmStatus\":{\"severityList\":null,\"typeList\":null},\"dataToValue\":{\"type\":\"NONE\",\"compareToValue\":true,\"dataToValueFunction\":\"/* Should return boolean value */\\nreturn data;\"},\"executeRpc\":{\"method\":null,\"requestTimeout\":null,\"requestPersistent\":null,\"persistentPollingInterval\":null}},\"leftButtonClick\":{\"type\":\"updateDashboardState\",\"targetDashboardStateId\":null,\"openRightLayout\":false,\"setEntityId\":true,\"stateEntityParamName\":null},\"rightButtonClick\":{\"type\":\"updateDashboardState\",\"targetDashboardStateId\":null,\"openRightLayout\":false,\"setEntityId\":true,\"stateEntityParamName\":null},\"disabledState\":{\"action\":\"DO_NOTHING\",\"defaultValue\":false,\"getAttribute\":{\"key\":\"state\",\"scope\":null},\"getTimeSeries\":{\"key\":\"state\"},\"getAlarmStatus\":{\"severityList\":null,\"typeList\":null},\"dataToValue\":{\"type\":\"NONE\",\"compareToValue\":true,\"dataToValueFunction\":\"/* Should return boolean value */\\nreturn data;\"}},\"appearance\":{\"layout\":\"squared\",\"autoScale\":true,\"cardBorder\":1,\"cardBorderColor\":\"#305680\",\"leftAppearance\":{\"showLabel\":true,\"label\":\"Traditional\",\"labelFont\":{\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"size\":14,\"sizeUnit\":\"px\",\"lineHeight\":\"18px\"},\"showIcon\":true,\"icon\":\"rocket_launch\",\"iconSize\":24,\"iconSizeUnit\":\"px\"},\"rightAppearance\":{\"showLabel\":true,\"label\":\"Hi-Perf\",\"labelFont\":{\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"size\":14,\"sizeUnit\":\"px\",\"lineHeight\":\"18px\"},\"showIcon\":true,\"icon\":\"rocket_launch\",\"iconSize\":24,\"iconSizeUnit\":\"px\"},\"selectedStyle\":{\"mainColor\":\"#FFFFFF\",\"backgroundColor\":\"#305680\",\"customStyle\":{\"enabled\":null,\"hovered\":null,\"disabled\":null}},\"unselectedStyle\":{\"mainColor\":\"#000000C2\",\"backgroundColor\":\"#E8E8E8\",\"customStyle\":{\"enabled\":null,\"hovered\":null,\"disabled\":null}}}},\"title\":\"Segmented button\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"datasources\":[],\"actions\":{\"click\":[{\"id\":\"56ebc389-f91d-fc25-36ef-0d18329fbeda\",\"name\":\"onClick\",\"icon\":\"more_horiz\",\"type\":\"doNothing\",\"targetDashboardStateId\":null,\"openRightLayout\":false,\"setEntityId\":true,\"stateEntityParamName\":null,\"openInSeparateDialog\":false,\"openInPopover\":false}]},\"borderRadius\":\"4px\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}}}" + }, + "resources": [ + { + "link": "/api/images/system/two-segment-button.svg", + "title": "\"Two segment button\" system widget image", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "two-segment-button.svg", + "publicResourceKey": "FmYUNvspfrtAsh4lx8vu1fgDA1wBjoC9", + "mediaType": "image/svg+xml", + "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgZmlsbD0ibm9uZSIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogPHJlY3QgeD0iLjM4IiB5PSI1OC4yIiB3aWR0aD0iMTk5LjI1IiBoZWlnaHQ9IjQzLjYxIiByeD0iMi42MyIgc3Ryb2tlPSIjMzA1NjgwIiBzdHJva2Utd2lkdGg9Ii43NSIvPgogPHJlY3QgeD0iMy4wMSIgeT0iNjAuODMiIHdpZHRoPSI5Ni45OSIgaGVpZ2h0PSIzOC4zNSIgcng9IjEuNSIgZmlsbD0iIzMwNTY4MCIvPgogPHBhdGggZD0iTTI5LjI4IDc1LjUyVjgzSDI4di03LjQ4aDEuMjh6bTIuMzUgMHYxLjAyaC01Ljk2di0xLjAyaDUuOTZ6bTEuOTIgMi45OFY4M2gtMS4yM3YtNS41NmgxLjE4bC4wNSAxLjA2em0xLjctMS4xdjEuMTVhMi40NCAyLjQ0IDAgMCAwLTEuMDcuMDYgMS4wNiAxLjA2IDAgMCAwLS42NS42NWMtLjA2LjE2LS4xLjM0LS4xLjUzbC0uMjkuMDJjMC0uMzUuMDQtLjY3LjEtLjk3LjA3LS4zLjE4LS41Ni4zMS0uNzguMTQtLjIzLjMyLS40LjUzLS41M2ExLjQgMS40IDAgMCAxIC45OC0uMTdsLjIuMDR6bTMuOTIgNC40OHYtMi42NWMwLS4yLS4wNC0uMzctLjEtLjUxYS43Ni43NiAwIDAgMC0uMzQtLjM0IDEuMTIgMS4xMiAwIDAgMC0uNTQtLjExYy0uMiAwLS4zOC4wMy0uNTMuMWEuODUuODUgMCAwIDAtLjM0LjI4LjY3LjY3IDAgMCAwLS4xMi40aC0xLjI0YzAtLjIzLjA2LS40NC4xNi0uNjUuMS0uMi4yNi0uMzguNDYtLjU1LjItLjE2LjQ1LS4yOC43My0uMzguMjgtLjA5LjYtLjEzLjk0LS4xMy40MiAwIC44LjA3IDEuMTIuMi4zMi4xNS41OC4zNi43Ni42NC4xOS4yOC4yOC42NC4yOCAxLjA2djIuNDdjMCAuMjYuMDIuNDkuMDYuNjkuMDMuMi4wOS4zNy4xNS41MlY4M2gtMS4yN2EyLjIgMi4yIDAgMCAxLS4xMy0uNWMtLjAzLS4yMi0uMDUtLjQyLS4wNS0uNjJ6bS4xOC0yLjI2LjAxLjc2aC0uODljLS4yMyAwLS40My4wMy0uNi4wNy0uMTguMDQtLjMzLjEtLjQ0LjE5cy0uMi4xOC0uMjYuM2EuODcuODcgMCAwIDAtLjEuMzljMCAuMTUuMDQuMjguMTEuNC4wNy4xMy4xNy4yMi4zLjI5LjEzLjA3LjMuMS40OC4xYTEuMzYgMS4zNiAwIDAgMCAxLjEyLS41NGMuMS0uMTUuMTctLjMuMTctLjQ0bC40LjU1Yy0uMDQuMTQtLjEuMy0uMi40Ni0uMS4xNi0uMjQuMzEtLjQuNDZhMS45NCAxLjk0IDAgMCAxLTEuMzMuNWMtLjM2IDAtLjY5LS4wOC0uOTctLjIyLS4yOS0uMTUtLjUtLjM1LS42Ny0uNmExLjc3IDEuNzcgMCAwIDEtLjA4LTEuNjNjLjEyLS4yMi4yOC0uNDIuNS0uNTcuMjItLjE1LjQ4LS4yNy44LS4zNS4zMS0uMDguNjctLjEyIDEuMDgtLjEyaC45N3ptNS45NCAyLjIzVjc1LjFoMS4yNFY4M2gtMS4xMmwtLjEyLTEuMTV6bS0zLjYyLTEuNTd2LS4xYzAtLjQzLjA1LS44MS4xNS0xLjE2LjEtLjM1LjI0LS42NS40My0uOWExLjkgMS45IDAgMCAxIDEuNi0uNzggMS44NCAxLjg0IDAgMCAxIDEuNTMuNzZjLjE4LjIzLjMzLjUyLjQzLjg1LjExLjM0LjE4LjcuMjMgMS4xMXYuMzVjLS4wNS40LS4xMi43Ni0uMjMgMS4wOS0uMS4zMy0uMjUuNjEtLjQyLjg1YTEuODQgMS44NCAwIDAgMS0xLjU1Ljc1IDEuOTUgMS45NSAwIDAgMS0xLjU5LS44IDIuNzkgMi43OSAwIDAgMS0uNDMtLjljLS4xLS4zNC0uMTUtLjcyLS4xNS0xLjEyem0xLjI0LS4xdi4xYzAgLjI1LjAyLjUuMDcuNzEuMDQuMjIuMTIuNDEuMjIuNTguMS4xNy4yMi4zLjM4LjQuMTYuMDguMzYuMTMuNTguMTMuMjggMCAuNTEtLjA2LjctLjE4cy4zMi0uMy40Mi0uNWMuMS0uMjIuMTgtLjQ1LjIxLS43MXYtLjkzYy0uMDItLjItLjA2LS40LS4xMi0uNTdhMS41MSAxLjUxIDAgMCAwLS4yNi0uNDYgMS4yNCAxLjI0IDAgMCAwLS45NS0uNDJjLS4yMiAwLS40MS4wNS0uNTcuMTUtLjE1LjEtLjI4LjIyLS4zOS40LS4xLjE2LS4xNy4zNi0uMjIuNThzLS4wNy40Ni0uMDcuNzF6bTYuNDMtMi43NFY4M0g0OC4xdi01LjU2aDEuMjR6bS0xLjMyLTEuNDZjMC0uMTkuMDYtLjM1LjE4LS40N2EuNy43IDAgMCAxIC41My0uMTljLjIxIDAgLjM5LjA2LjUxLjIuMTMuMTEuMi4yNy4yLjQ2IDAgLjE4LS4wNy4zNC0uMi40NmEuNzEuNzEgMCAwIDEtLjUxLjE5LjcyLjcyIDAgMCAxLS41My0uMTkuNjMuNjMgMCAwIDEtLjE4LS40NnptNS40MiAxLjQ2di45SDUwLjN2LS45aDMuMTR6bS0yLjIzLTEuMzZoMS4yM3Y1LjM4YzAgLjE3LjAzLjMuMDguNC4wNS4wOS4xMi4xNS4yLjE4LjEuMDMuMi4wNC4zMi4wNGExLjkgMS45IDAgMCAwIC40NC0uMDR2Ljk0YTMuMTcgMy4xNyAwIDAgMS0uODIuMTJjLS4yOCAwLS41NC0uMDUtLjc2LS4xNS0uMjEtLjEtLjM5LS4yNy0uNS0uNWExLjk0IDEuOTQgMCAwIDEtLjItLjkxdi01LjQ2em00LjcgMS4zNlY4M2gtMS4yNXYtNS41NmgxLjI1em0tMS4zMy0xLjQ2YzAtLjE5LjA2LS4zNS4xOS0uNDdhLjcuNyAwIDAgMSAuNTItLjE5Yy4yMiAwIC40LjA2LjUyLjIuMTMuMTEuMTkuMjcuMTkuNDYgMCAuMTgtLjA2LjM0LS4yLjQ2YS43MS43MSAwIDAgMS0uNTEuMTkuNzIuNzIgMCAwIDEtLjUyLS4xOS42My42MyAwIDAgMS0uMTktLjQ2em0yLjYzIDQuM3YtLjEyYzAtLjQuMDYtLjc3LjE4LTEuMTEuMTItLjM1LjI4LS42NS41LS45LjIzLS4yNi41LS40Ni44Mi0uNi4zMi0uMTQuNjgtLjIxIDEuMDktLjIxLjQgMCAuNzcuMDcgMS4wOC4yMS4zMy4xNC42LjM0LjgyLjYuMjIuMjUuNC41NS41MS45LjEyLjM0LjE4LjcxLjE4IDEuMTF2LjEyYzAgLjQtLjA2Ljc3LS4xOCAxLjEyYTIuMzYgMi4zNiAwIDAgMS0yLjQgMS43Yy0uNDEgMC0uNzgtLjA3LTEuMS0uMmEyLjM2IDIuMzYgMCAwIDEtLjgxLS42Yy0uMjItLjI2LS40LS41Ni0uNTEtLjlhMy40NCAzLjQ0IDAgMCAxLS4xOC0xLjEyem0xLjI0LS4xMnYuMTJjMCAuMjUuMDMuNDkuMDguNzEuMDUuMjIuMTMuNDIuMjQuNTkuMTEuMTYuMjUuMy40Mi40LjE4LjA5LjM4LjE0LjYyLjE0YTEuMTggMS4xOCAwIDAgMCAxLjAxLS41NGMuMTEtLjE3LjItLjM3LjI0LS41OS4wNi0uMjIuMDktLjQ2LjA5LS43di0uMTNjMC0uMjQtLjAzLS40OC0uMDktLjdhMS44IDEuOCAwIDAgMC0uMjQtLjU5IDEuMTggMS4xOCAwIDAgMC0xLjAyLS41NWMtLjI0IDAtLjQ0LjA1LS42MS4xNS0uMTcuMS0uMy4yMy0uNDIuNC0uMS4xNy0uMTkuMzctLjI0LjYtLjA1LjIxLS4wOC40NS0uMDguN3ptNi4zOS0xLjUzVjgzSDYzLjZ2LTUuNTZoMS4xN2wuMDcgMS4xOXpNNjQuNjIgODBoLS40YzAtLjQuMDYtLjc2LjE2LTEuMDkuMTEtLjMzLjI2LS42LjQ2LS44NGEyIDIgMCAwIDEgMS42LS43NGMuMjcgMCAuNS4wMy43Mi4xLjIyLjA4LjQuMi41Ni4zNi4xNi4xNy4yOC4zOC4zNi42NS4wOS4yNi4xMy41OC4xMy45NlY4M2gtMS4yNXYtMy42YzAtLjI3LS4wNC0uNDgtLjEyLS42M2EuNjYuNjYgMCAwIDAtLjMzLS4zM2MtLjE0LS4wNy0uMzItLjEtLjU0LS4xYTEuMiAxLjIgMCAwIDAtMSAuNWMtLjEuMTUtLjIuMzItLjI2LjUzLS4wNi4yLS4xLjQxLS4xLjY0em04LjE3IDEuODd2LTIuNjVjMC0uMi0uMDMtLjM3LS4xLS41MWEuNzYuNzYgMCAwIDAtLjMzLS4zNCAxLjEyIDEuMTIgMCAwIDAtLjU1LS4xMWMtLjIgMC0uMzcuMDMtLjUyLjFhLjg1Ljg1IDAgMCAwLS4zNS4yOC42Ny42NyAwIDAgMC0uMTIuNEg2OS42YzAtLjIzLjA1LS40NC4xNi0uNjUuMS0uMi4yNi0uMzguNDYtLjU1LjItLjE2LjQ0LS4yOC43Mi0uMzguMjgtLjA5LjYtLjEzLjk1LS4xMy40MiAwIC43OS4wNyAxLjExLjIuMzMuMTUuNTguMzYuNzcuNjQuMTguMjguMjguNjQuMjggMS4wNnYyLjQ3YzAgLjI2LjAyLjQ5LjA1LjY5LjA0LjIuMDkuMzcuMTYuNTJWODNoLTEuMjdhMi4yIDIuMiAwIDAgMS0uMTQtLjVjLS4wMy0uMjItLjA1LS40Mi0uMDUtLjYyem0uMTgtMi4yNi4wMS43NmgtLjg4Yy0uMjMgMC0uNDQuMDMtLjYxLjA3LS4xOC4wNC0uMzIuMS0uNDQuMTlzLS4yLjE4LS4yNi4zYS44Ny44NyAwIDAgMC0uMDkuMzljMCAuMTUuMDQuMjguMS40LjA3LjEzLjE3LjIyLjMuMjkuMTQuMDcuMy4xLjQ5LjFhMS4zNiAxLjM2IDAgMCAwIDEuMTEtLjU0Yy4xMS0uMTUuMTctLjMuMTgtLjQ0bC40LjU1YTIuMiAyLjIgMCAwIDEtLjYuOTEgMS45NCAxLjk0IDAgMCAxLTEuMzQuNWMtLjM2IDAtLjY4LS4wNy0uOTctLjIxLS4yOC0uMTUtLjUtLjM1LS42Ni0uNmExLjc3IDEuNzcgMCAwIDEtLjA4LTEuNjNjLjExLS4yMi4yOC0uNDIuNS0uNTcuMjEtLjE1LjQ4LS4yNy44LS4zNS4zLS4wOC42Ny0uMTIgMS4wNy0uMTJoLjk3em0zLjg4LTQuNTJWODNINzUuNnYtNy45aDEuMjV6IiBmaWxsPSIjZmZmIi8+CiA8cGF0aCBkPSJNMTM2Ljg2IDc4LjY0djEuMDJoLTMuOTd2LTEuMDJoMy45N3ptLTMuNjUtMy4xMlY4M2gtMS4zdi03LjQ4aDEuM3ptNC42NCAwVjgzaC0xLjI4di03LjQ4aDEuMjh6bTIuOTMgMS45MlY4M2gtMS4yNHYtNS41NmgxLjI0em0tMS4zMy0xLjQ2YzAtLjE5LjA3LS4zNS4xOS0uNDcuMTMtLjEzLjMtLjE5LjUyLS4xOS4yMiAwIC40LjA2LjUyLjIuMTMuMTEuMi4yNy4yLjQ2IDAgLjE4LS4wNy4zNC0uMi40NmEuNzEuNzEgMCAwIDEtLjUyLjE5LjcyLjcyIDAgMCAxLS41Mi0uMTkuNjMuNjMgMCAwIDEtLjE5LS40NnptNS4zNyAzLjMzdi45OWgtMi43MnYtMWgyLjcyem00LjI0LjloLTEuOTV2LTEuMDJoMS45NGMuMzQgMCAuNjItLjA2LjgzLS4xNy4yLS4xLjM2LS4yNi40Ni0uNDUuMS0uMi4xNC0uNDIuMTQtLjY3IDAtLjI0LS4wNS0uNDYtLjE0LS42Ni0uMS0uMjEtLjI1LS4zOC0uNDYtLjVzLS40OS0uMi0uODItLjJoLTEuNTZWODNoLTEuMjl2LTcuNDhoMi44NWMuNTcgMCAxLjA3LjEgMS40Ny4zLjQuMi43Mi40OS45My44NS4yMS4zNS4zMi43Ni4zMiAxLjIyIDAgLjQ4LS4xLjktLjMyIDEuMjQtLjIxLjM1LS41Mi42Mi0uOTMuOC0uNC4xOS0uOS4yOC0xLjQ3LjI4em02LjMyIDIuOWMtLjQxIDAtLjc4LS4wNy0xLjExLS4yYTIuNDYgMi40NiAwIDAgMS0xLjM4LTEuNDQgMy4wMSAzLjAxIDAgMCAxLS4xOC0xLjA2di0uMmMwLS40NC4wNi0uODQuMTktMS4xOS4xMi0uMzUuMy0uNjUuNTMtLjkuMjItLjI2LjQ5LS40NS44LS41OC4zLS4xNC42NC0uMiAxLS4yLjQgMCAuNzUuMDYgMS4wNC4yLjMuMTMuNTUuMzIuNzQuNTYuMi4yNC4zNS41My40NS44Ni4xLjMzLjE1LjcuMTUgMS4xdi41M2gtNC4zdi0uODloMy4wN3YtLjFjMC0uMjItLjA1LS40My0uMTMtLjYyLS4wOC0uMi0uMi0uMzYtLjM3LS40OGExLjEgMS4xIDAgMCAwLS42NS0uMThjLS4yMSAwLS40LjA1LS41NS4xNC0uMTYuMDgtLjMuMi0uNC4zN3MtLjE5LjM2LS4yNS42YTMuNCAzLjQgMCAwIDAtLjA4Ljc3di4yYzAgLjI1LjAzLjQ4LjEuNjkuMDcuMi4xNy4zOC4zLjU0YTEuMzQgMS4zNCAwIDAgMCAxLjEuNWMuMyAwIC41Ny0uMDcuOC0uMTlzLjQzLS4yOS42LS41bC42Ni42MmEyLjM0IDIuMzQgMCAwIDEtMS4xNy44OWMtLjI4LjEtLjYuMTUtLjk2LjE1ek0xNjAgNzguNVY4M2gtMS4yNHYtNS41NmgxLjE5bC4wNSAxLjA2em0xLjctMS4xdjEuMTVhMi40NyAyLjQ3IDAgMCAwLTEuMDcuMDYgMS4wNSAxLjA1IDAgMCAwLS42NS42NWMtLjA2LjE2LS4xLjM0LS4xLjUzbC0uMjkuMDJjMC0uMzUuMDQtLjY3LjEtLjk3LjA3LS4zLjE4LS41Ni4zMS0uNzguMTQtLjIzLjMyLS40LjUzLS41M2ExLjQgMS40IDAgMCAxIC45Ny0uMTdsLjIuMDR6bTIuNzQgNS42aC0xLjI0di02LjFjMC0uNDEuMDgtLjc2LjIzLTEuMDQuMTYtLjI4LjM4LS41LjY3LS42NGEyLjU4IDIuNTggMCAwIDEgMS43NS0uMTNsLS4wMy45NmExLjA0IDEuMDQgMCAwIDAtLjk3LjA2LjcyLjcyIDAgMCAwLS4zMS4zYy0uMDcuMTMtLjEuMy0uMS41Vjgzem0xLjE0LTUuNTZ2LjloLTMuMjR2LS45aDMuMjR6IiBmaWxsPSIjMDAwIiBmaWxsLW9wYWNpdHk9Ii43NiIvPgo8L3N2Zz4K", + "public": true + } + ], + "scada": false, + "tags": [ + "button", + "action", + "navigation", + "navigate", + "dashboard state" + ] +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 8470566af3..2d868a1135 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListenableFuture; import jakarta.mail.MessagingException; +import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.ConstraintViolation; import lombok.Getter; @@ -28,6 +29,7 @@ import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.DataAccessException; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -182,7 +184,9 @@ import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; +import java.io.IOException; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -194,6 +198,7 @@ import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.zip.GZIPOutputStream; import static org.thingsboard.server.common.data.StringUtils.isNotEmpty; import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD; @@ -1008,6 +1013,22 @@ public abstract class BaseController { } } + protected void compressResponseWithGzipIFAccepted(String acceptEncodingHeader, HttpServletResponse response, byte[] content) throws IOException { + if (StringUtils.isNotEmpty(acceptEncodingHeader) && acceptEncodingHeader.contains("gzip")) { + response.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip"); + response.setCharacterEncoding(StandardCharsets.UTF_8.displayName()); + try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream(response.getOutputStream())) { + gzipOutputStream.write(content); + gzipOutputStream.finish(); + } + } else { + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(content); + outputStream.flush(); + } + } + } + protected ResponseEntity response(HttpStatus status) { return ResponseEntity.status(status).build(); } diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index e3e793014d..e50484bed3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -22,8 +22,10 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; @@ -31,6 +33,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; @@ -67,6 +70,7 @@ import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DASHBOARD_ID_PARAM_DESCRIPTION; @@ -149,17 +153,20 @@ public class DashboardController extends BaseController { ) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/dashboard/{dashboardId}") - public Dashboard getDashboardById(@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) + public void getDashboardById(@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) @PathVariable(DASHBOARD_ID) String strDashboardId, @Parameter(description = INCLUDE_RESOURCES_DESCRIPTION) - @RequestParam(value = INCLUDE_RESOURCES, required = false) boolean includeResources) throws ThingsboardException { + @RequestParam(value = INCLUDE_RESOURCES, required = false) boolean includeResources, + @RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader, + HttpServletResponse response) throws Exception { checkParameter(DASHBOARD_ID, strDashboardId); DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); Dashboard dashboard = checkDashboardId(dashboardId, Operation.READ); if (includeResources) { dashboard.setResources(tbResourceService.exportResources(dashboard, getCurrentUser())); } - return dashboard; + response.setContentType(APPLICATION_JSON_VALUE); + compressResponseWithGzipIFAccepted(acceptEncodingHeader, response, JacksonUtil.writeValueAsBytes(dashboard)); } @ApiOperation(value = "Create Or Update Dashboard (saveDashboard)", @@ -171,11 +178,15 @@ public class DashboardController extends BaseController { TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping(value = "/dashboard") - public Dashboard saveDashboard(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the dashboard.") - @RequestBody Dashboard dashboard) throws Exception { + public void saveDashboard(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the dashboard.") + @RequestBody Dashboard dashboard, + @RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader, + HttpServletResponse response) throws Exception { dashboard.setTenantId(getTenantId()); checkEntity(dashboard.getId(), dashboard, Resource.DASHBOARD); - return tbDashboardService.save(dashboard, getCurrentUser()); + var savedDashboard = tbDashboardService.save(dashboard, getCurrentUser()); + response.setContentType(APPLICATION_JSON_VALUE); + compressResponseWithGzipIFAccepted(acceptEncodingHeader, response, JacksonUtil.writeValueAsBytes(savedDashboard)); } @ApiOperation(value = "Delete the Dashboard (deleteDashboard)", @@ -408,12 +419,13 @@ public class DashboardController extends BaseController { "If 'homeDashboardId' parameter is not set on the User and Customer levels then checks the same parameter for the Tenant that owns the user. " + DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/dashboard/home", method = RequestMethod.GET) - @ResponseBody - public HomeDashboard getHomeDashboard() throws ThingsboardException { + @GetMapping(value = "/dashboard/home") + public void getHomeDashboard(@RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader, + HttpServletResponse response) throws Exception { SecurityUser securityUser = getCurrentUser(); + response.setContentType(APPLICATION_JSON_VALUE); if (securityUser.isSystemAdmin()) { - return null; + return; } User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId()); JsonNode additionalInfo = user.getAdditionalInfo(); @@ -431,7 +443,9 @@ public class DashboardController extends BaseController { homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo); } } - return homeDashboard; + if (homeDashboard != null) { + compressResponseWithGzipIFAccepted(acceptEncodingHeader, response, JacksonUtil.writeValueAsBytes(homeDashboard)); + } } @ApiOperation(value = "Get Home Dashboard Info (getHomeDashboardInfo)", diff --git a/application/src/main/java/org/thingsboard/server/controller/ImageController.java b/application/src/main/java/org/thingsboard/server/controller/ImageController.java index cd1116a420..5a7bd19d8e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java @@ -61,7 +61,9 @@ import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.io.ByteArrayOutputStream; import java.util.concurrent.TimeUnit; +import java.util.zip.GZIPOutputStream; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; @@ -70,6 +72,7 @@ import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_INC import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; +import static org.thingsboard.server.dao.util.ImageUtils.mediaTypeToFileExtension; @Slf4j @RestController @@ -179,15 +182,17 @@ public class ImageController extends BaseController { @PathVariable String type, @Parameter(description = IMAGE_KEY_PARAM_DESCRIPTION, required = true) @PathVariable String key, - @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception { - return downloadIfChanged(type, key, etag, false); + @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag, + @RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader) throws Exception { + return downloadIfChanged(type, key, etag, acceptEncodingHeader, false); } @GetMapping(value = "/api/images/public/{publicResourceKey}", produces = "image/*") public ResponseEntity downloadPublicImage(@PathVariable String publicResourceKey, - @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception { + @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag, + @RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader) throws Exception { ImageCacheKey cacheKey = ImageCacheKey.forPublicImage(publicResourceKey); - return downloadIfChanged(cacheKey, etag, () -> imageService.getPublicImageInfoByKey(publicResourceKey)); + return downloadIfChanged(cacheKey, etag, acceptEncodingHeader, () -> imageService.getPublicImageInfoByKey(publicResourceKey)); } @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @@ -213,8 +218,9 @@ public class ImageController extends BaseController { @PathVariable String type, @Parameter(description = IMAGE_KEY_PARAM_DESCRIPTION, required = true) @PathVariable String key, - @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception { - return downloadIfChanged(type, key, etag, true); + @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag, + @RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader) throws Exception { + return downloadIfChanged(type, key, etag, acceptEncodingHeader, true); } @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @@ -268,12 +274,12 @@ public class ImageController extends BaseController { return (result.isSuccess() ? ResponseEntity.ok() : ResponseEntity.badRequest()).body(result); } - private ResponseEntity downloadIfChanged(String type, String key, String etag, boolean preview) throws Exception { + private ResponseEntity downloadIfChanged(String type, String key, String etag, String acceptEncodingHeader, boolean preview) throws Exception { ImageCacheKey cacheKey = ImageCacheKey.forImage(getTenantId(type), key, preview); - return downloadIfChanged(cacheKey, etag, () -> checkImageInfo(type, key, Operation.READ)); + return downloadIfChanged(cacheKey, etag, acceptEncodingHeader, () -> checkImageInfo(type, key, Operation.READ)); } - private ResponseEntity downloadIfChanged(ImageCacheKey cacheKey, String etag, ThrowingSupplier imageInfoSupplier) throws Exception { + private ResponseEntity downloadIfChanged(ImageCacheKey cacheKey, String etag, String acceptEncodingHeader, ThrowingSupplier imageInfoSupplier) throws Exception { if (StringUtils.isNotEmpty(etag)) { etag = StringUtils.remove(etag, '\"'); // etag is wrapped in double quotes due to HTTP specification if (etag.equals(tbImageService.getETag(cacheKey))) { @@ -294,7 +300,6 @@ public class ImageController extends BaseController { tbImageService.putETag(cacheKey, descriptor.getEtag()); var result = ResponseEntity.ok() .header("Content-Type", descriptor.getMediaType()) - .contentLength(data.length) .eTag(descriptor.getEtag()); if (!cacheKey.isPublic()) { result @@ -308,7 +313,19 @@ public class ImageController extends BaseController { } else { result.cacheControl(CacheControl.noCache()); } - return result.body(new ByteArrayResource(data)); + var responseData = data; + if (mediaTypeToFileExtension(descriptor.getMediaType()).equals("svg") && + StringUtils.isNotEmpty(acceptEncodingHeader) && acceptEncodingHeader.contains("gzip")) { + result.header(HttpHeaders.CONTENT_ENCODING, "gzip"); + var outputStream = new ByteArrayOutputStream(); + try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream)) { + gzipOutputStream.write(data); + gzipOutputStream.finish(); + } + responseData = outputStream.toByteArray(); + } + result.contentLength(responseData.length); + return result.body(new ByteArrayResource(responseData)); } private TbResourceInfo checkImageInfo(String imageType, String key, Operation operation) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java index e055c6d6ae..72c339c635 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java @@ -477,7 +477,7 @@ public class NotificationController extends BaseController { SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @GetMapping("/notification/deliveryMethods") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - public Set getAvailableDeliveryMethods(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { + public List getAvailableDeliveryMethods(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { return notificationCenter.getAvailableDeliveryMethods(user.getTenantId()); } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java index 933f6fad3e..cfa5eeac7e 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java @@ -117,9 +117,10 @@ public class HousekeeperService { throw new IllegalArgumentException("Unsupported task type " + taskType); } + Future future = null; try { long startTs = System.currentTimeMillis(); - Future future = taskExecutor.submit(() -> { + future = taskExecutor.submit(() -> { taskProcessor.process((T) task); return null; }); @@ -137,7 +138,7 @@ public class HousekeeperService { if (e instanceof ExecutionException) { error = e.getCause(); } else if (e instanceof TimeoutException) { - error = new TimeoutException("Timeout after " + config.getTaskProcessingTimeout() + " seconds"); + error = new TimeoutException("Timeout after " + config.getTaskProcessingTimeout() + " ms"); } if (msg.getTask().getAttempt() < config.getMaxReprocessingAttempts()) { @@ -153,6 +154,10 @@ public class HousekeeperService { .build()); } statsService.ifPresent(statsService -> statsService.reportFailure(taskType, msg)); + } finally { + if (future != null && !future.isDone()) { + future.cancel(true); + } } } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java index f193b154db..5ef8b2f7f4 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java @@ -20,6 +20,8 @@ import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; import org.thingsboard.server.common.msg.housekeeper.HousekeeperClient; +import java.util.concurrent.Future; + public abstract class HousekeeperTaskProcessor { @Autowired @@ -29,4 +31,13 @@ public abstract class HousekeeperTaskProcessor { public abstract HousekeeperTaskType getTaskType(); + public V wait(Future future) throws Exception { + try { + return future.get(); // will be interrupted after taskProcessingTimeout + } catch (InterruptedException e) { + future.cancel(true); // interrupting the underlying task + throw e; + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/LatestTsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/LatestTsDeletionTaskProcessor.java index 1f4b6b57ae..64d0043c5a 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/LatestTsDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/LatestTsDeletionTaskProcessor.java @@ -33,7 +33,7 @@ public class LatestTsDeletionTaskProcessor extends HousekeeperTaskProcessor { Dashboard dashboard = dashboardService.findDashboardById(TenantId.SYS_TENANT_ID, dashboardId); - boolean updated = resourceService.updateResourcesUsage(dashboard); // will convert resources ids to new structure + boolean updated = resourceService.updateResourcesUsage(dashboard.getTenantId(), dashboard); // will convert resources ids to new structure if (updated) { dashboardService.saveDashboard(dashboard); updatedCount.incrementAndGet(); @@ -130,7 +130,7 @@ public class ResourcesUpdater { for (WidgetTypeId widgetTypeId : widgets) { executor.submit(() -> { WidgetTypeDetails widgetTypeDetails = widgetTypeService.findWidgetTypeDetailsById(TenantId.SYS_TENANT_ID, widgetTypeId); - boolean updated = resourceService.updateResourcesUsage(widgetTypeDetails); + boolean updated = resourceService.updateResourcesUsage(widgetTypeDetails.getTenantId(), widgetTypeDetails); if (updated) { widgetTypeService.saveWidgetType(widgetTypeDetails); updatedCount.incrementAndGet(); diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java index 311fe6cb2c..af63256f0f 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java @@ -417,7 +417,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple } @Override - public Set getAvailableDeliveryMethods(TenantId tenantId) { + public List getAvailableDeliveryMethods(TenantId tenantId) { return channels.values().stream() .filter(channel -> { try { @@ -428,7 +428,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple } }) .map(NotificationChannel::getDeliveryMethod) - .collect(Collectors.toSet()); + .sorted().toList(); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java index 90ad3e38dd..e3a79a8ba5 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java @@ -46,7 +46,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.List; -import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -132,12 +132,12 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements @Override public List exportResources(Dashboard dashboard, SecurityUser user) throws ThingsboardException { - return exportResources(dashboard, imageService::getUsedImages, resourceService::getUsedResources, user); + return exportResources(() -> imageService.getUsedImages(dashboard), () -> resourceService.getUsedResources(user.getTenantId(), dashboard), user); } @Override public List exportResources(WidgetTypeDetails widgetTypeDetails, SecurityUser user) throws ThingsboardException { - return exportResources(widgetTypeDetails, imageService::getUsedImages, resourceService::getUsedResources, user); + return exportResources(() -> imageService.getUsedImages(widgetTypeDetails), () -> resourceService.getUsedResources(user.getTenantId(), widgetTypeDetails), user); } @Override @@ -153,13 +153,12 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements } } - private List exportResources(T entity, - Function> imagesProcessor, - Function> resourcesProcessor, + private List exportResources(Supplier> imagesProcessor, + Supplier> resourcesProcessor, SecurityUser user) throws ThingsboardException { List resources = new ArrayList<>(); - resources.addAll(imagesProcessor.apply(entity)); - resources.addAll(resourcesProcessor.apply(entity)); + resources.addAll(imagesProcessor.get()); + resources.addAll(resourcesProcessor.get()); for (TbResourceInfo resourceInfo : resources) { accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.READ, resourceInfo.getId(), resourceInfo); } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index f642d7fea5..ef6d2204d8 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -206,7 +206,7 @@ ui: # Help parameters help: # Base URL for UI help assets - base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.9}" + base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-4.0}" # Database telemetry parameters database: diff --git a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java index 88dd8f241c..ffbf95d990 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.controller; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -602,13 +603,14 @@ public class DashboardControllerTest extends AbstractControllerTest { dashboard.setTitle("My dashboard"); dashboard.setConfiguration(JacksonUtil.newObjectNode() .put("someImage", "tb-image;/api/images/tenant/" + imageInfo.getResourceKey()) - .set("widgets", JacksonUtil.toJsonNode(""" + .set("widgets", JacksonUtil.toJsonNode(""" {"xxx": {"config":{"actions":{"elementClick":[ {"customResources":[{"url":{"entityType":"TB_RESOURCE","id": "tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js"},"isModule":true}, {"url":"tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js","isModule":true}]}]}}}} - """))); + """)) + .put("someResource", "tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js")); dashboard = doPost("/api/dashboard", dashboard, Dashboard.class); Dashboard exportedDashboard = doGet("/api/dashboard/" + dashboard.getUuidId() + "?includeResources=true", Dashboard.class); @@ -637,12 +639,18 @@ public class DashboardControllerTest extends AbstractControllerTest { doPost("/api/resource", resource, TbResourceInfo.class); Dashboard importedDashboard = doPost("/api/dashboard", exportedDashboard, Dashboard.class); + String newResourceKey = "gateway-management-extension_(1).js"; + imageRef = importedDashboard.getConfiguration().get("someImage").asText(); assertThat(imageRef).isEqualTo("tb-image;/api/images/tenant/" + imageInfo.getResourceKey()); - resourceRef = importedDashboard.getConfiguration().get("widgets").get("xxx").get("config") - .get("actions").get("elementClick").get(0).get("customResources").get(0).get("url").asText(); - String newResourceKey = "gateway-management-extension_(1).js"; - assertThat(resourceRef).isEqualTo("tb-resource;/api/resource/js_module/tenant/" + newResourceKey); + + List resourcesRefs = new ArrayList<>(); + resourcesRefs.add(importedDashboard.getConfiguration().get("widgets").get("xxx").get("config") + .get("actions").get("elementClick").get(0).get("customResources").get(0).get("url").asText()); + resourcesRefs.add(importedDashboard.getConfiguration().get("someResource").asText()); + assertThat(resourcesRefs).allSatisfy(ref -> { + assertThat(ref).isEqualTo("tb-resource;/api/resource/js_module/tenant/" + newResourceKey); + }); TbResourceInfo importedImageInfo = doGet("/api/images/tenant/" + imageInfo.getResourceKey() + "/info", TbResourceInfo.class); assertThat(importedImageInfo.getEtag()).isEqualTo(imageInfo.getEtag()); diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java index e4a972cacc..a3e2996bd2 100644 --- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -93,8 +93,12 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -103,6 +107,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; @@ -113,7 +118,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @TestPropertySource(properties = { "queue.core.housekeeper.task-reprocessing-delay-ms=2000", "queue.core.housekeeper.poll-interval-ms=1000", - "queue.core.housekeeper.max-reprocessing-attempts=5" + "queue.core.housekeeper.max-reprocessing-attempts=5", + "queue.core.housekeeper.task-processing-timeout-ms=5000", }) public class HousekeeperServiceTest extends AbstractControllerTest { @@ -320,7 +326,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { @Test public void whenTaskProcessingFails_thenReprocess() throws Exception { - TimeoutException error = new TimeoutException("Test timeout"); + Exception error = new RuntimeException("Just a test"); doThrow(error).when(tsHistoryDeletionTaskProcessor).process(any()); Device device = createDevice("test", "test"); @@ -344,6 +350,54 @@ public class HousekeeperServiceTest extends AbstractControllerTest { }); } + @Test + public void whenTaskProcessingTimedOut_thenInterruptAndReprocess() throws Exception { + ExecutorService someExecutor = Executors.newSingleThreadExecutor(); + AtomicBoolean taskInterrupted = new AtomicBoolean(false); + AtomicBoolean underlyingTaskInterrupted = new AtomicBoolean(false); + doAnswer(invocationOnMock -> { + Future> future = someExecutor.submit(() -> { + try { + Thread.sleep(TimeUnit.HOURS.toMillis(24)); + } catch (InterruptedException e) { + underlyingTaskInterrupted.set(true); + } + }); + try { + future.get(); + } catch (InterruptedException e) { + taskInterrupted.set(true); + future.cancel(true); + throw e; + } + return null; + }).when(tsHistoryDeletionTaskProcessor).process(any()); + + Device device = createDevice("test", "test"); + createRelatedData(device.getId()); + + doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); + + int attempts = 2; + await().atMost(30, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> { + for (int i = 0; i <= attempts; i++) { + int attempt = i; + verify(housekeeperReprocessingService).submitForReprocessing(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, + task -> task.getAttempt() == attempt)), argThat(error -> error instanceof TimeoutException)); + } + }); + assertThat(taskInterrupted).isTrue(); + assertThat(underlyingTaskInterrupted).isTrue(); + + assertThat(getTimeseriesHistory(device.getId())).isNotEmpty(); + doCallRealMethod().when(tsHistoryDeletionTaskProcessor).process(any()); + someExecutor.shutdown(); + + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(getTimeseriesHistory(device.getId())).isEmpty(); + }); + } + @Test public void whenReprocessingAttemptsExceeded_thenDropTheTask() throws Exception { TimeoutException error = new TimeoutException("Test timeout"); diff --git a/common/actor/pom.xml b/common/actor/pom.xml index cc19cc949f..9b0053e0b6 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT common org.thingsboard.common diff --git a/common/cache/pom.xml b/common/cache/pom.xml index 1ecb95c6c5..fd9802a689 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT common org.thingsboard.common diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index bd7dc214cf..d53ac4904d 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT common org.thingsboard.common diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml index 401d1e94fd..30001c929d 100644 --- a/common/coap-server/pom.xml +++ b/common/coap-server/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT common org.thingsboard.common diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index 62c1e420c2..0bd0f476ec 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT common org.thingsboard.common diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java index 0e2acb2e4e..f7f4b1ce4b 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java @@ -80,13 +80,13 @@ public interface ResourceService extends EntityDaoService { TbResourceInfo findSystemOrTenantResourceByEtag(TenantId tenantId, ResourceType resourceType, String etag); - boolean updateResourcesUsage(Dashboard dashboard); + boolean updateResourcesUsage(TenantId tenantId, Dashboard dashboard); - boolean updateResourcesUsage(WidgetTypeDetails widgetTypeDetails); + boolean updateResourcesUsage(TenantId tenantId, WidgetTypeDetails widgetTypeDetails); - Collection getUsedResources(Dashboard dashboard); + Collection getUsedResources(TenantId tenantId, Dashboard dashboard); - Collection getUsedResources(WidgetTypeDetails widgetTypeDetails); + Collection getUsedResources(TenantId tenantId, WidgetTypeDetails widgetTypeDetails); TbResource createOrUpdateSystemResource(ResourceType resourceType, String resourceKey, byte[] data); diff --git a/common/data/pom.xml b/common/data/pom.xml index b206f0b173..f7c559c728 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT common org.thingsboard.common diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java index d025aab3d5..fed271cbbe 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java @@ -50,7 +50,8 @@ public class WidgetType extends BaseWidgetType { @JsonIgnore public JsonNode getDefaultConfig() { - return Optional.ofNullable(descriptor.get("defaultConfig")) + return Optional.ofNullable(descriptor) + .map(descriptor -> descriptor.get("defaultConfig")) .filter(JsonNode::isTextual).map(JsonNode::asText) .map(json -> { try { diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml index 31d90de948..4f0fe5aac0 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT common org.thingsboard.common diff --git a/common/message/pom.xml b/common/message/pom.xml index ac56095e7e..0e290d1e01 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT common org.thingsboard.common diff --git a/common/pom.xml b/common/pom.xml index c905e0eee9..4c8f56cbce 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT thingsboard common diff --git a/common/proto/pom.xml b/common/proto/pom.xml index 4409d6b97f..9a9b262d82 100644 --- a/common/proto/pom.xml +++ b/common/proto/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT common org.thingsboard.common diff --git a/common/queue/pom.xml b/common/queue/pom.xml index 7485294f6e..6fad5efd5a 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT common org.thingsboard.common diff --git a/common/script/pom.xml b/common/script/pom.xml index 88e35e8429..8c4ea360cf 100644 --- a/common/script/pom.xml +++ b/common/script/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT common org.thingsboard.common diff --git a/common/script/remote-js-client/pom.xml b/common/script/remote-js-client/pom.xml index 7309e6af51..2b780bf489 100644 --- a/common/script/remote-js-client/pom.xml +++ b/common/script/remote-js-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT script org.thingsboard.common.script diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml index 08ef60713e..ed93701513 100644 --- a/common/script/script-api/pom.xml +++ b/common/script/script-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT script org.thingsboard.common.script diff --git a/common/stats/pom.xml b/common/stats/pom.xml index 31fbc72dcc..7cc67d5821 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index d4528944e0..86459e9821 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index 054fc3cbbe..8ab2243ceb 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index 54232ac104..a6e7209f21 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index e672f7a5dd..d074afc6b5 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/pom.xml b/common/transport/pom.xml index c87fa8c2bf..fdd3ad21d5 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index 78665bfde2..5d4d37547f 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 7d221c48bf..e2f03b060a 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/util/pom.xml b/common/util/pom.xml index ad3bbaba58..dc75047267 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT common org.thingsboard.common diff --git a/common/version-control/pom.xml b/common/version-control/pom.xml index f37139b3c5..ea568dfcd9 100644 --- a/common/version-control/pom.xml +++ b/common/version-control/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT common org.thingsboard.common diff --git a/dao/pom.xml b/dao/pom.xml index 1dac54c142..5021742154 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT thingsboard dao diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index b547de1eca..2e9e37577c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -162,18 +162,19 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb dashboardValidator.validate(dashboard, DashboardInfo::getTenantId); } try { + TenantId tenantId = dashboard.getTenantId(); if (CollectionUtils.isNotEmpty(dashboard.getResources())) { - resourceService.importResources(dashboard.getTenantId(), dashboard.getResources()); + resourceService.importResources(tenantId, dashboard.getResources()); } imageService.updateImagesUsage(dashboard); - resourceService.updateResourcesUsage(dashboard); + resourceService.updateResourcesUsage(tenantId, dashboard); - var saved = dashboardDao.save(dashboard.getTenantId(), dashboard); + var saved = dashboardDao.save(tenantId, dashboard); publishEvictEvent(new DashboardTitleEvictEvent(saved.getId())); - eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(saved.getTenantId()) + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId) .entityId(saved.getId()).created(dashboard.getId() == null).build()); if (dashboard.getId() == null) { - countService.publishCountEntityEvictEvent(saved.getTenantId(), EntityType.DASHBOARD); + countService.publishCountEntityEvictEvent(tenantId, EntityType.DASHBOARD); } return saved; } catch (Exception e) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java index 683c323ab5..452bd7ccf7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java @@ -64,7 +64,7 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA @Override public List findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType(String pkgName, PlatformType platformType) { - log.trace("Executing findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType pkgName=[{}] platformType=[{}]",pkgName, platformType); + log.trace("Executing findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType pkgName=[{}] platformType=[{}]", pkgName, platformType); return oauth2ClientDao.findEnabledByPkgNameAndPlatformType(pkgName, platformType) .stream() .map(OAuth2Utils::toClientLoginInfo) 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 22aa2ae748..9395aae6c7 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 @@ -57,6 +57,7 @@ import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.ResourceDataValidator; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Base64; import java.util.Collection; import java.util.Collections; @@ -401,18 +402,33 @@ public class BaseResourceService extends AbstractCachedEntityService links = getResourcesLinks(dashboard.getResources()); - return updateResourcesUsage(dashboard.getTenantId(), dashboard.getConfiguration(), DASHBOARD_RESOURCES_MAPPING, links); + return updateResourcesUsage(tenantId, List.of(dashboard.getConfiguration()), List.of(DASHBOARD_RESOURCES_MAPPING), links); } @Override - public boolean updateResourcesUsage(WidgetTypeDetails widgetTypeDetails) { + public boolean updateResourcesUsage(TenantId tenantId, WidgetTypeDetails widgetTypeDetails) { Map links = getResourcesLinks(widgetTypeDetails.getResources()); - boolean updated = updateResourcesUsage(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), WIDGET_RESOURCES_MAPPING, links); + List jsonNodes = new ArrayList<>(2); + List> mappings = new ArrayList<>(2); + + if (widgetTypeDetails.getDescriptor() != null) { + jsonNodes.add(widgetTypeDetails.getDescriptor()); + mappings.add(WIDGET_RESOURCES_MAPPING); + } + JsonNode defaultConfig = widgetTypeDetails.getDefaultConfig(); if (defaultConfig != null) { - updated |= updateResourcesUsage(widgetTypeDetails.getTenantId(), defaultConfig, WIDGET_DEFAULT_CONFIG_RESOURCES_MAPPING, links); + jsonNodes.add(defaultConfig); + mappings.add(WIDGET_DEFAULT_CONFIG_RESOURCES_MAPPING); + } + + boolean updated = updateResourcesUsage(tenantId, jsonNodes, mappings, links); + if (defaultConfig != null) { widgetTypeDetails.setDefaultConfig(defaultConfig); } return updated; @@ -433,8 +449,9 @@ public class BaseResourceService extends AbstractCachedEntityService mapping, Map links) { - return processResources(jsonNode, mapping, value -> { + private boolean updateResourcesUsage(TenantId tenantId, List jsonNodes, List> mappings, Map links) { + log.trace("[{}] updateResourcesUsage (new links: {}) for {}", tenantId, links, jsonNodes); + return processResources(jsonNodes, mappings, value -> { String link = getResourceLink(value); if (link != null) { String newLink = links.get(link); @@ -462,23 +479,31 @@ public class BaseResourceService extends AbstractCachedEntityService getUsedResources(Dashboard dashboard) { - return getUsedResources(dashboard.getTenantId(), dashboard.getConfiguration(), DASHBOARD_RESOURCES_MAPPING).values(); + public Collection getUsedResources(TenantId tenantId, Dashboard dashboard) { + return getUsedResources(tenantId, List.of(dashboard.getConfiguration()), List.of(DASHBOARD_RESOURCES_MAPPING)).values(); } @Override - public Collection getUsedResources(WidgetTypeDetails widgetTypeDetails) { - Map resources = getUsedResources(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), WIDGET_RESOURCES_MAPPING); + public Collection getUsedResources(TenantId tenantId, WidgetTypeDetails widgetTypeDetails) { + List jsonNodes = new ArrayList<>(2); + List> mappings = new ArrayList<>(2); + + jsonNodes.add(widgetTypeDetails.getDescriptor()); + mappings.add(WIDGET_RESOURCES_MAPPING); + JsonNode defaultConfig = widgetTypeDetails.getDefaultConfig(); if (defaultConfig != null) { - resources.putAll(getUsedResources(widgetTypeDetails.getTenantId(), defaultConfig, WIDGET_DEFAULT_CONFIG_RESOURCES_MAPPING)); + jsonNodes.add(defaultConfig); + mappings.add(WIDGET_DEFAULT_CONFIG_RESOURCES_MAPPING); } - return resources.values(); + + return getUsedResources(tenantId, jsonNodes, mappings).values(); } - private Map getUsedResources(TenantId tenantId, JsonNode jsonNode, Map mapping) { + private Map getUsedResources(TenantId tenantId, List jsonNodes, List> mappings) { Map resources = new HashMap<>(); - processResources(jsonNode, mapping, value -> { + log.trace("[{}] getUsedResources for {}", tenantId, jsonNodes); + processResources(jsonNodes, mappings, value -> { String link = getResourceLink(value); if (link == null) { return value; @@ -517,32 +542,57 @@ public class BaseResourceService extends AbstractCachedEntityService mapping, UnaryOperator processor) { + private boolean processResources(List jsonNodes, List> mappings, UnaryOperator processor) { AtomicBoolean updated = new AtomicBoolean(false); - JacksonUtil.replaceByMapping(jsonNode, mapping, Collections.emptyMap(), (name, urlNode) -> { - String value = null; - if (urlNode.isTextual()) { // link is in the right place - value = urlNode.asText(); - } else { - JsonNode id = urlNode.get("id"); // old structure is used - if (id != null && id.isTextual()) { - value = id.asText(); - } - } - if (StringUtils.isNotBlank(value)) { - value = processor.apply(value); - } else { - value = ""; - } + for (int i = 0; i < jsonNodes.size(); i++) { + JsonNode jsonNode = jsonNodes.get(i); + // processing by mappings first + if (i <= mappings.size() - 1) { + JacksonUtil.replaceByMapping(jsonNode, mappings.get(i), Collections.emptyMap(), (name, urlNode) -> { + String value = null; + if (urlNode.isTextual()) { // link is in the right place + value = urlNode.asText(); + } else { + JsonNode id = urlNode.get("id"); // old structure is used + if (id != null && id.isTextual()) { + value = id.asText(); + } + } - JsonNode newValue = new TextNode(value); - if (!newValue.toString().equals(urlNode.toString())) { - updated.set(true); - log.trace("Replaced '{}' with '{}'", urlNode, newValue); + if (StringUtils.isNotBlank(value)) { + value = processor.apply(value); + } else { + value = ""; + } + + JsonNode newValue = new TextNode(value); + if (!newValue.toString().equals(urlNode.toString())) { + updated.set(true); + log.trace("Replaced by mapping '{}' ({}) with '{}'", value, name, newValue); + } + return newValue; + }); } - return newValue; - }); + + + // processing all + JacksonUtil.replaceAll(jsonNode, "", (name, value) -> { + if (!StringUtils.startsWith(value, DataConstants.TB_RESOURCE_PREFIX + "/api/resource/")) { + return value; + } + + String newValue = processor.apply(value); + if (StringUtils.equals(value, newValue)) { + return value; + } else { + updated.set(true); + log.trace("Replaced '{}' ({}) with '{}'", value, name, newValue); + return newValue; + } + }); + } + return updated.get(); } @@ -555,7 +605,7 @@ public class BaseResourceService extends AbstractCachedEntityService findEnabledByPkgNameAndPlatformType(String pkgName, PlatformType platformType) { - return DaoUtil.convertDataList(repository.findEnabledByPkgNameAndPlatformType(pkgName, - platformType != null ? platformType.name() : null)); + List clientEntities; + if (platformType != null) { + clientEntities = switch (platformType) { + case ANDROID -> repository.findEnabledByAndroidPkgNameAndPlatformType(pkgName, platformType.name()); + case IOS -> repository.findEnabledByIosPkgNameAndPlatformType(pkgName, platformType.name()); + default -> Collections.emptyList(); + }; + } else { + clientEntities = Collections.emptyList(); + } + return DaoUtil.convertDataList(clientEntities); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java index f7fe5bce3a..2a5526c146 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java @@ -49,11 +49,21 @@ public interface OAuth2ClientRepository extends JpaRepository findEnabledByPkgNameAndPlatformType(@Param("pkgName") String pkgName, - @Param("platformFilter") String platformFilter); + List findEnabledByAndroidPkgNameAndPlatformType(@Param("pkgName") String pkgName, + @Param("platformFilter") String platformFilter); + + @Query("SELECT c " + + "FROM OAuth2ClientEntity c " + + "LEFT JOIN MobileAppBundleOauth2ClientEntity ac ON c.id = ac.oauth2ClientId " + + "LEFT JOIN MobileAppBundleEntity b ON ac.mobileAppBundleId = b.id " + + "LEFT JOIN MobileAppEntity iosApp ON b.iosAppID = iosApp.id " + + "WHERE iosApp.pkgName = :pkgName AND b.oauth2Enabled = true " + + "AND (:platformFilter IS NULL OR c.platforms IS NULL OR c.platforms = '' OR ilike(c.platforms, CONCAT('%', :platformFilter, '%')) = true)") + List findEnabledByIosPkgNameAndPlatformType(@Param("pkgName") String pkgName, + @Param("platformFilter") String platformFilter); @Query("SELECT c " + "FROM OAuth2ClientEntity c " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java index fd9b1260ba..a1e241073f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java @@ -99,14 +99,15 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { log.trace("Executing saveWidgetType [{}]", widgetTypeDetails); widgetTypeValidator.validate(widgetTypeDetails, WidgetType::getTenantId); try { + TenantId tenantId = widgetTypeDetails.getTenantId(); if (CollectionUtils.isNotEmpty(widgetTypeDetails.getResources())) { - resourceService.importResources(widgetTypeDetails.getTenantId(), widgetTypeDetails.getResources()); + resourceService.importResources(tenantId, widgetTypeDetails.getResources()); } imageService.updateImagesUsage(widgetTypeDetails); - resourceService.updateResourcesUsage(widgetTypeDetails); + resourceService.updateResourcesUsage(tenantId, widgetTypeDetails); - WidgetTypeDetails result = widgetTypeDao.save(widgetTypeDetails.getTenantId(), widgetTypeDetails); - eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(result.getTenantId()) + WidgetTypeDetails result = widgetTypeDao.save(tenantId, widgetTypeDetails); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId) .entityId(result.getId()).created(widgetTypeDetails.getId() == null).build()); return result; } catch (Exception t) { diff --git a/monitoring/pom.xml b/monitoring/pom.xml index 013a0b5c47..d30a9ca71f 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -21,7 +21,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT thingsboard diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index 4d34a13f38..c1c0980b64 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/js-executor/api/jsInvokeMessageProcessor.ts b/msa/js-executor/api/jsInvokeMessageProcessor.ts index 98b9c0fe0c..fe18c320bc 100644 --- a/msa/js-executor/api/jsInvokeMessageProcessor.ts +++ b/msa/js-executor/api/jsInvokeMessageProcessor.ts @@ -130,7 +130,7 @@ export class JsInvokeMessageProcessor { processCompileRequest(requestId: string, responseTopic: string, headers: any, compileRequest: JsCompileRequest) { const scriptId = JsInvokeMessageProcessor.getScriptId(compileRequest); - this.logger.debug('[%s] Processing compile request, scriptId: [%s]', requestId, scriptId); + this.logger.debug('[%s] Processing compile request, scriptId: [%s], compileRequest [%s]', requestId, scriptId, compileRequest); if (this.scriptMap.has(scriptId)) { const compileResponse = JsInvokeMessageProcessor.createCompileResponse(scriptId, true); this.logger.debug('[%s] Script was already compiled, scriptId: [%s]', requestId, scriptId); @@ -154,7 +154,7 @@ export class JsInvokeMessageProcessor { processInvokeRequest(requestId: string, responseTopic: string, headers: any, invokeRequest: JsInvokeRequest) { const scriptId = JsInvokeMessageProcessor.getScriptId(invokeRequest); - this.logger.debug('[%s] Processing invoke request, scriptId: [%s]', requestId, scriptId); + this.logger.debug('[%s] Processing invoke request, scriptId: [%s], invokeRequest [%s]', requestId, scriptId, invokeRequest); this.executedScriptsCounter++; if (this.executedScriptsCounter % statFrequency == 0) { const nowMs = performance.now(); @@ -217,7 +217,7 @@ export class JsInvokeMessageProcessor { processReleaseRequest(requestId: string, responseTopic: string, headers: any, releaseRequest: JsReleaseRequest) { const scriptId = JsInvokeMessageProcessor.getScriptId(releaseRequest); - this.logger.debug('[%s] Processing release request, scriptId: [%s]', requestId, scriptId); + this.logger.debug('[%s] Processing release request, scriptId: [%s], releaseRequest [%s]', requestId, scriptId, releaseRequest); if (this.scriptMap.has(scriptId)) { const index = this.scriptIds.indexOf(scriptId); if (index > -1) { diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index 0fb241c558..a29dc1ef54 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-js-executor", "private": true, - "version": "3.9.0", + "version": "4.0.0", "description": "ThingsBoard JavaScript Executor Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index f747db1438..62545f5ad6 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml index 3a4834236c..1b03765586 100644 --- a/msa/monitoring/pom.xml +++ b/msa/monitoring/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT msa diff --git a/msa/pom.xml b/msa/pom.xml index d77384cc74..ff394295f4 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT thingsboard msa diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 11295dc4b9..6142c3c5d8 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/tb/docker/install-tb.sh b/msa/tb/docker/install-tb.sh index 9567908316..b926aac0bb 100644 --- a/msa/tb/docker/install-tb.sh +++ b/msa/tb/docker/install-tb.sh @@ -40,7 +40,6 @@ fi CONF_FOLDER="${pkg.installFolder}/conf" jarfile=${pkg.installFolder}/bin/${pkg.name}.jar configfile=${pkg.name}.conf -upgradeversion=${DATA_FOLDER}/.upgradeversion source "${CONF_FOLDER}/${configfile}" @@ -54,5 +53,3 @@ java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardI -Dinstall.upgrade=false \ -Dlogging.config=/usr/share/thingsboard/bin/install/logback.xml \ org.springframework.boot.loader.launch.PropertiesLauncher - -echo "${pkg.upgradeVersion}" > ${upgradeversion} diff --git a/msa/tb/docker/upgrade-tb.sh b/msa/tb/docker/upgrade-tb.sh index 545bf4b8d6..3c8e4f194d 100644 --- a/msa/tb/docker/upgrade-tb.sh +++ b/msa/tb/docker/upgrade-tb.sh @@ -20,28 +20,15 @@ start-db.sh CONF_FOLDER="${pkg.installFolder}/conf" jarfile=${pkg.installFolder}/bin/${pkg.name}.jar configfile=${pkg.name}.conf -upgradeversion=${DATA_FOLDER}/.upgradeversion source "${CONF_FOLDER}/${configfile}" -FROM_VERSION=`cat ${upgradeversion}` - echo "Starting ThingsBoard upgrade ..." -if [[ -z "${FROM_VERSION// }" ]]; then - echo "FROM_VERSION variable is invalid or unspecified!" - exit 1 -else - fromVersion="${FROM_VERSION// }" -fi - java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardInstallApplication \ -Dspring.jpa.hibernate.ddl-auto=none \ -Dinstall.upgrade=true \ - -Dinstall.upgrade.from_version=${fromVersion} \ -Dlogging.config=/usr/share/thingsboard/bin/install/logback.xml \ org.springframework.boot.loader.launch.PropertiesLauncher -echo "${pkg.upgradeVersion}" > ${upgradeversion} - stop-db.sh \ No newline at end of file diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 79a2a66385..0c6046c09e 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT msa org.thingsboard.msa @@ -38,7 +38,6 @@ tb-postgres tb-cassandra /usr/share/${pkg.name} - 3.9.0 diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index 9ec8589d9a..325d4d45fa 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index d562f89cd5..945c523455 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml index 992187316a..4d0370c1cf 100644 --- a/msa/transport/lwm2m/pom.xml +++ b/msa/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index 32aa6a4f23..20791ad61c 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index 34be0bd338..0b92e1fa97 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index 95919ad175..b16469523e 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT org.thingsboard.msa.transport diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml index 8aeeed2b1e..72995b05ab 100644 --- a/msa/vc-executor-docker/pom.xml +++ b/msa/vc-executor-docker/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml index 0932bf7c06..1b88a41692 100644 --- a/msa/vc-executor/pom.xml +++ b/msa/vc-executor/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index 84b1a4a488..3374112096 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-web-ui", "private": true, - "version": "3.9.0", + "version": "4.0.0", "description": "ThingsBoard Web UI Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index 195ae123f4..361487b516 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index 7de766b2da..f7361a686d 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT thingsboard netty-mqtt - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT jar Netty MQTT Client diff --git a/pom.xml b/pom.xml index a1d5f26ecf..f18740b658 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT pom Thingsboard @@ -42,13 +42,13 @@ 4.0.2 2.4.0-b180830.0359 4.0.5 - 3.2.4 - 3.2.5 - 3.2.5 - 6.1.6 - 6.2.4 - 6.2.4 - 5.1.2 + 3.2.12 + 3.2.12 + 3.2.12 + 6.1.15 + 6.2.11 + 6.2.8 + 5.1.5 0.12.5 2.0.13 2.23.1 @@ -81,13 +81,13 @@ 2.0.1 5.6.0 3.9.2 - 3.25.3 + 3.25.5 1.63.0 - 1.2.4 + 1.2.5 1.18.32 1.2.5 1.2.5 - 4.1.109.Final + 4.1.115.Final 2.0.65.Final 1.1.18 1.7.1 @@ -98,7 +98,7 @@ 2.2.21 0.8 1.19.0 - 1.78 + 1.78.1 2.0.1 42.7.3 org/thingsboard/server/gen/**/*, @@ -110,7 +110,7 @@ - 3.7.0 + 3.7.1 8.10.1 3.5.3 2.2 @@ -122,7 +122,7 @@ 1.6.1 1.9.4 4.4 - 1.12.5 + 1.12.12 1.0.4TB 3.7.1 10.1.3 @@ -137,7 +137,7 @@ 4.2.1 2.7.3 1.5.6 - 5.10.2 + 5.10.5 5.15.0 1.3.0 1.2.7 @@ -147,7 +147,7 @@ 3.25.3 5.4.0 2.2 - 1.19.7 + 1.20.4 1.0.1 1.12 4.19.1 diff --git a/rest-client/pom.xml b/rest-client/pom.xml index 069f7b84f0..c8232512f7 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT thingsboard rest-client diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index e6abae70ef..bd14daf22e 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index 57bc5771d2..886a2169f3 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java index 02ee53f3ad..b8fb9b61ff 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.notification.info.NotificationInfo; import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; -import java.util.Set; +import java.util.List; public interface NotificationCenter { @@ -48,6 +48,6 @@ public interface NotificationCenter { void deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId); - Set getAvailableDeliveryMethods(TenantId tenantId); + List getAvailableDeliveryMethods(TenantId tenantId); } diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 99853b0950..750701894f 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/SemaphoreWithTbMsgQueue.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/SemaphoreWithTbMsgQueue.java index fa00856b4b..50a109163c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/SemaphoreWithTbMsgQueue.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/SemaphoreWithTbMsgQueue.java @@ -115,7 +115,7 @@ public class SemaphoreWithTbMsgQueue { } TbMsg msg = tbMsgTbContext.msg(); TbContext ctx = tbMsgTbContext.ctx(); - log.warn("[{}] Failed to process message: {}", entityId, msg, t); + log.debug("[{}] Failed to process message: {}", entityId, msg, t); ctx.tellFailure(msg, t); // you are not allowed to throw here, because queue will remain unprocessed continue; // We are probably the last who process the queue. We have to continue poll until get successful callback or queue is empty } diff --git a/tools/pom.xml b/tools/pom.xml index 2d196cec7d..4f7c3f48a6 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT thingsboard tools diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index 76655178ae..452ef6ec9a 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/http/pom.xml b/transport/http/pom.xml index 7e569ea1f1..2408340792 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml index b5b7867146..3ba43f5b53 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index 88009474aa..e31d618890 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/pom.xml b/transport/pom.xml index 8e7f77be9b..dd1c415e6c 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT thingsboard transport diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index 02fe36f657..08f23d2c2d 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT transport diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 0eff3bbd8d..d8870a3568 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -1,6 +1,6 @@ { "name": "thingsboard", - "version": "3.9.0", + "version": "4.0.0", "scripts": { "ng": "ng", "start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open", diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index f16e352d71..9515f3b3ec 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT thingsboard org.thingsboard diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index b67f41d0cf..fed2de125f 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -711,21 +711,6 @@ export function safeExecuteTbFunction(func: CompiledT return res; } - -export function safeExecute(func: (...args: any[]) => any, params = []) { - let res = null; - if (func && typeof (func) === 'function') { - try { - res = func(...params); - } - catch (err) { - console.log('error in external function:', err); - res = null; - } - } - return res; -} - export function padValue(val: any, dec: number): string { let strVal; let n; diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html index 67ef5908e5..ab127e45cd 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html @@ -41,10 +41,10 @@ {{ 'version-control.export-credentials' | translate }} - + {{ 'version-control.export-attributes' | translate }} - + {{ 'version-control.export-relations' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts index 26d45de2c0..7d3e89eda8 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts @@ -18,6 +18,7 @@ import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular import { PageComponent } from '@shared/components/page.component'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { + entityTypesWithoutRelatedData, SingleEntityVersionCreateRequest, VersionCreateRequestType, VersionCreationResult @@ -62,6 +63,8 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni entityTypes = EntityType; + entityTypesWithoutRelatedData = entityTypesWithoutRelatedData; + resultMessage: string; versionCreateResult$: Observable; @@ -108,8 +111,10 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni branch: this.createVersionFormGroup.get('branch').value, versionName: this.createVersionFormGroup.get('versionName').value, config: { - saveRelations: this.createVersionFormGroup.get('saveRelations').value, - saveAttributes: this.createVersionFormGroup.get('saveAttributes').value, + saveRelations: !entityTypesWithoutRelatedData.has(this.entityId.entityType) + ? this.createVersionFormGroup.get('saveRelations').value : false, + saveAttributes: !entityTypesWithoutRelatedData.has(this.entityId.entityType) + ? this.createVersionFormGroup.get('saveAttributes').value : false, saveCredentials: this.entityId.entityType === EntityType.DEVICE ? this.createVersionFormGroup.get('saveCredentials').value : false }, type: VersionCreateRequestType.SINGLE_ENTITY 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 a32d7c2311..800deec7e5 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 @@ -143,6 +143,9 @@ import { UnreadNotificationBasicConfigComponent } from '@home/components/widget/config/basic/cards/unread-notification-basic-config.component'; import { ScadaSymbolBasicConfigComponent } from '@home/components/widget/config/basic/scada/scada-symbol-basic-config.component'; +import { + SegmentedButtonBasicConfigComponent +} from '@home/components/widget/config/basic/button/segmented-button-basic-config.component'; @NgModule({ declarations: [ @@ -174,6 +177,7 @@ import { ScadaSymbolBasicConfigComponent } from '@home/components/widget/config/ BarChartWithLabelsBasicConfigComponent, SingleSwitchBasicConfigComponent, ActionButtonBasicConfigComponent, + SegmentedButtonBasicConfigComponent, CommandButtonBasicConfigComponent, PowerButtonBasicConfigComponent, SliderBasicConfigComponent, @@ -227,6 +231,7 @@ import { ScadaSymbolBasicConfigComponent } from '@home/components/widget/config/ BarChartWithLabelsBasicConfigComponent, SingleSwitchBasicConfigComponent, ActionButtonBasicConfigComponent, + SegmentedButtonBasicConfigComponent, CommandButtonBasicConfigComponent, PowerButtonBasicConfigComponent, SliderBasicConfigComponent, @@ -272,6 +277,7 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type + + + + + widgets.action-button.behavior + + widgets.button-state.selected-state + + + + + widgets.action-button.first-button-click + + + + + widgets.action-button.second-button-click + + + + + + widgets.button-state.disabled-state + + + + + widget-config.appearance + + + + {{ segmentedButtonLayoutTranslationMap.get(layout) | translate }} + + + + + {{ 'widgets.button.auto-scale' | translate }} + + + + widgets.segmented-button.card-border + + + + px + + + + + + + + + + widgets.segmented-button.button-appearance + + {{ 'widgets.segmented-button.first' | translate }} + {{ 'widgets.segmented-button.second' | translate }} + + + + + + {{ 'widgets.button.label' | translate }} + + + + + + + + + + + + {{ 'widgets.button.icon' | translate }} + + + + + + + + + + + + + + + + {{ 'widgets.button.label' | translate }} + + + + + + + + + + + + {{ 'widgets.button.icon' | translate }} + + + + + + + + + + + + + + + + widgets.segmented-button.color-styles + + {{ 'widgets.segmented-button.selected' | translate }} + {{ 'widgets.segmented-button.unselected' | translate }} + + + + + {{ 'widgets.button.color-palette' | translate }} + + + widgets.button.main + + + + + + widgets.button.background + + + + + + + + + + widgets.button.custom-styles + + + + + {{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }} + + + + + + + + + + {{ 'widgets.button.color-palette' | translate }} + + + widgets.button.main + + + + + + widgets.button.background + + + + + + + + + + widgets.button.custom-styles + + + + + {{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }} + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts new file mode 100644 index 0000000000..cf9c830fe8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts @@ -0,0 +1,145 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } 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 { Datasource, TargetDevice, } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { ValueType } from '@shared/models/constants'; +import { getTargetDeviceFromDatasources } from '@shared/models/widget-settings.models'; +import { + SegmentedButtonAppearanceType, + SegmentedButtonColorStylesType, + segmentedButtonDefaultSettings, + segmentedButtonLayoutBorder, + segmentedButtonLayoutImages, + segmentedButtonLayouts, + segmentedButtonLayoutTranslations, + SegmentedButtonWidgetSettings, + WidgetButtonToggleState, + widgetButtonToggleStatesTranslations +} from '@home/components/widget/lib/button/segmented-button-widget.models'; + +@Component({ + selector: 'tb-segmented-button-basic-config', + templateUrl: './segmented-button-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class SegmentedButtonBasicConfigComponent extends BasicWidgetConfigComponent { + + get targetDevice(): TargetDevice { + const datasources: Datasource[] = this.segmentedButtonWidgetConfigForm.get('datasources').value; + return getTargetDeviceFromDatasources(datasources); + } + + segmentedButtonAppearanceType: SegmentedButtonAppearanceType = 'first'; + segmentedButtonColorStylesType: SegmentedButtonColorStylesType = 'selected'; + + widgetButtonToggleStates = Object.keys(WidgetButtonToggleState) as WidgetButtonToggleState[]; + widgetButtonToggleStatesTranslationsMap = widgetButtonToggleStatesTranslations; + + segmentedButtonLayouts = segmentedButtonLayouts; + segmentedButtonLayoutTranslationMap = segmentedButtonLayoutTranslations; + segmentedButtonLayoutImageMap = segmentedButtonLayoutImages; + segmentedButtonLayoutBorderMap = segmentedButtonLayoutBorder; + + valueType = ValueType; + + segmentedButtonWidgetConfigForm: UntypedFormGroup; + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.segmentedButtonWidgetConfigForm; + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: SegmentedButtonWidgetSettings = {...segmentedButtonDefaultSettings, ...(configData.config.settings || {})}; + + this.segmentedButtonWidgetConfigForm = this.fb.group({ + datasources: [configData.config.datasources, []], + + initialState: [settings.initialState, []], + leftButtonClick: [settings.leftButtonClick, []], + rightButtonClick: [settings.rightButtonClick, []], + disabledState: [settings.disabledState, []], + + appearance: this.fb.group({ + layout: [settings.appearance.layout, []], + autoScale: [settings.appearance.autoScale, []], + cardBorder: [settings.appearance.cardBorder, []], + cardBorderColor: [settings.appearance.cardBorderColor, []], + leftAppearance: this.fb.group({ + showLabel: [settings.appearance.leftAppearance.showLabel, []], + label: [settings.appearance.leftAppearance.label, []], + labelFont: [settings.appearance.leftAppearance.labelFont, []], + showIcon: [settings.appearance.leftAppearance.showIcon, []], + icon: [settings.appearance.leftAppearance.icon, []], + iconSize: [settings.appearance.leftAppearance.iconSize, []], + iconSizeUnit: [settings.appearance.leftAppearance.iconSizeUnit, []], + }), + rightAppearance: this.fb.group({ + showLabel: [settings.appearance.rightAppearance.showLabel, []], + label: [settings.appearance.rightAppearance.label, []], + labelFont: [settings.appearance.rightAppearance.labelFont, []], + showIcon: [settings.appearance.rightAppearance.showIcon, []], + icon: [settings.appearance.rightAppearance.icon, []], + iconSize: [settings.appearance.rightAppearance.iconSize, []], + iconSizeUnit: [settings.appearance.rightAppearance.iconSizeUnit, []], + }), + selectedStyle: this.fb.group({ + mainColor: [settings.appearance.selectedStyle.mainColor, []], + backgroundColor: [settings.appearance.selectedStyle.backgroundColor, []], + customStyle: this.fb.group({ + enabled: [settings.appearance.selectedStyle.customStyle.enabled, []], + hovered: [settings.appearance.selectedStyle.customStyle.hovered, []], + disabled: [settings.appearance.selectedStyle.customStyle.disabled, []], + }) + }), + unselectedStyle: this.fb.group({ + mainColor: [settings.appearance.unselectedStyle.mainColor, []], + backgroundColor: [settings.appearance.unselectedStyle.backgroundColor, []], + customStyle: this.fb.group({ + enabled: [settings.appearance.unselectedStyle.customStyle.enabled, []], + hovered: [settings.appearance.unselectedStyle.customStyle.hovered, []], + disabled: [settings.appearance.unselectedStyle.customStyle.disabled, []], + }) + }), + }) + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + this.widgetConfig.config.datasources = config.datasources; + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + this.widgetConfig.config.settings.initialState = config.initialState; + this.widgetConfig.config.settings.disabledState = config.disabledState; + this.widgetConfig.config.settings.leftButtonClick = config.leftButtonClick; + this.widgetConfig.config.settings.rightButtonClick = config.rightButtonClick; + this.widgetConfig.config.settings.appearance = config.appearance; + this.widgetConfig.config.borderRadius = this.segmentedButtonLayoutBorderMap.get(config.appearance.layout); + return this.widgetConfig; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts index 38d704d51b..b4a46d9592 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts @@ -22,7 +22,17 @@ import { telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models'; import { WidgetContext } from '@home/models/widget-component.models'; -import { BehaviorSubject, forkJoin, Observable, Observer, of, Subscription, throwError } from 'rxjs'; +import { + BehaviorSubject, + forkJoin, + Observable, + Observer, + of, + ReplaySubject, + Subscription, + switchMap, + throwError +} from 'rxjs'; import { catchError, delay, map, share, take } from 'rxjs/operators'; import { AfterViewInit, ChangeDetectorRef, Directive, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core'; import { @@ -45,6 +55,8 @@ import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.m import { EntityId } from '@shared/models/id/entity-id'; import { isDefinedAndNotNull } from '@core/utils'; import { parseError } from '@shared/models/error.models'; +import { CompiledTbFunction, compileTbFunction } from '@shared/models/js-function.models'; +import { HttpClient } from '@angular/common/http'; @Directive() // eslint-disable-next-line @angular-eslint/directive-class-suffix @@ -163,46 +175,57 @@ type DataToValueFunction = (data: any) => V; export class DataToValueConverter { - private readonly dataToValueFunction: DataToValueFunction; + private readonly dataToValueFunction$: Observable>>; private readonly compareToValue: any; - constructor(private settings: DataToValueSettings, + constructor(private http: HttpClient, + private settings: DataToValueSettings, private valueType: ValueType) { this.compareToValue = settings.compareToValue; switch (settings.type) { case DataToValueType.FUNCTION: - try { - this.dataToValueFunction = new Function('data', settings.dataToValueFunction) as DataToValueFunction; - } catch (e) { - this.dataToValueFunction = (data) => data; - } + this.dataToValueFunction$ = compileTbFunction(this.http, settings.dataToValueFunction, 'data').pipe( + catchError(() => { + return of(new CompiledTbFunction((data: any) => data, [])); + }), + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: false + }) + ); break; case DataToValueType.NONE: break; } } - dataToValue(data: any): V { - let result: V; + dataToValue(data: any): Observable { + let result: Observable; switch (this.settings.type) { case DataToValueType.FUNCTION: - result = data; - try { - let input = data; - if (!!data) { - try { - input = JSON.parse(data); - } catch (_e) {} - } - result = this.dataToValueFunction(input); - } catch (_e) {} + result = this.dataToValueFunction$.pipe( + map((dataToValueFunction) => { + let input = data; + if (!!data) { + try { + input = JSON.parse(data); + } catch (_e) {} + } + return dataToValueFunction.execute(input); + }), + catchError(() => of(data)) + ); break; case DataToValueType.NONE: - result = data; + result = of(data); break; } if (this.valueType === ValueType.BOOLEAN) { - result = (result === this.compareToValue) as any; + result = result.pipe( + map(val => (val === this.compareToValue) as V) + ); } return result; } @@ -260,17 +283,17 @@ export abstract class ValueGetter extends ValueAction { protected simulated: boolean) { super(ctx, settings); if (this.settings.action !== GetValueAction.DO_NOTHING && this.settings.action !== GetValueAction.GET_ALARM_STATUS) { - this.dataConverter = new DataToValueConverter(settings.dataToValue, valueType); + this.dataConverter = new DataToValueConverter(ctx.http, settings.dataToValue, valueType); } } getValue(): Observable { const valueObservable: Observable = this.doGetValue().pipe( - map((data) => { + switchMap((data) => { if (this.dataConverter) { return this.dataConverter.dataToValue(data); } else { - return data; + return of(data); } }), catchError(err => { @@ -308,9 +331,10 @@ type ValueToDataFunction = (value: V) => any; export class ValueToDataConverter { private readonly constantValue: any; - private readonly valueToDataFunction: ValueToDataFunction; + private readonly valueToDataFunction$: Observable>>; - constructor(protected settings: ValueToDataSettings) { + constructor(private http: HttpClient, + private settings: ValueToDataSettings) { switch (settings.type) { case ValueToDataType.VALUE: break; @@ -318,31 +342,38 @@ export class ValueToDataConverter { this.constantValue = this.settings.constantValue; break; case ValueToDataType.FUNCTION: - try { - this.valueToDataFunction = new Function('value', settings.valueToDataFunction) as ValueToDataFunction; - } catch (e) { - this.valueToDataFunction = (data) => data; - } + this.valueToDataFunction$ = compileTbFunction(this.http, settings.valueToDataFunction, 'value').pipe( + catchError(() => { + return of(new CompiledTbFunction((value: any) => value, [])); + }), + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: false + }) + ); break; case ValueToDataType.NONE: break; } } - valueToData(value: V): any { + valueToData(value: V): Observable { switch (this.settings.type) { case ValueToDataType.VALUE: - return value; + return of(value); case ValueToDataType.CONSTANT: - return this.constantValue; + return of(this.constantValue); case ValueToDataType.FUNCTION: - let result = value; - try { - result = this.valueToDataFunction(value); - } catch (e) {} - return result; + return this.valueToDataFunction$.pipe( + map((valueToDataFunction) => { + return valueToDataFunction.execute(value); + }), + catchError(() => of(value)) + ); case ValueToDataType.NONE: - return null; + return of(null); } } } @@ -368,14 +399,15 @@ export abstract class ValueSetter extends ValueAction { protected settings: SetValueSettings, protected simulated: boolean) { super(ctx, settings); - this.valueToDataConverter = new ValueToDataConverter(settings.valueToData); + this.valueToDataConverter = new ValueToDataConverter(ctx.http, settings.valueToData); } setValue(value: V): Observable { if (this.simulated) { return of(null).pipe(delay(500)); } else { - return this.doSetValue(this.valueToDataConverter.valueToData(value)).pipe( + return this.valueToDataConverter.valueToData(value).pipe( + switchMap(data => this.doSetValue(data)), catchError(err => { throw this.handleError(err); }) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.models.ts new file mode 100644 index 0000000000..3c0417a406 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.models.ts @@ -0,0 +1,346 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { DataToValueType, GetValueAction, GetValueSettings } from '@shared/models/action-widget-settings.models'; +import { cssUnit, Font } from '@shared/models/widget-settings.models'; +import { defaultWidgetAction, WidgetAction } from '@shared/models/widget.models'; + +const defaultMainColor = '#305680'; +const defaultBackgroundColor = '#E8E8E8'; + +export const defaultMainColorDisabled = 'rgba(0, 0, 0, 0.38)'; +export const defaultBackgroundColorDisabled = 'rgba(0, 0, 0, 0.03)'; +export const defaultBorderColorDisabled = defaultBackgroundColorDisabled; + +const defaultBorderColor = defaultBackgroundColor; + +export enum SegmentedButtonLayout { + squared = 'squared', + rounded = 'rounded' +} + +export type SegmentedButtonAppearanceType = 'first' | 'second'; +export type SegmentedButtonColorStylesType = 'selected' | 'unselected'; + +export const segmentedButtonLayouts = Object.keys(SegmentedButtonLayout) as SegmentedButtonLayout[]; + +export const segmentedButtonLayoutTranslations = new Map( + [ + [SegmentedButtonLayout.squared, 'widgets.segmented-button.layout-squared'], + [SegmentedButtonLayout.rounded, 'widgets.segmented-button.layout-rounded'] + ] +); + +export const segmentedButtonLayoutImages = new Map( + [ + [SegmentedButtonLayout.squared, 'assets/widget/segmented-button/squared-layout.svg'], + [SegmentedButtonLayout.rounded, 'assets/widget/segmented-button/rounded-layout.svg'] + ] +); + +export const segmentedButtonLayoutBorder = new Map( + [ + [SegmentedButtonLayout.squared, '4px'], + [SegmentedButtonLayout.rounded, '40px'] + ] +); + +export interface SegmentedButtonAppearance { + showLabel: boolean; + label: string; + labelFont: Font; + showIcon: boolean; + icon: string; + iconSize: number; + iconSizeUnit: cssUnit; +} + +export interface SegmentedButtonStyles { + mainColor: string; + backgroundColor: string; + customStyle: WidgetButtonToggleCustomStyles; +} + +export interface ButtonToggleAppearance { + layout: SegmentedButtonLayout; + autoScale: boolean; + cardBorder: number; + cardBorderColor: string; + leftAppearance: SegmentedButtonAppearance; + rightAppearance: SegmentedButtonAppearance; + selectedStyle: SegmentedButtonStyles; + unselectedStyle: SegmentedButtonStyles; +} + +export interface SegmentedButtonWidgetSettings { + initialState: GetValueSettings; + disabledState: GetValueSettings; + leftButtonClick: WidgetAction; + rightButtonClick: WidgetAction; + + appearance: ButtonToggleAppearance; +} + +export const segmentedButtonDefaultAppearance: ButtonToggleAppearance = { + layout: SegmentedButtonLayout.squared, + autoScale: true, + cardBorder: 1, + cardBorderColor: '#305680', + leftAppearance: { + showLabel: true, + label: 'Traditional', + labelFont: { + family: 'Roboto', + weight: '500', + style: 'normal', + size: 14, + sizeUnit: 'px', + lineHeight: '18px' + }, + showIcon: true, + icon: 'home', + iconSize: 24, + iconSizeUnit: 'px', + }, + rightAppearance: { + showLabel: true, + label: 'Hi-Perf', + labelFont: { + family: 'Roboto', + weight: '500', + style: 'normal', + size: 14, + sizeUnit: 'px', + lineHeight: '18px' + }, + showIcon: true, + icon: 'home', + iconSize: 24, + iconSizeUnit: 'px', + }, + selectedStyle: { + mainColor: '#FFFFFF', + backgroundColor: '#00695C', + customStyle: { + enabled: null, + hovered: null, + disabled: null + } + }, + unselectedStyle: { + mainColor: '#000000C2', + backgroundColor: '#E8E8E8', + customStyle: { + enabled: null, + hovered: null, + disabled: null + } + } +} + +export const segmentedButtonDefaultSettings: SegmentedButtonWidgetSettings = { + initialState: { + action: GetValueAction.DO_NOTHING, + defaultValue: true, + getAttribute: { + key: 'state', + scope: null + }, + getTimeSeries: { + key: 'state' + }, + getAlarmStatus: { + severityList: null, + typeList: null + }, + dataToValue: { + type: DataToValueType.NONE, + compareToValue: true, + dataToValueFunction: '/* Should return boolean value */\nreturn data;' + } + }, + disabledState: { + action: GetValueAction.DO_NOTHING, + defaultValue: false, + getAttribute: { + key: 'state', + scope: null + }, + getTimeSeries: { + key: 'state' + }, + getAlarmStatus: { + severityList: null, + typeList: null + }, + dataToValue: { + type: DataToValueType.NONE, + compareToValue: true, + dataToValueFunction: '/* Should return boolean value */\nreturn data;' + } + }, + leftButtonClick: defaultWidgetAction(), + rightButtonClick: defaultWidgetAction(), + appearance: segmentedButtonDefaultAppearance +}; + + +const mainCheckedColorVarPrefix = '--tb-widget-button-toggle-main-checked-color-'; +const backgroundCheckedColorVarPrefix = '--tb-widget-button-toggle-background-checked-color-'; +const borderCheckedColorVarPrefix = '--tb-widget-button-toggle-border-checked-color-'; + +const mainUncheckedColorVarPrefix = '--tb-widget-button-toggle-main-unchecked-color-'; +const backgroundUncheckedColorVarPrefix = '--tb-widget-button-toggle-background-unchecked-color-'; +const borderUncheckedColorVarPrefix = '--tb-widget-button-toggle-border-unchecked-color-'; + +export enum WidgetButtonToggleState { + enabled = 'enabled', + hovered = 'hovered', + disabled = 'disabled' +} + +export const widgetButtonToggleStates = Object.keys(WidgetButtonToggleState) as WidgetButtonToggleState[]; + +export const widgetButtonToggleStatesTranslations = new Map( + [ + [WidgetButtonToggleState.enabled, 'widgets.button-state.enabled'], + [WidgetButtonToggleState.hovered, 'widgets.button-state.hovered'], + [WidgetButtonToggleState.disabled, 'widgets.button-state.disabled'] + ] +); + +export interface WidgetButtonToggleCustomStyle { + overrideMainColor?: boolean; + mainColor?: string; + overrideBackgroundColor?: boolean; + backgroundColor?: string; + overrideBorderColor?: boolean; + borderColor?: string; +} + +export type WidgetButtonToggleCustomStyles = Record; + +export abstract class ButtonToggleStateCssGenerator { + + constructor() {} + + public generateStateCss(selectedAppearance: SegmentedButtonStyles, unselectedAppearance: SegmentedButtonStyles): string { + const selectedColor = this.getColors(selectedAppearance); + const unselectedColor = this.getColors(unselectedAppearance); + + return `${mainCheckedColorVarPrefix}${this.state}: ${selectedColor.mainColor};\n`+ + `${backgroundCheckedColorVarPrefix}${this.state}: ${selectedColor.backgroundColor};\n`+ + `${borderCheckedColorVarPrefix}${this.state}: ${selectedColor.borderColor};\n`+ + `${mainUncheckedColorVarPrefix}${this.state}: ${unselectedColor.mainColor};\n`+ + `${backgroundUncheckedColorVarPrefix}${this.state}: ${unselectedColor.backgroundColor};\n`+ + `${borderUncheckedColorVarPrefix}${this.state}: ${unselectedColor.borderColor};`; + } + + private getColors(appearance: SegmentedButtonStyles) { + let mainColor = this.getMainColor(appearance); + let backgroundColor = this.getBackgroundColor(appearance); + let borderColor = this.getBorderColor(); + const stateCustomStyle = appearance.customStyle[this.state]; + if (stateCustomStyle?.overrideMainColor && stateCustomStyle?.mainColor) { + mainColor = stateCustomStyle.mainColor; + } + if (stateCustomStyle?.overrideBackgroundColor && stateCustomStyle?.backgroundColor) { + backgroundColor = stateCustomStyle.backgroundColor; + } + if (stateCustomStyle?.overrideBorderColor && stateCustomStyle?.borderColor) { + borderColor = stateCustomStyle.borderColor; + } + return { + mainColor, + backgroundColor, + borderColor + } + } + + protected abstract get state(): WidgetButtonToggleState; + + protected getMainColor(appearance: SegmentedButtonStyles): string { + return appearance.mainColor || defaultMainColor; + } + + protected getBackgroundColor(appearance: SegmentedButtonStyles): string { + return appearance.backgroundColor || defaultBackgroundColor; + } + + protected getBorderColor(): string { + return defaultBorderColor; + } +} + +class EnabledButtonStateCssGenerator extends ButtonToggleStateCssGenerator { + + protected get state(): WidgetButtonToggleState { + return WidgetButtonToggleState.enabled; + } +} + +class HoveredButtonStateCssGenerator extends ButtonToggleStateCssGenerator { + + protected get state(): WidgetButtonToggleState { + return WidgetButtonToggleState.hovered; + } +} + +class DisabledButtonStateCssGenerator extends ButtonToggleStateCssGenerator { + + protected get state(): WidgetButtonToggleState { + return WidgetButtonToggleState.disabled; + } + + protected getMainColor(): string { + return defaultMainColorDisabled; + } + + protected getBackgroundColor(): string { + return defaultBackgroundColorDisabled; + } + + protected getBorderColor(): string { + return defaultBorderColorDisabled; + } +} + +const buttonToggleStateCssGeneratorsMap = new Map( + [ + [WidgetButtonToggleState.enabled, new EnabledButtonStateCssGenerator()], + [WidgetButtonToggleState.hovered, new HoveredButtonStateCssGenerator()], + [WidgetButtonToggleState.disabled, new DisabledButtonStateCssGenerator()] + ] +); + +const widgetButtonCssSelector = '.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header'; + +export const generateWidgetButtonToggleAppearanceCss = (selectedAppearance: SegmentedButtonStyles, unselectedAppearance: SegmentedButtonStyles): string => { + let statesCss = ''; + for (const state of widgetButtonToggleStates) { + const generator = buttonToggleStateCssGeneratorsMap.get(state); + statesCss += `\n${generator.generateStateCss(selectedAppearance, unselectedAppearance)}`; + } + return `${widgetButtonCssSelector} {\n`+ + `${statesCss}\n`+ + `}`; +}; + +export const generateWidgetButtonToggleBorderLayout = (layout: SegmentedButtonLayout): string => { + return `${widgetButtonCssSelector} {\n`+ + `--tb-widget-button-toggle-border-radius: ${segmentedButtonLayoutBorder.get(layout)}\n`+ + `}`; +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.html new file mode 100644 index 0000000000..1aac07bd4a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.html @@ -0,0 +1,34 @@ + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.scss new file mode 100644 index 0000000000..66aec79aa4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.scss @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-segmented-button-widget { + width: 100%; + height: 100%; + position: relative; + + .tb-segmented-button-container { + height: 100%; + + .tb-widget-button-toggle { + display: block; + height: 100%; + } + } + + > div.tb-segmented-button-widget-title-panel { + position: absolute; + top: 12px; + left: 12px; + right: 12px; + z-index: 2; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.ts new file mode 100644 index 0000000000..042f1518b6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.ts @@ -0,0 +1,118 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; +import { BasicActionWidgetComponent } from '@home/components/widget/lib/action/action-widget.models'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; +import { ValueType } from '@shared/models/constants'; +import { + ButtonToggleAppearance, + segmentedButtonDefaultSettings, + SegmentedButtonWidgetSettings +} from '@home/components/widget/lib/button/segmented-button-widget.models'; +import { ComponentStyle } from '@shared/models/widget-settings.models'; +import { MatButtonToggleChange } from '@angular/material/button-toggle'; + +@Component({ + selector: 'tb-two-segment-button-widget', + templateUrl: './two-segment-button-widget.component.html', + styleUrls: ['../action/action-widget.scss', './two-segment-button-widget.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class TwoSegmentButtonWidgetComponent extends + BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { + + settings: SegmentedButtonWidgetSettings; + + overlayStyle: ComponentStyle = {}; + + value = false; + disabled = false; + + autoScale: boolean; + appearance: ButtonToggleAppearance; + + constructor(protected imagePipe: ImagePipe, + protected sanitizer: DomSanitizer, + protected cd: ChangeDetectorRef) { + super(cd); + } + + ngOnInit(): void { + super.ngOnInit(); + this.settings = {...segmentedButtonDefaultSettings, ...this.ctx.settings}; + + this.autoScale = this.settings.appearance.autoScale; + + const getInitialStateSettings = + {...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.initial-state')}; + this.createValueGetter(getInitialStateSettings, ValueType.BOOLEAN, { + next: (value) => this.onValue(value) + }); + + const disabledStateSettings = + {...this.settings.disabledState, actionLabel: this.ctx.translate.instant('widgets.button-state.disabled-state')}; + this.createValueGetter(disabledStateSettings, ValueType.BOOLEAN, { + next: (value) => this.onDisabled(value) + }); + + this.appearance = this.settings.appearance; + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + } + + ngOnDestroy() { + super.ngOnDestroy(); + } + + public onInit() { + super.onInit(); + const borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; + this.cd.detectChanges(); + } + + private onValue(value: boolean): void { + const newValue = !!value; + if (this.value !== newValue) { + this.value = newValue; + this.appearance = this.settings.appearance; + this.cd.markForCheck(); + } + } + + private onDisabled(value: boolean): void { + const newDisabled = !!value; + if (this.disabled !== newDisabled) { + this.disabled = newDisabled; + this.cd.markForCheck(); + } + } + + public onClick(_$event: MatButtonToggleChange) { + if (!this.ctx.isEdit && !this.ctx.isPreview) { + if (_$event.value) { + this.ctx.actionsApi.onWidgetAction(event, this.settings.leftButtonClick); + } else { + this.ctx.actionsApi.onWidgetAction(event, this.settings.rightButtonClick); + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/circle.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/circle.ts index 225dc66e06..680f3faca6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/circle.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/circle.ts @@ -16,14 +16,11 @@ import L, { LeafletMouseEvent } from 'leaflet'; import { CircleData, WidgetCircleSettings } from '@home/components/widget/lib/maps/map-models'; -import { - functionValueCalculator, - parseWithTranslation -} from '@home/components/widget/lib/maps/common-maps-utils'; +import { functionValueCalculator, parseWithTranslation } from '@home/components/widget/lib/maps/common-maps-utils'; import LeafletMap from '@home/components/widget/lib/maps/leaflet-map'; import { createTooltip } from '@home/components/widget/lib/maps/maps-utils'; import { FormattedData } from '@shared/models/widget.models'; -import { fillDataPattern, processDataPattern, safeExecute } from '@core/utils'; +import { fillDataPattern, processDataPattern, safeExecuteTbFunction } from '@core/utils'; export class Circle { @@ -94,7 +91,7 @@ export class Circle { if (this.settings.showCircleLabel) { if (!this.map.circleLabelText || this.settings.useCircleLabelFunction) { const pattern = this.settings.useCircleLabelFunction ? - safeExecute(this.settings.parsedCircleLabelFunction, + safeExecuteTbFunction(this.settings.parsedCircleLabelFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.circleLabel; this.map.circleLabelText = parseWithTranslation.prepareProcessPattern(pattern, true); this.map.replaceInfoTooltipCircle = processDataPattern(this.map.circleLabelText, this.data); @@ -109,7 +106,7 @@ export class Circle { private updateTooltip() { const pattern = this.settings.useCircleTooltipFunction ? - safeExecute(this.settings.parsedCircleTooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : + safeExecuteTbFunction(this.settings.parsedCircleTooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.circleTooltipPattern; this.tooltip.setContent(parseWithTranslation.parseTemplate(pattern, this.data, true)); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts index 25f7b1ceec..cd974ee22c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts @@ -29,6 +29,7 @@ import { map } from 'rxjs/operators'; import { FormattedData } from '@shared/models/widget.models'; import L from 'leaflet'; import { ImagePipe } from '@shared/pipe/image.pipe'; +import { CompiledTbFunction, GenericFunction } from '@shared/models/js-function.models'; export function getRatio(firsMoment: number, secondMoment: number, intermediateMoment: number): number { return (intermediateMoment - firsMoment) / (secondMoment - firsMoment); @@ -257,11 +258,11 @@ export const parseWithTranslation = { } }; -export function functionValueCalculator(useFunction: boolean, func: (...args: any[]) => any, params = [], defaultValue: T): T { +export function functionValueCalculator(useFunction: boolean, func: CompiledTbFunction, params = [], defaultValue: T): T { let res: T; if (useFunction && isDefined(func) && isFunction(func)) { try { - res = func(...params); + res = func.execute(...params); if (!isDefinedAndNotNull(res) || res === '') { res = defaultValue; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts index 29b2768e58..ee5af0bfb5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts @@ -54,7 +54,7 @@ import { isNotEmptyStr, isString, mergeFormattedData, - safeExecute + safeExecuteTbFunction } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; import { @@ -63,9 +63,9 @@ import { } from '@home/components/widget/lib/maps/dialogs/select-entity-dialog.component'; import { MatDialog } from '@angular/material/dialog'; import { FormattedData, ReplaceInfo } from '@shared/models/widget.models'; -import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import { ImagePipe } from '@shared/pipe/image.pipe'; import { take, tap } from 'rxjs/operators'; +import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; export default abstract class LeafletMap { @@ -149,7 +149,7 @@ export default abstract class LeafletMap { const childCount = cluster.getChildCount(); const formattedData = cluster.getAllChildMarkers().map(clusterMarker => clusterMarker.options.tbMarkerData); const markerColor = markerClusteringSettings.clusterMarkerFunction - ? safeExecute(markerClusteringSettings.parsedClusterMarkerFunction, + ? safeExecuteTbFunction(markerClusteringSettings.parsedClusterMarkerFunction, [formattedData, childCount]) : null; if (isDefinedAndNotNull(markerColor) && tinycolor(markerColor).isValid()) { @@ -899,7 +899,7 @@ export default abstract class LeafletMap { rawMarkers.forEach(data => { if (data.rotationAngle || data.rotationAngle === 0) { const currentImage: MarkerImageInfo = this.options.useMarkerImageFunction ? - safeExecute(this.options.parsedMarkerImageFunction, + safeExecuteTbFunction(this.options.parsedMarkerImageFunction, [data, this.options.markerImages, markersData, data.dsIndex]) : this.options.currentImage; const imageUrl$ = currentImage @@ -1042,7 +1042,7 @@ export default abstract class LeafletMap { if (!!this.extractPosition(pdata)) { const dsData = pointsData.map(ds => ds[tsIndex]); if (this.options.useColorPointFunction) { - pointColor = safeExecute(this.options.parsedColorPointFunction, [pdata, dsData, pdata.dsIndex]); + pointColor = safeExecuteTbFunction(this.options.parsedColorPointFunction, [pdata, dsData, pdata.dsIndex]); } const point = L.circleMarker(this.convertPosition(pdata, dsData), { color: pointColor, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts index af8f51ffdd..6705c7b853 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts @@ -18,6 +18,7 @@ import { Datasource, FormattedData } from '@app/shared/models/widget.models'; import tinycolor from 'tinycolor2'; import { BaseIconOptions, Icon } from 'leaflet'; import { Observable } from 'rxjs'; +import { CompiledTbFunction, TbFunction } from '@shared/models/js-function.models'; export const DEFAULT_MAP_PAGE_SIZE = 16384; export const DEFAULT_ZOOM_LEVEL = 8; @@ -273,7 +274,7 @@ export interface TripAnimationCommonSettings { } export interface WidgetTripAnimationCommonSettings extends TripAnimationCommonSettings { - parsedTooltipFunction: GenericFunction; + parsedTooltipFunction: CompiledTbFunction; } export const defaultTripAnimationCommonSettings: TripAnimationCommonSettings = { @@ -305,35 +306,35 @@ export const showTooltipActionTranslationMap = new Map; + parsedTooltipFunction: CompiledTbFunction; + parsedColorFunction: CompiledTbFunction; + parsedMarkerImageFunction: CompiledTbFunction; markerClick: { [name: string]: actionsHandler }; currentImage: MarkerImageInfo; tinyColor: tinycolor.Instance; @@ -382,8 +383,7 @@ export interface TripAnimationMarkerSettings { } export interface WidgetTripAnimationMarkerSettings extends TripAnimationMarkerSettings { - parsedLabelFunction: GenericFunction; - parsedMarkerImageFunction: MarkerImageFunction; + parsedLabelFunction: CompiledTbFunction; } export const defaultTripAnimationMarkersSettings: TripAnimationMarkerSettings = { @@ -406,29 +406,29 @@ export interface PolygonSettings { showPolygonLabel: boolean; usePolygonLabelFunction: boolean; polygonLabel?: string; - polygonLabelFunction?: string; + polygonLabelFunction?: TbFunction; showPolygonTooltip: boolean; showPolygonTooltipAction: ShowTooltipAction; autoClosePolygonTooltip: boolean; usePolygonTooltipFunction: boolean; polygonTooltipPattern?: string; - polygonTooltipFunction?: string; + polygonTooltipFunction?: TbFunction; polygonColor?: string; polygonOpacity?: number; usePolygonColorFunction: boolean; - polygonColorFunction?: string; + polygonColorFunction?: TbFunction; polygonStrokeColor?: string; polygonStrokeOpacity?: number; polygonStrokeWeight?: number; usePolygonStrokeColorFunction: boolean; - polygonStrokeColorFunction?: string; + polygonStrokeColorFunction?: TbFunction; } export interface WidgetPolygonSettings extends PolygonSettings, WidgetToolipSettings { - parsedPolygonLabelFunction: GenericFunction; - parsedPolygonTooltipFunction: GenericFunction; - parsedPolygonColorFunction: GenericFunction; - parsedPolygonStrokeColorFunction: GenericFunction; + parsedPolygonLabelFunction: CompiledTbFunction; + parsedPolygonTooltipFunction: CompiledTbFunction; + parsedPolygonColorFunction: CompiledTbFunction; + parsedPolygonStrokeColorFunction: CompiledTbFunction; polygonClick: { [name: string]: actionsHandler }; } @@ -464,29 +464,29 @@ export interface CircleSettings { showCircleLabel: boolean; useCircleLabelFunction: boolean; circleLabel?: string; - circleLabelFunction?: string; + circleLabelFunction?: TbFunction; showCircleTooltip: boolean; showCircleTooltipAction: ShowTooltipAction; autoCloseCircleTooltip: boolean; useCircleTooltipFunction: boolean; circleTooltipPattern?: string; - circleTooltipFunction?: string; + circleTooltipFunction?: TbFunction; circleFillColor?: string; circleFillColorOpacity?: number; useCircleFillColorFunction: boolean; - circleFillColorFunction?: string; + circleFillColorFunction?: TbFunction; circleStrokeColor?: string; circleStrokeOpacity?: number; circleStrokeWeight?: number; useCircleStrokeColorFunction: boolean; - circleStrokeColorFunction?: string; + circleStrokeColorFunction?: TbFunction; } export interface WidgetCircleSettings extends CircleSettings, WidgetToolipSettings { - parsedCircleLabelFunction: GenericFunction; - parsedCircleTooltipFunction: GenericFunction; - parsedCircleFillColorFunction: GenericFunction; - parsedCircleStrokeColorFunction: GenericFunction; + parsedCircleLabelFunction: CompiledTbFunction; + parsedCircleTooltipFunction: CompiledTbFunction; + parsedCircleFillColorFunction: CompiledTbFunction; + parsedCircleStrokeColorFunction: CompiledTbFunction; circleClick: { [name: string]: actionsHandler }; } @@ -530,13 +530,13 @@ export const polylineDecoratorSymbolTranslationMap = new Map; + parsedStrokeOpacityFunction: CompiledTbFunction; + parsedStrokeWeightFunction: CompiledTbFunction; } export const defaultRouteMapSettings: PolylineSettings = { @@ -578,7 +578,7 @@ export interface PointsSettings { showPoints?: boolean; pointColor?: string; useColorPointFunction?: false; - colorPointFunction?: string; + colorPointFunction?: TbFunction; pointSize?: number; usePointAsAnchor?: false; pointAsAnchorFunction?: string; @@ -586,8 +586,8 @@ export interface PointsSettings { } export interface WidgetPointsSettings extends PointsSettings { - parsedColorPointFunction: GenericFunction; - parsedPointAsAnchorFunction: GenericFunction; + parsedColorPointFunction: CompiledTbFunction; + parsedPointAsAnchorFunction: CompiledTbFunction; } export const defaultTripAnimationPointSettings: PointsSettings = { @@ -612,11 +612,11 @@ export interface MarkerClusteringSettings { chunkedLoading: boolean; removeOutsideVisibleBounds: boolean; useIconCreateFunction: boolean; - clusterMarkerFunction?: string; + clusterMarkerFunction?: TbFunction; } export interface WidgetMarkerClusteringSettings extends MarkerClusteringSettings { - parsedClusterMarkerFunction?: GenericFunction; + parsedClusterMarkerFunction?: CompiledTbFunction; } export const defaultMarkerClusteringSettings: MarkerClusteringSettings = { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts index 58702653e5..8dea835e24 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts @@ -30,9 +30,9 @@ import { TranslateService } from '@ngx-translate/core'; import { UtilsService } from '@core/services/utils.service'; import { EntityDataPageLink } from '@shared/models/query/query.models'; import { providerClass } from '@home/components/widget/lib/maps/providers/public-api'; -import { isDefined, isDefinedAndNotNull, parseFunction } from '@core/utils'; +import { isDefined, isDefinedAndNotNull, parseFunction, parseTbFunction } from '@core/utils'; import L from 'leaflet'; -import { forkJoin, Observable, of } from 'rxjs'; +import { firstValueFrom, forkJoin, from, Observable, of } from 'rxjs'; import { AttributeService } from '@core/http/attribute.service'; import { EntityId } from '@shared/models/id/entity-id'; import { AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; @@ -40,12 +40,18 @@ import { AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/tel // @dynamic export class MapWidgetController implements MapWidgetInterface { + private updatePending = false; + private latestUpdatePending = false; + private resizePending = false; + private destroyed = false; + constructor( public mapProvider: MapProviders, private drawRoutes: boolean, public ctx: WidgetContext, $element: HTMLElement, - isEdit?: boolean + isEdit = false, + mapLoaded?: (map: LeafletMap) => void ) { if (this.map) { this.map.map.remove(); @@ -56,37 +62,53 @@ export class MapWidgetController implements MapWidgetInterface { if (!$element) { $element = ctx.$container[0]; } - this.settings = this.initSettings(ctx.settings, isEdit); - this.settings.tooltipAction = this.getDescriptors('tooltipAction'); - this.settings.markerClick = this.getDescriptors('markerClick'); - this.settings.polygonClick = this.getDescriptors('polygonClick'); - this.settings.circleClick = this.getDescriptors('circleClick'); + from(this.initSettings(ctx.settings, isEdit)).subscribe(settings => { + if (!this.destroyed) { + this.settings = settings; + this.settings.tooltipAction = this.getDescriptors('tooltipAction'); + this.settings.markerClick = this.getDescriptors('markerClick'); + this.settings.polygonClick = this.getDescriptors('polygonClick'); + this.settings.circleClick = this.getDescriptors('circleClick'); - const MapClass = providerClass[this.provider]; - if (!MapClass) { - return; - } - parseWithTranslation.setTranslate(this.translate); - this.map = new MapClass(this.ctx, $element, this.settings); - (this.ctx as any).mapInstance = this.map; - this.map.saveMarkerLocation = this.setMarkerLocation.bind(this); - this.map.savePolygonLocation = this.savePolygonLocation.bind(this); - this.map.saveLocation = this.saveLocation.bind(this); - let pageSize = this.settings.mapPageSize; - if (isDefinedAndNotNull(this.ctx.widgetConfig.pageSize)) { - pageSize = Math.max(pageSize, this.ctx.widgetConfig.pageSize); - } - this.pageLink = { - page: 0, - pageSize, - textSearch: null, - dynamic: true - }; - this.map.setLoading(true); - this.ctx.defaultSubscription.paginatedDataSubscriptionUpdated.subscribe(() => { - this.map.resetState(); + const MapClass = providerClass[this.provider]; + if (!MapClass) { + return; + } + parseWithTranslation.setTranslate(this.translate); + this.map = new MapClass(this.ctx, $element, this.settings); + (this.ctx as any).mapInstance = this.map; + this.map.saveMarkerLocation = this.setMarkerLocation.bind(this); + this.map.savePolygonLocation = this.savePolygonLocation.bind(this); + this.map.saveLocation = this.saveLocation.bind(this); + let pageSize = this.settings.mapPageSize; + if (isDefinedAndNotNull(this.ctx.widgetConfig.pageSize)) { + pageSize = Math.max(pageSize, this.ctx.widgetConfig.pageSize); + } + this.pageLink = { + page: 0, + pageSize, + textSearch: null, + dynamic: true + }; + this.map.setLoading(true); + this.ctx.defaultSubscription.paginatedDataSubscriptionUpdated.subscribe(() => { + this.map.resetState(); + }); + this.ctx.defaultSubscription.subscribeAllForPaginatedData(this.pageLink, null); + if (this.updatePending) { + this.update(); + } + if (this.latestUpdatePending) { + this.latestDataUpdate(); + } + if (this.resizePending) { + this.resize(); + } + if (mapLoaded) { + mapLoaded(this.map); + } + } }); - this.ctx.defaultSubscription.subscribeAllForPaginatedData(this.pageLink, null); } map: LeafletMap; @@ -233,27 +255,27 @@ export class MapWidgetController implements MapWidgetInterface { } } - initSettings(settings: UnitedMapSettings, isEditMap?: boolean): WidgetUnitedMapSettings { + async initSettings(settings: UnitedMapSettings, isEditMap?: boolean): Promise { const functionParams = ['data', 'dsData', 'dsIndex']; this.provider = settings.provider || this.mapProvider; const parsedOptions: Partial = { provider: this.provider, - parsedLabelFunction: parseFunction(settings.labelFunction, functionParams), - parsedTooltipFunction: parseFunction(settings.tooltipFunction, functionParams), - parsedColorFunction: parseFunction(settings.colorFunction, functionParams), - parsedColorPointFunction: parseFunction(settings.colorPointFunction, functionParams), - parsedStrokeOpacityFunction: parseFunction(settings.strokeOpacityFunction, functionParams), - parsedStrokeWeightFunction: parseFunction(settings.strokeWeightFunction, functionParams), - parsedPolygonLabelFunction: parseFunction(settings.polygonLabelFunction, functionParams), - parsedPolygonColorFunction: parseFunction(settings.polygonColorFunction, functionParams), - parsedPolygonStrokeColorFunction: parseFunction(settings.polygonStrokeColorFunction, functionParams), - parsedPolygonTooltipFunction: parseFunction(settings.polygonTooltipFunction, functionParams), - parsedCircleLabelFunction: parseFunction(settings.circleLabelFunction, functionParams), - parsedCircleStrokeColorFunction: parseFunction(settings.circleStrokeColorFunction, functionParams), - parsedCircleFillColorFunction: parseFunction(settings.circleFillColorFunction, functionParams), - parsedCircleTooltipFunction: parseFunction(settings.circleTooltipFunction, functionParams), - parsedMarkerImageFunction: parseFunction(settings.markerImageFunction, ['data', 'images', 'dsData', 'dsIndex']), - parsedClusterMarkerFunction: parseFunction(settings.clusterMarkerFunction, ['data', 'childCount']), + parsedLabelFunction: await firstValueFrom(parseTbFunction(this.ctx.http, settings.labelFunction, functionParams)), + parsedTooltipFunction: await firstValueFrom(parseTbFunction(this.ctx.http, settings.tooltipFunction, functionParams)), + parsedColorFunction: await firstValueFrom(parseTbFunction(this.ctx.http, settings.colorFunction, functionParams)), + parsedColorPointFunction: await firstValueFrom(parseTbFunction(this.ctx.http, settings.colorPointFunction, functionParams)), + parsedStrokeOpacityFunction: await firstValueFrom(parseTbFunction(this.ctx.http, settings.strokeOpacityFunction, functionParams)), + parsedStrokeWeightFunction: await firstValueFrom(parseTbFunction(this.ctx.http, settings.strokeWeightFunction, functionParams)), + parsedPolygonLabelFunction: await firstValueFrom(parseTbFunction(this.ctx.http, settings.polygonLabelFunction, functionParams)), + parsedPolygonColorFunction: await firstValueFrom(parseTbFunction(this.ctx.http, settings.polygonColorFunction, functionParams)), + parsedPolygonStrokeColorFunction: await firstValueFrom(parseTbFunction(this.ctx.http, settings.polygonStrokeColorFunction, functionParams)), + parsedPolygonTooltipFunction: await firstValueFrom(parseTbFunction(this.ctx.http, settings.polygonTooltipFunction, functionParams)), + parsedCircleLabelFunction: await firstValueFrom(parseTbFunction(this.ctx.http, settings.circleLabelFunction, functionParams)), + parsedCircleStrokeColorFunction: await firstValueFrom(parseTbFunction(this.ctx.http, settings.circleStrokeColorFunction, functionParams)), + parsedCircleFillColorFunction: await firstValueFrom(parseTbFunction(this.ctx.http, settings.circleFillColorFunction, functionParams)), + parsedCircleTooltipFunction: await firstValueFrom(parseTbFunction(this.ctx.http, settings.circleTooltipFunction, functionParams)), + parsedMarkerImageFunction: await firstValueFrom(parseTbFunction(this.ctx.http, settings.markerImageFunction, ['data', 'images', 'dsData', 'dsIndex'])), + parsedClusterMarkerFunction: await firstValueFrom(parseTbFunction(this.ctx.http, settings.clusterMarkerFunction, ['data', 'childCount'])), // labelColor: this.ctx.widgetConfig.color, // polygonLabelColor: this.ctx.widgetConfig.color, polygonKeyName: (settings as any).polKeyName ? (settings as any).polKeyName : settings.polygonKeyName, @@ -277,20 +299,36 @@ export class MapWidgetController implements MapWidgetInterface { } update() { + if (this.map) { + this.updatePending = false; this.map.updateData(this.drawRoutes); this.map.setLoading(false); + } else { + this.updatePending = true; + } } latestDataUpdate() { - this.map.updateData(this.drawRoutes); + if (this.map) { + this.latestUpdatePending = false; + this.map.updateData(this.drawRoutes); + } else { + this.latestUpdatePending = true; + } } resize() { - this.map.onResize(); - this.map?.invalidateSize(); + if (this.map) { + this.resizePending = false; + this.map.onResize(); + this.map.invalidateSize(); + } else { + this.resizePending = true; + } } destroy() { + this.destroyed = true; if (this.map) { this.map.remove(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts index 125bf4b5c4..d47ed8f110 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts @@ -15,13 +15,11 @@ /// import L from 'leaflet'; -import { - GenericFunction, - ShowTooltipAction, WidgetToolipSettings -} from './map-models'; +import { GenericFunction, ShowTooltipAction, WidgetToolipSettings } from './map-models'; import { Datasource, FormattedData } from '@app/shared/models/widget.models'; -import { fillDataPattern, isDefinedAndNotNull, isString, processDataPattern, safeExecute } from '@core/utils'; +import { fillDataPattern, isDefinedAndNotNull, isString, processDataPattern, safeExecuteTbFunction } from '@core/utils'; import { parseWithTranslation } from '@home/components/widget/lib/maps/common-maps-utils'; +import { CompiledTbFunction } from '@shared/models/js-function.models'; export function createTooltip(target: L.Layer, settings: Partial, @@ -90,7 +88,7 @@ export function isJSON(data: string): boolean { export interface LabelSettings { showLabel: boolean; useLabelFunction: boolean; - parsedLabelFunction: GenericFunction; + parsedLabelFunction: CompiledTbFunction; label: string; } @@ -98,7 +96,7 @@ export function entitiesParseName(entities: FormattedData[], labelSettings: Labe const div = document.createElement('div'); for (const entity of entities) { if (labelSettings?.showLabel) { - const pattern = labelSettings.useLabelFunction ? safeExecute(labelSettings.parsedLabelFunction, + const pattern = labelSettings.useLabelFunction ? safeExecuteTbFunction(labelSettings.parsedLabelFunction, [entity, entities, entity.dsIndex]) : labelSettings.label; const markerLabelText = parseWithTranslation.prepareProcessPattern(pattern, true); const replaceInfoLabelMarker = processDataPattern(pattern, entity); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts index e41928d9e9..d62b2b9217 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts @@ -19,7 +19,13 @@ import { MarkerIconInfo, MarkerIconReadyFunction, MarkerImageInfo, WidgetMarkers import { bindPopupActions, createTooltip } from './maps-utils'; import { loadImageWithAspect, parseWithTranslation } from './common-maps-utils'; import tinycolor from 'tinycolor2'; -import { fillDataPattern, isDefined, isDefinedAndNotNull, processDataPattern, safeExecute } from '@core/utils'; +import { + fillDataPattern, + isDefined, + isDefinedAndNotNull, + processDataPattern, + safeExecuteTbFunction +} from '@core/utils'; import LeafletMap from './leaflet-map'; import { FormattedData } from '@shared/models/widget.models'; import { ImagePipe } from '@shared/pipe/image.pipe'; @@ -101,7 +107,7 @@ export class Marker { updateMarkerTooltip(data: FormattedData) { if (!this.map.markerTooltipText || this.settings.useTooltipFunction) { const pattern = this.settings.useTooltipFunction ? - safeExecute(this.settings.parsedTooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.tooltipPattern; + safeExecuteTbFunction(this.settings.parsedTooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.tooltipPattern; this.map.markerTooltipText = parseWithTranslation.prepareProcessPattern(pattern, true); this.map.replaceInfoTooltipMarker = processDataPattern(this.map.markerTooltipText, data); } @@ -123,7 +129,7 @@ export class Marker { if (settings.showLabel) { if (!this.map.markerLabelText || settings.useLabelFunction) { const pattern = settings.useLabelFunction ? - safeExecute(settings.parsedLabelFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.label; + safeExecuteTbFunction(settings.parsedLabelFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.label; this.map.markerLabelText = parseWithTranslation.prepareProcessPattern(pattern, true); this.map.replaceInfoLabelMarker = processDataPattern(this.map.markerLabelText, this.data); } @@ -165,11 +171,11 @@ export class Marker { return; } const currentImage: MarkerImageInfo = this.settings.useMarkerImageFunction ? - safeExecute(this.settings.parsedMarkerImageFunction, + safeExecuteTbFunction(this.settings.parsedMarkerImageFunction, [this.data, this.settings.markerImages, this.dataSources, this.data.dsIndex]) : this.settings.currentImage; let currentColor = this.settings.tinyColor; if (this.settings.useColorFunction) { - const functionColor = safeExecute(this.settings.parsedColorFunction, + const functionColor = safeExecuteTbFunction(this.settings.parsedColorFunction, [this.data, this.dataSources, this.data.dsIndex]); if (isDefinedAndNotNull(functionColor)) { currentColor = tinycolor(functionColor); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts index aef95731e5..cb9ba7c35f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts @@ -16,13 +16,10 @@ import L, { LatLngExpression, LeafletMouseEvent } from 'leaflet'; import { createTooltip, isCutPolygon } from './maps-utils'; -import { - functionValueCalculator, - parseWithTranslation -} from './common-maps-utils'; +import { functionValueCalculator, parseWithTranslation } from './common-maps-utils'; import { WidgetPolygonSettings } from './map-models'; import { FormattedData } from '@shared/models/widget.models'; -import { fillDataPattern, processDataPattern, safeExecute } from '@core/utils'; +import { fillDataPattern, processDataPattern, safeExecuteTbFunction } from '@core/utils'; import LeafletMap from '@home/components/widget/lib/maps/leaflet-map'; export class Polygon { @@ -92,7 +89,7 @@ export class Polygon { updateTooltip(data: FormattedData) { const pattern = this.settings.usePolygonTooltipFunction ? - safeExecute(this.settings.parsedPolygonTooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : + safeExecuteTbFunction(this.settings.parsedPolygonTooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.polygonTooltipPattern; this.tooltip.setContent(parseWithTranslation.parseTemplate(pattern, data, true)); } @@ -102,7 +99,7 @@ export class Polygon { if (settings.showPolygonLabel) { if (!this.map.polygonLabelText || settings.usePolygonLabelFunction) { const pattern = settings.usePolygonLabelFunction ? - safeExecute(settings.parsedPolygonLabelFunction, + safeExecuteTbFunction(settings.parsedPolygonLabelFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.polygonLabel; this.map.polygonLabelText = parseWithTranslation.prepareProcessPattern(pattern, true); this.map.replaceInfoLabelPolygon = processDataPattern(this.map.polygonLabelText, this.data); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts index 8cf1822ad5..0abcc9f7c8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts @@ -23,16 +23,17 @@ import { PosFunction, WidgetUnitedMapSettings } from '../map-models'; -import { Observable, of, ReplaySubject, switchMap } from 'rxjs'; +import { forkJoin, Observable, of, ReplaySubject, switchMap } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { calculateNewPointCoordinate, loadImageWithAspect } from '@home/components/widget/lib/maps/common-maps-utils'; import { WidgetContext } from '@home/models/widget-component.models'; import { DataSet, DatasourceType, FormattedData, widgetType } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; -import { isDefinedAndNotNull, isEmptyStr, isNotEmptyStr, parseFunction } from '@core/utils'; +import { isDefinedAndNotNull, isEmptyStr, isNotEmptyStr, parseFunction, parseTbFunction } from '@core/utils'; import { EntityDataPageLink } from '@shared/models/query/query.models'; import { ImagePipe } from '@shared/pipe/image.pipe'; +import { CompiledTbFunction } from '@shared/models/js-function.models'; const maxZoom = 4; // ? @@ -43,13 +44,20 @@ export class ImageMap extends LeafletMap { width = 0; height = 0; imageUrl: string; - posFunction: PosFunction; + posFunction: CompiledTbFunction; constructor(ctx: WidgetContext, $container: HTMLElement, options: WidgetUnitedMapSettings) { super(ctx, $container, options); - this.posFunction = parseFunction(options.posFunction, - ['origXPos', 'origYPos', 'data', 'dsData', 'dsIndex', 'aspect']) as PosFunction; - this.mapImage(options).subscribe((mapImage) => { + + const initData = { + posFunction: parseTbFunction(this.ctx.http, options.posFunction, + ['origXPos', 'origYPos', 'data', 'dsData', 'dsIndex', 'aspect']), + mapImage: this.mapImage(options) + }; + + forkJoin(initData).subscribe(inited => { + this.posFunction = inited.posFunction; + const mapImage = inited.mapImage; this.imageUrl = mapImage.imageUrl; this.aspect = mapImage.aspect; if (mapImage.update) { @@ -272,7 +280,7 @@ export class ImageMap extends LeafletMap { convertPosition(data: FormattedData, dsData: FormattedData[]): L.LatLng { const position = this.extractPosition(data); if (position) { - const converted = this.posFunction(position.x, position.y, data, dsData, data.dsIndex, this.aspect) || {x: 0, y: 0}; + const converted = this.posFunction.execute(position.x, position.y, data, dsData, data.dsIndex, this.aspect) || {x: 0, y: 0}; return this.positionToLatLng(converted); } else { return null; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html new file mode 100644 index 0000000000..e259919cc6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html @@ -0,0 +1,263 @@ + + + + widgets.action-button.behavior + + widgets.button-state.selected-state + + + + + widgets.action-button.first-button-click + + + + + widgets.action-button.second-button-click + + + + + + widgets.button-state.disabled-state + + + + + widget-config.appearance + + + + {{ segmentedButtonLayoutTranslationMap.get(layout) | translate }} + + + + + {{ 'widgets.button.auto-scale' | translate }} + + + + widgets.segmented-button.card-border + + + + px + + + + + + + + + + widgets.segmented-button.button-appearance + + {{ 'widgets.segmented-button.first' | translate }} + {{ 'widgets.segmented-button.second' | translate }} + + + + + + {{ 'widgets.button.label' | translate }} + + + + + + + + + + + + {{ 'widgets.button.icon' | translate }} + + + + + + + + + + + + + + + + {{ 'widgets.button.label' | translate }} + + + + + + + + + + + + {{ 'widgets.button.icon' | translate }} + + + + + + + + + + + + + + + + widgets.segmented-button.color-styles + + {{ 'widgets.segmented-button.selected' | translate }} + {{ 'widgets.segmented-button.unselected' | translate }} + + + + + {{ 'widgets.button.color-palette' | translate }} + + + widgets.button.main + + + + + + widgets.button.background + + + + + + + + + + widgets.button.custom-styles + + + + + {{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }} + + + + + + + + + + {{ 'widgets.button.color-palette' | translate }} + + + widgets.button.main + + + + + + widgets.button.background + + + + + + + + + + widgets.button.custom-styles + + + + + {{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }} + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts new file mode 100644 index 0000000000..7dab81ccad --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts @@ -0,0 +1,134 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetType } from '@shared/models/widget.models'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ValueType } from '@shared/models/constants'; +import { getTargetDeviceFromDatasources } from '@shared/models/widget-settings.models'; +import { + SegmentedButtonAppearanceType, + SegmentedButtonColorStylesType, + segmentedButtonDefaultSettings, + segmentedButtonLayoutBorder, + segmentedButtonLayoutImages, + segmentedButtonLayouts, + segmentedButtonLayoutTranslations, + WidgetButtonToggleState, + widgetButtonToggleStatesTranslations +} from '@home/components/widget/lib/button/segmented-button-widget.models'; + +@Component({ + selector: 'tb-segmented-button-widget-settings', + templateUrl: './segmented-button-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class SegmentedButtonWidgetSettingsComponent extends WidgetSettingsComponent { + + get targetDevice(): TargetDevice { + const datasources = this.widgetConfig?.config?.datasources; + return getTargetDeviceFromDatasources(datasources); + } + + get widgetType(): widgetType { + return this.widgetConfig?.widgetType; + } + get borderRadius(): string { + return this.segmentedButtonLayoutBorderMap.get(this.segmentedButtonWidgetSettingsForm.get('appearance.layout').value); + } + + segmentedButtonAppearanceType: SegmentedButtonAppearanceType = 'first'; + segmentedButtonColorStylesType: SegmentedButtonColorStylesType = 'selected'; + + widgetButtonToggleStates = Object.keys(WidgetButtonToggleState) as WidgetButtonToggleState[]; + widgetButtonToggleStatesTranslationsMap = widgetButtonToggleStatesTranslations; + + segmentedButtonLayouts = segmentedButtonLayouts; + segmentedButtonLayoutTranslationMap = segmentedButtonLayoutTranslations; + segmentedButtonLayoutImageMap = segmentedButtonLayoutImages; + segmentedButtonLayoutBorderMap = segmentedButtonLayoutBorder; + + valueType = ValueType; + + segmentedButtonWidgetSettingsForm: UntypedFormGroup; + + constructor(protected store: Store, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.segmentedButtonWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return {...segmentedButtonDefaultSettings}; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.segmentedButtonWidgetSettingsForm = this.fb.group({ + initialState: [settings.initialState, []], + leftButtonClick: [settings.leftButtonClick, []], + rightButtonClick: [settings.rightButtonClick, []], + disabledState: [settings.disabledState, []], + + appearance: this.fb.group({ + layout: [settings.appearance.layout, []], + autoScale: [settings.appearance.autoScale, []], + cardBorder: [settings.appearance.cardBorder, []], + cardBorderColor: [settings.appearance.cardBorderColor, []], + leftAppearance: this.fb.group({ + showLabel: [settings.appearance.leftAppearance.showLabel, []], + label: [settings.appearance.leftAppearance.label, []], + labelFont: [settings.appearance.leftAppearance.labelFont, []], + showIcon: [settings.appearance.leftAppearance.showIcon, []], + icon: [settings.appearance.leftAppearance.icon, []], + iconSize: [settings.appearance.leftAppearance.iconSize, []], + iconSizeUnit: [settings.appearance.leftAppearance.iconSizeUnit, []], + }), + rightAppearance: this.fb.group({ + showLabel: [settings.appearance.rightAppearance.showLabel, []], + label: [settings.appearance.rightAppearance.label, []], + labelFont: [settings.appearance.rightAppearance.labelFont, []], + showIcon: [settings.appearance.rightAppearance.showIcon, []], + icon: [settings.appearance.rightAppearance.icon, []], + iconSize: [settings.appearance.rightAppearance.iconSize, []], + iconSizeUnit: [settings.appearance.rightAppearance.iconSizeUnit, []], + }), + selectedStyle: this.fb.group({ + mainColor: [settings.appearance.selectedStyle.mainColor, []], + backgroundColor: [settings.appearance.selectedStyle.backgroundColor, []], + customStyle: this.fb.group({ + enabled: [settings.appearance.selectedStyle.customStyle.enabled, []], + hovered: [settings.appearance.selectedStyle.customStyle.hovered, []], + disabled: [settings.appearance.selectedStyle.customStyle.disabled, []], + }) + }), + unselectedStyle: this.fb.group({ + mainColor: [settings.appearance.unselectedStyle.mainColor, []], + backgroundColor: [settings.appearance.unselectedStyle.backgroundColor, []], + customStyle: this.fb.group({ + enabled: [settings.appearance.unselectedStyle.customStyle.enabled, []], + hovered: [settings.appearance.unselectedStyle.customStyle.hovered, []], + disabled: [settings.appearance.unselectedStyle.customStyle.disabled, []], + }) + }), + }) + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html index 87a81db0eb..cb6cd5cbe0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html @@ -138,6 +138,7 @@ + + {{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }} + + + + {{ 'widgets.button.main' | translate }} + + + + + + + {{ 'widgets.button.background' | translate }} + + + + + + + {{ 'widgets.button.border' | translate }} + + + + + + + widgets.button.preview + + + + + + + + {{ 'widgets.button.copy-style-from' | translate }} + + + + + {{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }} + + + + + + {{ 'action.cancel' | translate }} + + + {{ 'action.apply' | translate }} + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.scss new file mode 100644 index 0000000000..dfbbb91620 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.scss @@ -0,0 +1,68 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../../scss/constants'; + +.tb-widget-button-custom-style-panel { + width: 530px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-lt-md} { + width: 90vw; + } + .tb-widget-button-custom-style-panel-content { + display: flex; + flex-direction: column; + gap: 16px; + overflow: auto; + } + .tb-widget-button-custom-style-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-widget-button-custom-style-preview { + flex: 1; + background: rgba(0, 0, 0, 0.04); + display: flex; + flex-direction: column; + padding: 12px 16px 24px 16px; + align-items: center; + gap: 12px; + .tb-widget-button-custom-style-preview-title { + align-self: stretch; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 24px; + color: rgba(0, 0, 0, 0.38); + } + tb-widget-button { + width: 200px; + height: 60px; + } + } + .tb-widget-button-custom-style-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.ts new file mode 100644 index 0000000000..98ced2dc2e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.ts @@ -0,0 +1,210 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnInit, + Output, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + defaultBackgroundColorDisabled, + defaultMainColorDisabled +} from '@shared/components/button/widget-button.models'; +import { merge } from 'rxjs'; +import { deepClone } from '@core/utils'; +import { WidgetButtonToggleComponent } from '@shared/components/button/widget-button-toggle.component'; +import { + ButtonToggleAppearance, + SegmentedButtonStyles, + WidgetButtonToggleCustomStyle, + WidgetButtonToggleState, + widgetButtonToggleStates, + widgetButtonToggleStatesTranslations +} from '@home/components/widget/lib/button/segmented-button-widget.models'; + +@Component({ + selector: 'tb-widget-button-toggle-custom-style-panel', + templateUrl: './widget-button-toggle-custom-style-panel.component.html', + providers: [], + styleUrls: ['./widget-button-toggle-custom-style-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class WidgetButtonToggleCustomStylePanelComponent extends PageComponent implements OnInit { + + @ViewChild('widgetButtonTogglePreview') + widgetButtonTogglePreview: WidgetButtonToggleComponent; + + @Input() + appearance: ButtonToggleAppearance; + + @Input() + value: boolean = false; + + @Input() + borderRadius: string; + + @Input() + autoScale: boolean; + + @Input() + state: WidgetButtonToggleState; + + @Input() + customStyle: WidgetButtonToggleCustomStyle; + + private popoverValue: TbPopoverComponent; + + @Input() + set popover(popover: TbPopoverComponent) { + this.popoverValue = popover; + popover.tbAnimationDone.subscribe(() => { + this.widgetButtonTogglePreview?.validateSize(); + }); + } + + get popover(): TbPopoverComponent { + return this.popoverValue; + } + + @Output() + customStyleApplied = new EventEmitter(); + + widgetButtonToggleStatesTranslationsMap = widgetButtonToggleStatesTranslations; + + WidgetButtonToggleState = WidgetButtonToggleState; + + previewAppearance: ButtonToggleAppearance; + + copyFromStates: WidgetButtonToggleState[]; + + customStyleFormGroup: UntypedFormGroup; + + style: SegmentedButtonStyles; + + constructor(private fb: UntypedFormBuilder, + protected store: Store, + private cd: ChangeDetectorRef) { + super(store); + } + + ngOnInit(): void { + this.style = this.value ? this.appearance.selectedStyle : this.appearance.unselectedStyle; + this.copyFromStates = widgetButtonToggleStates.filter(state => + state !== this.state && !!this.style.customStyle[state]); + this.customStyleFormGroup = this.fb.group( + { + overrideMainColor: [false, []], + mainColor: [null, []], + overrideBackgroundColor: [false, []], + backgroundColor: [null, []], + overrideBorderColor: [false, []], + borderColor: [null, []] + } + ); + merge(this.customStyleFormGroup.get('overrideMainColor').valueChanges, + this.customStyleFormGroup.get('overrideBackgroundColor').valueChanges, + this.customStyleFormGroup.get('overrideBorderColor').valueChanges) + .subscribe(() => { + this.updateValidators(); + }); + this.customStyleFormGroup.valueChanges.subscribe(() => { + this.updatePreviewAppearance(); + }); + this.setStyle(this.customStyle); + } + + copyStyle(state: WidgetButtonToggleState) { + this.customStyle = deepClone(this.style[state]); + this.setStyle(this.customStyle); + this.customStyleFormGroup.markAsDirty(); + } + + cancel() { + this.popover?.hide(); + } + + applyCustomStyle() { + const customStyle: SegmentedButtonStyles = this.customStyleFormGroup.value; + this.customStyleApplied.emit(customStyle); + } + + private setStyle(customStyle?: WidgetButtonToggleCustomStyle): void { + let mainColor = this.state === WidgetButtonToggleState.disabled ? defaultMainColorDisabled : this.style.mainColor; + if (customStyle?.overrideMainColor) { + mainColor = customStyle?.mainColor; + } + let backgroundColor = this.state === WidgetButtonToggleState.disabled ? defaultBackgroundColorDisabled : this.style.backgroundColor; + if (customStyle?.overrideBackgroundColor) { + backgroundColor = customStyle?.backgroundColor; + } + let borderColor = this.state === WidgetButtonToggleState.disabled ? defaultBackgroundColorDisabled : this.style.backgroundColor; + if (customStyle?.overrideBorderColor) { + borderColor = customStyle?.borderColor; + } + this.customStyleFormGroup.patchValue({ + overrideMainColor: customStyle?.overrideMainColor, + mainColor, + overrideBackgroundColor: customStyle?.overrideBackgroundColor, + backgroundColor, + overrideBorderColor: customStyle?.overrideBorderColor, + borderColor + }, {emitEvent: false}); + this.updateValidators(); + this.updatePreviewAppearance(); + } + + private updateValidators() { + const overrideMainColor: boolean = this.customStyleFormGroup.get('overrideMainColor').value; + const overrideBackgroundColor: boolean = this.customStyleFormGroup.get('overrideBackgroundColor').value; + const overrideBorderColor: boolean = this.customStyleFormGroup.get('overrideBorderColor').value; + + if (overrideMainColor) { + this.customStyleFormGroup.get('mainColor').enable({emitEvent: false}); + } else { + this.customStyleFormGroup.get('mainColor').disable({emitEvent: false}); + } + if (overrideBackgroundColor) { + this.customStyleFormGroup.get('backgroundColor').enable({emitEvent: false}); + } else { + this.customStyleFormGroup.get('backgroundColor').disable({emitEvent: false}); + } + if (overrideBorderColor) { + this.customStyleFormGroup.get('borderColor').enable({emitEvent: false}); + } else { + this.customStyleFormGroup.get('borderColor').disable({emitEvent: false}); + } + } + + private updatePreviewAppearance() { + this.previewAppearance = deepClone(this.appearance); + if (this.value) { + this.previewAppearance.selectedStyle.customStyle[this.state] = this.customStyleFormGroup.value; + } else { + this.previewAppearance.unselectedStyle.customStyle[this.state] = this.customStyleFormGroup.value; + } + this.cd.markForCheck(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.html new file mode 100644 index 0000000000..624d17af16 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.html @@ -0,0 +1,44 @@ + + + + + + + mdi:broom + + + + edit + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.scss new file mode 100644 index 0000000000..6badf1f804 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.scss @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../../scss/constants'; + +.tb-widget-button-custom-style { + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; + button.mat-mdc-icon-button { + color: rgba(0,0,0,0.56); + } + .tb-widget-button-preview-panel { + width: 148px; + height: 48px; + padding: 8px 12px; + border-radius: 4px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + tb-widget-button-toggle { + width: 84px; + height: 100%; + } + @media #{$mat-gt-xs} { + width: 188px; + tb-widget-button-toggle { + width: 124px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.ts new file mode 100644 index 0000000000..366c78a0c6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.ts @@ -0,0 +1,169 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + forwardRef, + Input, + OnChanges, + OnInit, + Renderer2, + SimpleChanges, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { MatIconButton } from '@angular/material/button'; +import { deepClone } from '@core/utils'; +import { + ButtonToggleAppearance, + WidgetButtonToggleCustomStyle, + WidgetButtonToggleState +} from '@home/components/widget/lib/button/segmented-button-widget.models'; +import { + WidgetButtonToggleCustomStylePanelComponent +} from '@home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component'; + +@Component({ + selector: 'tb-widget-button-toggle-custom-style', + templateUrl: './widget-button-toggle-custom-style.component.html', + styleUrls: ['./widget-button-toggle-custom-style.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetButtonToggleCustomStyleComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class WidgetButtonToggleCustomStyleComponent implements OnInit, OnChanges, ControlValueAccessor { + + @Input() + disabled = false; + + @Input() + value = false; + + @Input() + appearance: ButtonToggleAppearance; + + @Input() + borderRadius: string; + + @Input() + autoScale: boolean; + + @Input() + state: WidgetButtonToggleState; + + widgetButtonToggleState = WidgetButtonToggleState; + + modelValue: WidgetButtonToggleCustomStyle; + + previewAppearance: ButtonToggleAppearance; + + private propagateChange = (_val: any) => {}; + + constructor(private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef) {} + + ngOnInit(): void { + this.updatePreviewAppearance(); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange) { + if (propName === 'appearance') { + this.updatePreviewAppearance(); + } + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(_isDisabled: boolean): void { + } + + writeValue(value: WidgetButtonToggleCustomStyle): void { + this.modelValue = value; + this.updatePreviewAppearance(); + } + + clearStyle() { + this.updateModel(null); + } + + openButtonCustomStylePopup($event: Event, matButton: MatIconButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + appearance: this.appearance, + borderRadius: this.borderRadius, + autoScale: this.autoScale, + state: this.state, + value: this.value, + customStyle: this.modelValue + }; + const widgetButtonCustomStylePanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, WidgetButtonToggleCustomStylePanelComponent, + ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null, + ctx, + {}, + {}, {}, true); + widgetButtonCustomStylePanelPopover.tbComponentRef.instance.popover = widgetButtonCustomStylePanelPopover; + widgetButtonCustomStylePanelPopover.tbComponentRef.instance.customStyleApplied.subscribe((customStyle) => { + widgetButtonCustomStylePanelPopover.hide(); + this.updateModel(customStyle); + }); + } + } + + private updateModel(value: WidgetButtonToggleCustomStyle): void { + this.modelValue = value; + this.updatePreviewAppearance(); + this.propagateChange(this.modelValue); + } + + private updatePreviewAppearance() { + this.previewAppearance = deepClone(this.appearance); + if (this.modelValue) { + if (this.value) { + this.previewAppearance.selectedStyle.customStyle[this.state] = this.modelValue; + } else { + this.previewAppearance.unselectedStyle.customStyle[this.state] = this.modelValue; + } + } + this.cd.markForCheck(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index fbfffc973f..037096f406 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -158,6 +158,12 @@ import { import { ScadaSymbolObjectSettingsComponent } from '@home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component'; +import { + WidgetButtonToggleCustomStyleComponent +} from '@home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component'; +import { + WidgetButtonToggleCustomStylePanelComponent +} from '@home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component'; @NgModule({ declarations: [ @@ -196,7 +202,9 @@ import { WidgetActionSettingsPanelComponent, WidgetButtonAppearanceComponent, WidgetButtonCustomStyleComponent, + WidgetButtonToggleCustomStyleComponent, WidgetButtonCustomStylePanelComponent, + WidgetButtonToggleCustomStylePanelComponent, TimeSeriesChartAxisSettingsComponent, TimeSeriesChartThresholdsPanelComponent, TimeSeriesChartThresholdRowComponent, @@ -261,7 +269,9 @@ import { WidgetActionSettingsPanelComponent, WidgetButtonAppearanceComponent, WidgetButtonCustomStyleComponent, + WidgetButtonToggleCustomStyleComponent, WidgetButtonCustomStylePanelComponent, + WidgetButtonToggleCustomStylePanelComponent, TimeSeriesChartAxisSettingsComponent, TimeSeriesChartThresholdsPanelComponent, TimeSeriesChartThresholdRowComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.html index d719c84e9c..57ce448999 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.html @@ -62,6 +62,7 @@ { - this.historicalData = formattedDataArrayFromDatasourceData(this.ctx.data).map( - item => this.clearIncorrectFirsLastDatapoint(item)).filter(arr => arr.length); - this.interpolatedTimeData.length = 0; - this.formattedInterpolatedTimeData.length = 0; - const prevMinTime = this.minTime; - const prevMaxTime = this.maxTime; - this.calculateIntervals(); - const currentTime = this.calculateCurrentTime(prevMinTime, prevMaxTime); - if (currentTime !== this.currentTime) { - this.timeUpdated(currentTime); - } - this.mapWidget.map.map?.invalidateSize(); - this.mapWidget.map.setLoading(false); - this.cd.detectChanges(); + this.update(); }; subscription.callbacks.onLatestDataUpdated = () => { - this.formattedLatestData = formattedDataFormDatasourceData(this.ctx.latestData); - this.updateCurrentData(); + this.latestDataUpdate(); }; + from(this.initializeFunctions()).subscribe(() => { + this.initialized = true; + if (this.updatePending) { + this.updateCurrentData(); + } + }); + } ngAfterViewInit() { import('@home/components/widget/lib/maps/map-widget2').then( (mod) => { - this.mapWidget = new mod.MapWidgetController(MapProviders.openstreet, false, this.ctx, this.mapContainer.nativeElement); + this.mapWidget = new mod.MapWidgetController(MapProviders.openstreet, false, this.ctx, this.mapContainer.nativeElement, false, + () => { + if (this.mapWidgetUpdatePending) { + this.updateMapWidget(); + } + } + ); this.mapResize$ = new ResizeObserver(() => { this.mapWidget.resize(); }); @@ -177,22 +177,65 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy this.updateCurrentData(); } - private updateCurrentData() { - let currentPosition = this.formattedCurrentPosition; - if (this.formattedLatestData.length) { - currentPosition = mergeFormattedData(this.formattedCurrentPosition, this.formattedLatestData); + private async initializeFunctions(): Promise { + this.settings.parsedPointAsAnchorFunction = await firstValueFrom(parseTbFunction(this.ctx.http, this.settings.pointAsAnchorFunction, ['data', 'dsData', 'dsIndex'])); + this.settings.parsedTooltipFunction = await firstValueFrom(parseTbFunction(this.ctx.http, this.settings.tooltipFunction, ['data', 'dsData', 'dsIndex'])); + this.settings.parsedLabelFunction = await firstValueFrom(parseTbFunction(this.ctx.http, this.settings.labelFunction, ['data', 'dsData', 'dsIndex'])); + this.settings.parsedColorPointFunction = await firstValueFrom(parseTbFunction(this.ctx.http, this.settings.colorPointFunction, ['data', 'dsData', 'dsIndex'])); + } + + private update() { + this.historicalData = formattedDataArrayFromDatasourceData(this.ctx.data).map( + item => this.clearIncorrectFirsLastDatapoint(item)).filter(arr => arr.length); + this.interpolatedTimeData.length = 0; + this.formattedInterpolatedTimeData.length = 0; + const prevMinTime = this.minTime; + const prevMaxTime = this.maxTime; + this.calculateIntervals(); + const currentTime = this.calculateCurrentTime(prevMinTime, prevMaxTime); + if (currentTime !== this.currentTime) { + this.timeUpdated(currentTime); } - this.calcLabel(currentPosition); - this.calcMainTooltip(currentPosition); - if (this.mapWidget && this.mapWidget.map && this.mapWidget.map.map) { - this.mapWidget.map.updateFromData(true, currentPosition, this.formattedInterpolatedTimeData, (trip) => { - this.activeTrip = trip; - this.timeUpdated(this.currentTime); - this.cd.markForCheck(); - }); - if (this.settings.showPoints) { - this.mapWidget.map.updatePoints(this.formattedInterpolatedTimeData, this.calcTooltip); + this.updateMapWidget(); + } + + private latestDataUpdate() { + this.formattedLatestData = formattedDataFormDatasourceData(this.ctx.latestData); + this.updateCurrentData(); + } + + private updateMapWidget() { + if (this.mapWidget?.map) { + this.mapWidgetUpdatePending = false; + this.mapWidget.map.map?.invalidateSize(); + this.mapWidget.map.setLoading(false); + this.cd.detectChanges(); + } else { + this.mapWidgetUpdatePending = true; + } + } + + private updateCurrentData() { + if (this.initialized) { + this.updatePending = false; + let currentPosition = this.formattedCurrentPosition; + if (this.formattedLatestData.length) { + currentPosition = mergeFormattedData(this.formattedCurrentPosition, this.formattedLatestData); } + this.calcLabel(currentPosition); + this.calcMainTooltip(currentPosition); + if (this.mapWidget?.map?.map) { + this.mapWidget.map.updateFromData(true, currentPosition, this.formattedInterpolatedTimeData, (trip) => { + this.activeTrip = trip; + this.timeUpdated(this.currentTime); + this.cd.markForCheck(); + }); + if (this.settings.showPoints) { + this.mapWidget.map.updatePoints(this.formattedInterpolatedTimeData, this.calcTooltip); + } + } + } else { + this.updatePending = true; } } @@ -235,7 +278,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy if (this.useAnchors) { const anchorDate = Object.entries(_.union(this.interpolatedTimeData)[0]); this.anchors = anchorDate - .filter((data: [string, FormattedData], tsIndex) => safeExecute(this.settings.parsedPointAsAnchorFunction, [data[1], + .filter((data: [string, FormattedData], tsIndex) => safeExecuteTbFunction(this.settings.parsedPointAsAnchorFunction, [data[1], this.formattedInterpolatedTimeData.map(ds => ds[tsIndex]), data[1].dsIndex])) .map(data => parseInt(data[0], 10)); } @@ -244,7 +287,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy calcTooltip = (point: FormattedData, points: FormattedData[]): string => { const data = point ? point : this.activeTrip; const tooltipPattern: string = this.settings.useTooltipFunction ? - safeExecute(this.settings.parsedTooltipFunction, + safeExecuteTbFunction(this.settings.parsedTooltipFunction, [data, points, point.dsIndex]) : this.settings.tooltipPattern; return parseWithTranslation.parseTemplate(tooltipPattern, data, true); } @@ -261,7 +304,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy if (this.activeTrip) { const data = points[this.activeTrip.dsIndex]; const labelText: string = this.settings.useLabelFunction ? - safeExecute(this.settings.parsedLabelFunction, [data, points, data.dsIndex]) : this.settings.label; + safeExecuteTbFunction(this.settings.parsedLabelFunction, [data, points, data.dsIndex]) : this.settings.label; this.label = this.sanitizer.bypassSecurityTrustHtml(parseWithTranslation.parseTemplate(labelText, data, true)); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 349d891669..21f2d89dc6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -87,6 +87,7 @@ import { } from '@home/components/widget/lib/cards/notification-type-filter-panel.component'; import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive'; import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component'; +import { TwoSegmentButtonWidgetComponent } from '@home/components/widget/lib/button/two-segment-button-widget.component'; @NgModule({ declarations: [ @@ -124,6 +125,7 @@ import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/sc BarChartWithLabelsWidgetComponent, SingleSwitchWidgetComponent, ActionButtonWidgetComponent, + TwoSegmentButtonWidgetComponent, CommandButtonWidgetComponent, PowerButtonWidgetComponent, SliderWidgetComponent, @@ -185,6 +187,7 @@ import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/sc BarChartWithLabelsWidgetComponent, SingleSwitchWidgetComponent, ActionButtonWidgetComponent, + TwoSegmentButtonWidgetComponent, CommandButtonWidgetComponent, PowerButtonWidgetComponent, SliderWidgetComponent, diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index 704f909809..784aade083 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -27,6 +27,7 @@ import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component'; import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component'; +import { ResourceTabsComponent } from '@home/pages/admin/resource/resource-tabs.component'; import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component'; import { QueueComponent } from '@home/pages/admin/queue/queue.component'; import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component'; @@ -47,6 +48,7 @@ import { NgxFlowModule } from '@flowjs/ngx-flow'; SecuritySettingsComponent, HomeSettingsComponent, ResourcesLibraryComponent, + ResourceTabsComponent, ResourcesTableHeaderComponent, JsResourceComponent, JsLibraryTableHeaderComponent, diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts index b0fadd53d9..862adf48d5 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts @@ -43,6 +43,7 @@ import { EntityAction } from '@home/models/entity/entity-component.models'; import { JsLibraryTableHeaderComponent } from '@home/pages/admin/resource/js-library-table-header.component'; import { JsResourceComponent } from '@home/pages/admin/resource/js-resource.component'; import { switchMap } from 'rxjs/operators'; +import { ResourceTabsComponent } from '@home/pages/admin/resource/resource-tabs.component'; @Injectable() export class JsLibraryTableConfigResolver { @@ -57,6 +58,7 @@ export class JsLibraryTableConfigResolver { this.config.entityType = EntityType.TB_RESOURCE; this.config.entityComponent = JsResourceComponent; + this.config.entityTabsComponent = ResourceTabsComponent; this.config.entityTranslations = { details: 'javascript.javascript-resource-details', add: 'javascript.add', diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resource-tabs.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-tabs.component.html new file mode 100644 index 0000000000..fb608683d8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-tabs.component.html @@ -0,0 +1,27 @@ + + + + + + + diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resource-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-tabs.component.ts new file mode 100644 index 0000000000..2cd2f43d62 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-tabs.component.ts @@ -0,0 +1,36 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component'; +import { Resource } from '@shared/models/resource.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; + +@Component({ + selector: 'tb-resource-tabs', + templateUrl: './resource-tabs.component.html', + styleUrls: [] +}) +export class ResourceTabsComponent extends EntityTabsComponent { + + readonly NULL_UUID = NULL_UUID; + + constructor(protected store: Store) { + super(store); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts index eb71f4ff9f..ac73b10550 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts @@ -57,6 +57,7 @@ import { } from '@home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component'; import { IAliasController } from '@core/api/widget-api.models'; import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models'; +import { isNotEmptyTbFunction } from '@shared/models/js-function.models'; export const behaviorValid = (behavior: ScadaSymbolBehavior): boolean => { if (!behavior.id || !behavior.name || !behavior.type) { @@ -77,7 +78,7 @@ export const behaviorValid = (behavior: ScadaSymbolBehavior): boolean => { return false; } if (behavior.defaultSetValueSettings.valueToData?.type === ValueToDataType.FUNCTION - && isUndefinedOrNull(behavior.defaultSetValueSettings.valueToData?.valueToDataFunction)) { + && !isNotEmptyTbFunction(behavior.defaultSetValueSettings.valueToData?.valueToDataFunction)) { return false; } break; diff --git a/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.html b/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.html new file mode 100644 index 0000000000..1a9bd98f13 --- /dev/null +++ b/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.html @@ -0,0 +1,42 @@ + + + + + + {{ appearance.leftAppearance.icon }} + {{ leftLabel$ | async }} + + + + + {{ appearance.rightAppearance.icon }} + {{ rightLabel$ | async }} + + + + diff --git a/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.scss b/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.scss new file mode 100644 index 0000000000..4ce8c69ddb --- /dev/null +++ b/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.scss @@ -0,0 +1,189 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +$defaultBorderRadius: 4px; + +$defaultCheckedMainColor: #FFFFFF; +$defaultCheckedBackgroundColor: #305680; +$defaultCheckedBorderColor: #305680; + +$defaultUncheckedMainColor: rgba(0, 0, 0, 0.76); +$defaultUncheckedBackgroundColor: #E8E8E8; +$defaultUncheckedBorderColor: #E8E8E8; + +$defaultCheckedMainColorDisabled: #FFFFFFCC; +$defaultCheckedBackgroundColorDisabled: rgba(0, 0, 0, 0.12); +$defaultCheckedBorderColorDisabled: rgba(0, 0, 0, 0.12); + +$defaultUncheckedMainColorDisabled: rgba(0, 0, 0, 0.12); +$defaultUncheckedBackgroundColorDisabled: #E8E8E8; +$defaultUncheckedBorderColorDisabled: #E8E8E8; + +$buttonBorderRadius: var(--tb-widget-button-toggle-border-radius, $defaultBorderRadius); + +$mainCheckedColorEnabled: var(--tb-widget-button-toggle-main-checked-color-enabled, $defaultCheckedMainColor); +$backgroundCheckedColorEnabled: var(--tb-widget-button-toggle-background-checked-color-enabled, $defaultCheckedBackgroundColor); +$borderCheckedColorEnabled: var(--tb-widget-button-toggle-border-checked-color-enabled, $defaultCheckedBorderColor); + +$mainUncheckedColorEnabled: var(--tb-widget-button-toggle-main-unchecked-color-enabled, $defaultUncheckedMainColor); +$backgroundUncheckedColorEnabled: var(--tb-widget-button-toggle-background-unchecked-color-enabled, $defaultUncheckedBackgroundColor); +$borderUncheckedColorEnabled: var(--tb-widget-button-toggle-border-unchecked-color-enabled, $defaultUncheckedBorderColor); + +$mainCheckedColorDisabled: var(--tb-widget-button-toggle-main-checked-color-disabled, $defaultCheckedMainColorDisabled); +$backgroundCheckedColorDisabled: var(--tb-widget-button-toggle-background-checked-color-disabled, $defaultCheckedBackgroundColorDisabled); +$borderCheckedColorDisabled: var(--tb-widget-button-toggle-border-checked-color-disabled, $defaultCheckedBorderColorDisabled); + +$mainUncheckedColorDisabled: var(--tb-widget-button-toggle-main-unchecked-color-disabled, $defaultUncheckedMainColorDisabled); +$backgroundUncheckedColorDisabled: var(--tb-widget-button-toggle-background-unchecked-color-disabled, $defaultUncheckedBackgroundColorDisabled); +$borderUncheckedColorDisabled: var(--tb-widget-button-toggle-border-unchecked-color-disabled, $defaultUncheckedBorderColorDisabled); + +$mainCheckedColorHovered: var(--tb-widget-button-toggle-main-checked-color-hovered, $defaultCheckedMainColor); +$backgroundCheckedColorHovered: var(--tb-widget-button-toggle-background-checked-color-hovered, $defaultCheckedBackgroundColor); +$borderCheckedColorHovered: var(--tb-widget-button-toggle-border-checked-color-hovered, $defaultCheckedBorderColor); + +$mainUncheckedColorHovered: var(--tb-widget-button-toggle-main-unchecked-color-hovered, $defaultCheckedMainColor); +$backgroundUncheckedColorHovered: var(--tb-widget-button-toggle-background-unchecked-color-hovered, $defaultCheckedBackgroundColor); +$borderUncheckedColorHovered: var(--tb-widget-button-toggle-border-unchecked-color-hovered, $defaultCheckedBorderColor); + +@mixin _tb-widget-button-styles($main, $background, $boxShadow) { + color: $main; + background-color: $background; + border: 1px solid $boxShadow; +} + +:host { + max-width: 100%; + .tb-toggle-container { + display: block; + height: 100%; + } + .tb-toggle-header { + transition: transform 500ms cubic-bezier(0.35, 0, 0.25, 1); + } + + .tb-widget-button-content { + width: 100%; + display: flex; + flex-direction: row; + gap: 4px; + justify-content: center; + align-items: center; + .mat-icon { + margin: 0; + } + span.tb-widget-button-label { + line-height: normal; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } +} + +:host ::ng-deep { + .mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header { + overflow: visible; + width: 100%; + height: 100%; + padding: 3px; + border: 1px solid; + background: transparent; + .mat-button-toggle + .mat-button-toggle { + border-left: none; + } + .mat-button-toggle.mat-button-toggle-appearance-standard { + flex: 1; + width: 50%; + border-radius: $buttonBorderRadius; + + &.tb-hover-state { + .mat-button-toggle-button { + @include _tb-widget-button-styles($mainUncheckedColorHovered, $backgroundUncheckedColorHovered, $borderUncheckedColorHovered); + } + &.mat-button-toggle-checked { + .mat-button-toggle-button { + @include _tb-widget-button-styles($mainCheckedColorHovered, $backgroundCheckedColorHovered, $borderCheckedColorHovered); + } + } + } + + .mat-button-toggle-button { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + border-radius: $buttonBorderRadius; + @include _tb-widget-button-styles($mainUncheckedColorEnabled, $backgroundUncheckedColorEnabled, $borderUncheckedColorEnabled); + .mat-button-toggle-label-content { + .mat-pseudo-checkbox { + display: none; + } + } + } + &.mat-button-toggle-checked { + .mat-button-toggle-button { + @include _tb-widget-button-styles($mainCheckedColorEnabled, $backgroundCheckedColorEnabled, $borderCheckedColorEnabled); + + } + } + + } + &.tb-pointer-events { + .mat-button-toggle.mat-button-toggle-appearance-standard { + .mat-button-toggle-button { + pointer-events: none; + } + .mat-button-toggle-focus-overlay, .mat-button-toggle-ripple { + opacity: 0; + } + } + } + &:not(:disabled):not(.tb-disabled-state):not(.tb-pointer-events) { + .mat-button-toggle.mat-button-toggle-appearance-standard { + &:hover { + .mat-button-toggle-button { + @include _tb-widget-button-styles($mainUncheckedColorHovered, $backgroundUncheckedColorHovered, $borderUncheckedColorHovered); + } + &.mat-button-toggle-checked { + .mat-button-toggle-button { + @include _tb-widget-button-styles($mainCheckedColorHovered, $backgroundCheckedColorHovered, $borderCheckedColorHovered); + } + } + } + } + } + &:disabled, &.tb-disabled-state { + pointer-events: none; + .mat-button-toggle.mat-button-toggle-appearance-standard { + .mat-button-toggle-button { + @include _tb-widget-button-styles($mainUncheckedColorDisabled, $backgroundUncheckedColorDisabled, $borderUncheckedColorDisabled); + } + + &.mat-button-toggle-checked { + .mat-button-toggle-button { + @include _tb-widget-button-styles($mainCheckedColorDisabled, $backgroundCheckedColorDisabled, $borderCheckedColorDisabled); + } + } + } + .mat-button-toggle-focus-overlay, .mat-button-toggle-ripple { + opacity: 0; + } + } + .mat-button-toggle-focus-overlay, .mat-button-toggle-ripple { + border-radius: inherit; + } + } +} diff --git a/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.ts b/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.ts new file mode 100644 index 0000000000..4f281b2aa0 --- /dev/null +++ b/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.ts @@ -0,0 +1,234 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + Renderer2, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { ComponentStyle, iconStyle, textStyle, validateCssSize } from '@shared/models/widget-settings.models'; +import { UtilsService } from '@core/services/utils.service'; +import { Observable, of } from 'rxjs'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { isDefinedAndNotNull } from '@core/utils'; +import { + generateWidgetButtonToggleAppearanceCss, + generateWidgetButtonToggleBorderLayout, + segmentedButtonDefaultAppearance, + segmentedButtonLayoutBorder +} from '@home/components/widget/lib/button/segmented-button-widget.models'; +import { MatButtonToggleChange } from '@angular/material/button-toggle'; + +const initialButtonHeight = 60; +const horizontalLayoutPadding = 10; + +@Component({ + selector: 'tb-widget-button-toggle', + templateUrl: './widget-button-toggle.component.html', + styleUrls: ['./widget-button-toggle.component.scss'] +}) +export class WidgetButtonToggleComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges { + + @ViewChild('toggleGroupContainer', {static: false}) + toggleGroupContainer: ElementRef; + + @ViewChild('widgetButton', {read: ElementRef}) + widgetButton: ElementRef; + + @ViewChild('leftButtonContent', {static: false}) + leftButtonContent: ElementRef; + @ViewChild('rightButtonContent', {static: false}) + rightButtonContent: ElementRef; + + @Input() + appearance = segmentedButtonDefaultAppearance; + + @Input() + borderRadius: string; + + @Input() + autoScale: boolean; + + @Input() + @coerceBoolean() + value = false; + + @Input() + @coerceBoolean() + disabled = false; + + @Input() + @coerceBoolean() + hovered = false; + + @Input() + @coerceBoolean() + disableEvents = false; + + @Input() + ctx: WidgetContext; + + @Output() + clicked = new EventEmitter(); + + leftLabel$: Observable; + rightLabel$: Observable; + + leftIconStyle: ComponentStyle = {}; + rightIconStyle: ComponentStyle = {}; + + leftLabelStyle: ComponentStyle = {}; + rightLabelStyle: ComponentStyle = {}; + + computedBorderColor: string; + computedBorderWidth: string; + computedBorderRadius: string; + + private buttonResize$: ResizeObserver; + + private appearanceCssClass: string; + + constructor(private renderer: Renderer2, + private elementRef: ElementRef, + private utils: UtilsService) {} + + ngOnInit(): void { + this.updateAppearance(); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange) { + if (propName === 'appearance') { + this.updateAppearance(); + } else if (propName === 'borderRadius') { + this.updateBorderRadius(); + } else if (propName === 'autoScale') { + this.updateAutoScale(); + } + } + } + } + + ngAfterViewInit(): void { + this.updateAutoScale(); + } + + ngOnDestroy(): void { + if (this.buttonResize$) { + this.buttonResize$.disconnect(); + } + this.clearAppearanceCss(); + } + + public validateSize() { + if (this.appearance.autoScale && this.widgetButton.nativeElement) { + this.onResize(); + } + } + + private updateAppearance(): void { + this.clearAppearanceCss(); + this.computedBorderColor = this.appearance.cardBorderColor; + this.computedBorderWidth = validateCssSize(this.appearance.cardBorder.toString()); + if (this.appearance.leftAppearance.showIcon) { + this.leftIconStyle = iconStyle(this.appearance.leftAppearance.iconSize, this.appearance.leftAppearance.iconSizeUnit); + } + if (this.appearance.rightAppearance.showIcon) { + this.rightIconStyle = iconStyle(this.appearance.rightAppearance.iconSize, this.appearance.rightAppearance.iconSizeUnit); + } + if (this.appearance.leftAppearance.showLabel) { + this.leftLabelStyle = textStyle(this.appearance.leftAppearance.labelFont); + this.leftLabel$ = this.ctx ? this.ctx.registerLabelPattern(this.appearance.leftAppearance.label, this.leftLabel$) : of(this.appearance.leftAppearance.label); + } + if (this.appearance.rightAppearance.showLabel) { + this.rightLabelStyle = textStyle(this.appearance.rightAppearance.labelFont); + this.rightLabel$ = this.ctx ? this.ctx.registerLabelPattern(this.appearance.rightAppearance.label, this.rightLabel$) : of(this.appearance.rightAppearance.label); + } + this.updateBorderRadius(); + const appearanceCss = generateWidgetButtonToggleAppearanceCss(this.appearance.selectedStyle, this.appearance.unselectedStyle); + const layoutCss = generateWidgetButtonToggleBorderLayout(this.appearance.layout); + this.appearanceCssClass = this.utils.applyCssToElement(this.renderer, this.elementRef.nativeElement, + 'tb-widget-button', appearanceCss + layoutCss); + this.updateAutoScale(); + } + + private updateBorderRadius(): void { + if (this.borderRadius?.length) { + const validatedBorderRadius = validateCssSize(this.borderRadius); + if (validatedBorderRadius) { + this.computedBorderRadius = validatedBorderRadius; + } else { + this.computedBorderRadius = this.borderRadius; + } + } else { + this.computedBorderRadius = segmentedButtonLayoutBorder.get(this.appearance.layout); + } + + } + + private clearAppearanceCss(): void { + if (this.appearanceCssClass) { + this.utils.clearCssElement(this.renderer, this.appearanceCssClass, this.elementRef?.nativeElement); + this.appearanceCssClass = null; + } + } + + private updateAutoScale() { + if (this.buttonResize$) { + this.buttonResize$.disconnect(); + } + if (this.widgetButton && this.rightButtonContent && this.leftButtonContent) { + const autoScale = isDefinedAndNotNull(this.autoScale) ? this.autoScale : this.appearance.autoScale; + if (autoScale) { + this.buttonResize$ = new ResizeObserver(() => { + this.onResize(); + }); + this.buttonResize$.observe(this.widgetButton.nativeElement); + this.onResize(); + } else { + this.renderer.setStyle(this.widgetButton.nativeElement, 'transform', 'none'); + this.renderer.setStyle(this.widgetButton.nativeElement, 'width', '100%'); + } + } + } + + private onResize() { + const height = this.widgetButton.nativeElement.getBoundingClientRect().height; + const buttonScale = height / initialButtonHeight; + const buttonWidth = this.widgetButton.nativeElement.getBoundingClientRect().width; + const buttonHeight = this.widgetButton.nativeElement.getBoundingClientRect().height; + this.renderer.setStyle(this.leftButtonContent.nativeElement, 'transform', `scale(1)`); + this.renderer.setStyle(this.rightButtonContent.nativeElement, 'transform', `scale(1)`); + const contentWidth = this.leftButtonContent.nativeElement.getBoundingClientRect().width; + const contentHeight = this.leftButtonContent.nativeElement.getBoundingClientRect().height; + const maxScale = Math.max(1, buttonScale); + const scale = Math.min(Math.min((buttonWidth / 2 - horizontalLayoutPadding) / contentWidth, buttonHeight / contentHeight), maxScale); + this.renderer.setStyle(this.leftButtonContent.nativeElement, 'transform', `scale(${scale})`); + this.renderer.setStyle(this.rightButtonContent.nativeElement, 'transform', `scale(${scale})`); + } +} diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-schema-form.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-schema-form.tsx index 5ce35ae96f..bbdd90cbe7 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-schema-form.tsx +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-schema-form.tsx @@ -131,10 +131,10 @@ class ThingsboardSchemaForm extends React.Component { } if (form.condition) { this.hasConditions = true; - if (!this.conditionFunction) { - this.conditionFunction = new Function('form', 'model', 'index', `return ${form.condition};`); + if (!form.conditionFunction) { + form.conditionFunction = new Function('form', 'model', 'index', `return ${form.condition};`); } - if (this.conditionFunction(form, model, index) === false) { + if (form.conditionFunction(form, model, index) === false) { return null; } } diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form.models.ts b/ui-ngx/src/app/shared/components/json-form/react/json-form.models.ts index edba015521..ffcf201136 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form.models.ts +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form.models.ts @@ -87,6 +87,7 @@ export interface JsonFormData { required: boolean; default?: any; condition?: string; + conditionFunction?: Function; style?: any; rows?: number; rowsMax?: number; diff --git a/ui-ngx/src/app/shared/models/action-widget-settings.models.ts b/ui-ngx/src/app/shared/models/action-widget-settings.models.ts index fc03ef2eb9..b1c47a1fd3 100644 --- a/ui-ngx/src/app/shared/models/action-widget-settings.models.ts +++ b/ui-ngx/src/app/shared/models/action-widget-settings.models.ts @@ -17,6 +17,7 @@ import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { widgetType } from '@shared/models/widget.models'; import { AlarmSeverity } from '@shared/models/alarm.models'; +import { TbFunction } from '@shared/models/js-function.models'; export enum GetValueAction { DO_NOTHING = 'DO_NOTHING', @@ -79,7 +80,7 @@ export enum DataToValueType { export interface DataToValueSettings { type: DataToValueType; - dataToValueFunction: string; + dataToValueFunction: TbFunction; compareToValue?: any; } @@ -131,7 +132,7 @@ export enum ValueToDataType { export interface ValueToDataSettings { type: ValueToDataType; constantValue: any; - valueToDataFunction: string; + valueToDataFunction: TbFunction; } export interface SetValueSettings extends ValueActionSettings { diff --git a/ui-ngx/src/app/shared/models/vc.models.ts b/ui-ngx/src/app/shared/models/vc.models.ts index ba54a297a7..15d41656df 100644 --- a/ui-ngx/src/app/shared/models/vc.models.ts +++ b/ui-ngx/src/app/shared/models/vc.models.ts @@ -15,7 +15,7 @@ /// import { EntityId } from '@shared/models/id/entity-id'; -import { EntityType } from '@shared/models/entity-type.models'; +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; import { ExportableEntity } from '@shared/models/base-data'; import { EntityRelation } from '@shared/models/relation.models'; import { Device, DeviceCredentials } from '@shared/models/device.models'; @@ -38,7 +38,7 @@ export const exportableEntityTypes: Array = [ EntityType.NOTIFICATION_RULE ]; -export const entityTypesWithoutRelatedData: Set = new Set([ +export const entityTypesWithoutRelatedData: Set = new Set([ EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, @@ -104,8 +104,8 @@ export function createDefaultEntityTypesVersionCreate(): {[entityType: string]: for (const entityType of exportableEntityTypes) { res[entityType] = { syncStrategy: null, - saveAttributes: true, - saveRelations: true, + saveAttributes: !entityTypesWithoutRelatedData.has(entityType), + saveRelations: !entityTypesWithoutRelatedData.has(entityType), saveCredentials: true, allEntities: true, entityIds: [] @@ -151,8 +151,8 @@ export function createDefaultEntityTypesVersionLoad(): {[entityType: string]: En const res: {[entityType: string]: EntityTypeVersionLoadConfig} = {}; for (const entityType of exportableEntityTypes) { res[entityType] = { - loadAttributes: true, - loadRelations: true, + loadAttributes: !entityTypesWithoutRelatedData.has(entityType), + loadRelations: !entityTypesWithoutRelatedData.has(entityType), loadCredentials: true, removeOtherEntities: false, findExistingEntityByName: true diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 560c0151c0..c1f243ae14 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -154,6 +154,7 @@ import { OtaPackageAutocompleteComponent } from '@shared/components/ota-package/ import { MAT_DATE_LOCALE } from '@angular/material/core'; import { CopyButtonComponent } from '@shared/components/button/copy-button.component'; import { TogglePasswordComponent } from '@shared/components/button/toggle-password.component'; +import { WidgetButtonToggleComponent } from '@shared/components/button/widget-button-toggle.component'; import { HelpPopupComponent } from '@shared/components/help-popup.component'; import { TbPopoverComponent, TbPopoverDirective } from '@shared/components/popover.component'; import { TbStringTemplateOutletDirective } from '@shared/components/directives/sring-template-outlet.directive'; @@ -395,6 +396,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) WidgetsBundleSearchComponent, CopyButtonComponent, TogglePasswordComponent, + WidgetButtonToggleComponent, ProtobufContentComponent, BranchAutocompleteComponent, CountryAutocompleteComponent, @@ -658,6 +660,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) WidgetsBundleSearchComponent, CopyButtonComponent, TogglePasswordComponent, + WidgetButtonToggleComponent, ProtobufContentComponent, BranchAutocompleteComponent, CountryAutocompleteComponent, diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/gateway/bacnet-device-name-field-expression_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/bacnet-device-name-field-expression_fn.md new file mode 100644 index 0000000000..3a97cc4376 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/bacnet-device-name-field-expression_fn.md @@ -0,0 +1,21 @@ +## Expressions + +This field allows dynamically constructing a formatted device name using values extracted from a JSON object. You can specify variables to access the relevant fields in the JSON. + +# Available Variables + +You can use the following variables to extract specific device information: + +* `objectName`: Extracts the device's object name (e.g., `"Main Controller"`). +* `vendorId`: Extracts the device's vendor ID, typically a numeric identifier representing the manufacturer (e.g., `"1234"`). +* `objectId`: Extracts the device's unique object identifier (e.g., `"999"`). +* `address`: Extracts the device's network address (e.g., `"192.168.1.1"`). + +# Examples + +* `"Device ${objectName}"` + If the objectName variable exist and contains `"objectName": "Main Controller"`, the device on platform will have the following name: `Device Main Controller`. +* `"Vendor: ${vendorId}"` + If the vendorId variable exist and contains `"vendorId": 1234`, the device on platform will have the following name: `Vendor: 1234`. +* `"Device ID: ${objectId} at ${address}"` + If the objectId variable exist and contains `"vendorId": 999 `and address variable exist and contains `"address": "192.168.1.1"` , the device on platform will have the following name: `Device ID: 999 at 192.168.1.1`. diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/gateway/bacnet-device-profile-name-expression_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/bacnet-device-profile-name-expression_fn.md new file mode 100644 index 0000000000..1b84c4d6ff --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/bacnet-device-profile-name-expression_fn.md @@ -0,0 +1,21 @@ +## Expressions + +This field allows dynamically constructing a formatted profile name using values extracted from a JSON object. You can specify variables to access the relevant fields in the JSON. + +# Available Variables + +You can use the following variables to extract specific device information: + +* `objectName`: Extracts the device's object name (e.g., `"Main Controller"`). +* `vendorId`: Extracts the device's vendor ID, typically a numeric identifier representing the manufacturer (e.g., `"1234"`). +* `objectId`: Extracts the device's unique object identifier (e.g., `"999"`). +* `address`: Extracts the device's network address (e.g., `"192.168.1.1"`). + +# Examples + +* `"Device ${objectName}"` + If the objectName variable exist and contains `"objectName": "Main Controller"`, the device on platform will have the following name: `Device Main Controller`. +* `"Vendor: ${vendorId}"` + If the vendorId variable exist and contains `"vendorId": 1234`, the device on platform will have the following name: `Vendor: 1234`. +* `"Device ID: ${objectId} at ${address}"` + If the objectId variable exist and contains `"vendorId": 999 `and address variable exist and contains `"address": "192.168.1.1"` , the device on platform will have the following name: `Device ID: 999 at 192.168.1.1`. 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 aa21a9c0c3..c2f45da84f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3179,8 +3179,10 @@ "critical": "Critical", "critical-click": "Critical click", "critical-state-hint": "Indicates whether component is in critical state.", - "critical-state-animation": "Alarm state animation", + "critical-state-animation": "Critical state animation", "critical-state-animation-hint": "Whether to enable blinking animation when component is in critical state.", + "warning-critical-state-animation": "Warning/Critical state animation", + "warning-critical-state-animation-hint": "Whether to enable blinking animation when component is in warning or critical state.", "animation": "Animation", "broken": "Broken", "broken-hint": "Indicates whether component is broken.", @@ -5705,7 +5707,12 @@ "action-button": { "behavior": "Behavior", "on-click": "On click", - "on-click-hint": "Action triggered when the button is clicked" + "on-click-hint": "Action triggered when the button is clicked", + "first-button-click": "First button click", + "first-button-click-hint": "Action while pressing on first button.", + "second-button-click": "Second button click", + "second-button-click-hint": "Action while pressing on second button.", + "button-click-hint": "Action while pressing on widget." }, "command-button": { "behavior": "Behavior", @@ -5750,6 +5757,18 @@ "vertical-fill": "Vertical fill", "button-appearance": "Button appearance" }, + "segmented-button": { + "layout": "Layout", + "layout-squared": "Squared", + "layout-rounded": "Rounded", + "card-border": "Card border", + "button-appearance": "Button appearance", + "first": "First", + "second": "Second", + "color-styles": "Color styles", + "selected": "Selected", + "unselected": "Unselected" + }, "button": { "layout": "Layout", "outlined": "Outlined", @@ -5763,6 +5782,7 @@ "color-palette": "Color palette", "main": "Main", "background": "Background", + "border": "Border", "custom-styles": "Custom styles", "clear-style": "Clear style", "shadow": "Shadow", @@ -5776,11 +5796,16 @@ "activated-state-hint": "Configure condition under which the button is active.", "disabled-state": "Disabled state", "disabled-state-hint": "Configure condition under which the button is disabled.", + "selected-state": "Select button", + "selected-state-hint": "Configure condition under which the button is select.", "enabled": "Enabled", "hovered": "Hovered", "pressed": "Pressed", "activated": "Activated", - "disabled": "Disabled" + "disabled": "Disabled", + "initial": "First button", + "first": "First", + "second": "Second" }, "background": { "background": "Background", diff --git a/ui-ngx/src/assets/widget/segmented-button/rounded-layout.svg b/ui-ngx/src/assets/widget/segmented-button/rounded-layout.svg new file mode 100644 index 0000000000..6f0963016c --- /dev/null +++ b/ui-ngx/src/assets/widget/segmented-button/rounded-layout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui-ngx/src/assets/widget/segmented-button/squared-layout.svg b/ui-ngx/src/assets/widget/segmented-button/squared-layout.svg new file mode 100644 index 0000000000..6fb5abd30b --- /dev/null +++ b/ui-ngx/src/assets/widget/segmented-button/squared-layout.svg @@ -0,0 +1 @@ + \ No newline at end of file