diff --git a/application/src/main/data/json/system/widget_bundles/gateway_widgets.json b/application/src/main/data/json/system/widget_bundles/gateway_widgets.json
index bf94b2c5b5..bd6e15ed3d 100644
--- a/application/src/main/data/json/system/widget_bundles/gateway_widgets.json
+++ b/application/src/main/data/json/system/widget_bundles/gateway_widgets.json
@@ -3,7 +3,9 @@
"alias": "gateway_widgets",
"title": "Gateway widgets",
"image": "data:image/png;base64,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",
- "description": "Widgets to manage ThingsBoard IoT Gateway."
+ "description": "Widgets to manage ThingsBoard IoT Gateway.",
+ "externalId": null,
+ "name": "Gateway widgets"
},
"widgetTypes": [
{
@@ -64,22 +66,39 @@
}
},
{
- "alias": "gateway_list",
- "name": "Gateway list",
+ "alias": "gateway_configuration2",
+ "name": "Gateway configuration",
+ "image": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAQEBAQEBAQEBAQGBgUGBggHBwcHCAwJCQkJCQwTDA4MDA4MExEUEA8QFBEeFxUVFx4iHRsdIiolJSo0MjRERFwBBAQEBAQEBAQEBAYGBQYGCAcHBwcIDAkJCQkJDBMMDgwMDgwTERQQDxAUER4XFRUXHiIdGx0iKiUlKjQyNEREXP/CABEIA2cGdAMBIgACEQEDEQH/xAAdAAEAAwEBAQEBAQAAAAAAAAAAAwQFBgIBBwgJ/9oACAEBAAAAAP7vvgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM3SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZukAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzdJh4/aYu0B+Z/pPtyUXZBjRbwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM3Sfg0nBwa+fuKmN/S3Ac3P+ZbX6J+d9Hp4eHv4vUYf67+jAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGbpOF5vL1bPR8D0uVx/9G/h/TxQzbn8q/u82nz3U5PrftdiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZukAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzdIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABm6QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADN0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGbpAAIfNgVbQAAAAqWvvz6AAAAI6tz0AAAAArweY7F0AAAAQRXIfHvxZAAAABRj0gAAZukAA4flY7FbQufpYAAAA/L+Xz37pp/QAAAHKcBqwV/n39kAAAAHCcdpfPn64AAAAON4K3t4lr5Z7PfAAAAMPgsPbpfrdwAAzdIABDFBcqTLYAAAA56flui6EAAAAR1orcHMdTcAAAAEEE0tS8AAAAIIPkkNmLzbkAAAAFFZpaQAAzdIAAAAAAAD4PoAAAAAAAAAAAAAAAAAAAAAAAABm3/QAAAAA/HAAAAAHcdNJ9AAAAAAAAAAAAAAAAAAAAAAfPWbpAAAAAB8AAAAAH0AAAAAAAAAAAAAAAAAAAAAAM3SefgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPv36Zuk8ewAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIpTN0kUoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE5m6SKVU8gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABNN9IJzN0kUrPh56x6t42hBO864AAAAAAAAAAAAAAAAAAAAAAAAAAAAALloQTmbpIpWfDz+JoaVD0yuh1pAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAXLQgnM3SRSs+HPsx+1hUl9KdhBeqI9DI1wAAAAAAAAAAAAAAAAAAAAAAAAAAALloQTmbpIpWfD+dWuP7jkbfzpOTk738y02/lcXe62bkv24AAAAAAAAAAAAAAAAAAAAAAAAAAAC5aEE5m6SKVnw1cLQ1eT0Ieg5av1eTBf8Audmbd/Ez+8AAAAAAAAAAAAAAAAAAAAAAAAAAAAuWhBOZukilZ8IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAXLQgnM3SRSsysAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFvREE5m6SKUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACCczdJFKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBOZukilUIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtW/pBOZukilZ8OXS+RHqhq74AAAAAAAAAAAAAAAAAAAAAAAAAAAAAFy0IJzN0kUrPhyYc7ezr1Px0IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC5aEE5m6SKVnw8xu0Nkxdoxtln4+7zfXmRrhjbObpcfNo1fvRAx9jlOrxffnZMrVyb1bRc7qZlnYfOR6K6Y+wcrrUde2Mf3q0si3sgMjXAAAAAAAAAAAAAAALloQTmbpIpWfDw+HU567s8j1fOZH6xwX7Jk0ecscH01Da57O7fnMf9Z4H9O1eZ5vXp4fO6W1Uxtbf5rvuoh4L7j1N7E86Ulbpuxy+eyGFsbv510ePo5+14/V/f5xJrfnUlbuOR7Xkv2K7+R9F+e9Ry1hF0fIdnlfpHVgAAAAAAAAAAAAAAXLQgnM3SRSs+FnZOp8LkEFexrZ9/Ozb96jcip6UEFexJpc9qZWzDBZgmQVLNPo2fmbPyGu0cvze0Gfm6Hy5Rs1Z5adK7q/Y8K/7kpaNGtLss+hozUT5g6kGnpewAAAAAAAAAAAAAALloQTmbpIpWfCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFy0IJzN0kUrPhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI5LloQTmbpIpWfCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzaW/ctCCczdJFK5ykAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGnuiCczdJFKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBOZukilAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgnM3SRSgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQTmbpIpXG0s7f5GfO/XbgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBOZukilcDV4j9Y/L9bR/S5QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACCczdJFKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBOZukilK/sAAYP8+5oANr+iNUAAAAAAAAAAAAAAAAAAAAAAAAAACCczdJFKp0VeSR49xSvHsP4D/J2tkuj88808wftH+gwAAAAAAAAAAAAAAAAAAAAAAAAAAEE5m6SKVnVmdm+5Yfull/Pljxvn+W1R+0d/76T8Q/U9a7yN3+S2l/q39AAAAAAAAAAAAAAAAAAAAAAAAAABBOZukilRZQB8r+5q9g/j3+bQJ+soc31/IP6l/skAAAAAAAAAAAAAAAAAAAAAAAAAACCczdJFKefEgABFweKA+nzoe6+gAAAAAAAAAAAAAAAAAAAAAAAAAAQTmbpIpR8Affn0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIJzN0kUp55LwB41t8j9gD56PP0AfPR49AAefX0j9gPnoj9gD56AAAAAAAAAAAAAAAAABBOZukilPzjowCjL1tGmAC/bgzAAaFqrngAEmqyfAA1JqNMAGpMAAAAAAAAAAAAAAAAAEE5m6SKVztG4/F7subqdL17n/0TFM35D7eo5tI2s6spRefkT1avJNbJjUK9+CKH29efGx6aTNMVLFJZurOjimT6h9vvq1aWdEAAAAAAAAAAAAAAAAAIJzN0kUqnxW65eadn6+oxO/yY1fz7+PnieVJrUabxD9PUU8ixpZ1Z4j8fXz35fPU/1sMcq/JYIL0y3fyY1Z9ePaX2t3wAAAAAAAAAAAAAAAAAgnM3SRSnNYXz4+vJN19/5RAD5f8ASjF7APVz18o/AAPNmyr1fQHiW880QB4lvAAAAAAAAAAAAAAAAAAgnM3SRSgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQTmbpIpQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIJzN0kUqpH6AAAAAAAAAAAAAAAfLE1aMAAAAAAAAAAAAAAAS2BBOZukilZ8NN5+ffNS9dAAAAAAAAAAAAAA0/dWnhw6Pn7mWNeYAAAAAAAAAAAAAC5aEE5m6SKVnw4Gbm7VKDoNmQAAAAAAAAAAAAADT916OJnxT52nqaHsAAAAAAAAAAAAAC5aEE5m6SKVnw1486f1qAAAAAAAAAAAAAAfn+70en6gooUwAAAAAAAAAAAAAALloQTmbpIpWfDwH5b0G9d/RgAAAAAAAAAAAAAI5Gn6gouI4eXewP2sAAAAAAAAAAAAAAuWhBOZukilZ8OPkXL2R1gAAAAAAAAAAAAAA0/UFGlT5rz0XOfoIAAAAAAAAAAAAABctCCczdJFKz4QAAAAAAAAAAAAAABp+69EAAAAAAAAAAAAAAAXrAgnM3SRSsyp6AAAAAAAAAAAAAAAfL1/OqAAAAAAAAAAAAAAAC3oiCczdJFKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBOZukilAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgnM3SRSqEAAAAAAAAAAAaMtCAAAABfmqVQAAAAAAAAAAAAAABauCCczdJFKz4cul8khrze58yexTu74AAAAAAADQmz4ee6EAAAGn6go8jFq1mlBRuUrm+AAAAAAAAAAAAAL1gQTmbpIpWfDkw52tUnkxr80njxugAAAAAAAGhNnwgAAANP1BRzM6zJidBFW+zbIAAAAAAAAAAAAAXLQgnM3SRSs+Hl9jQAAAAAAAAAAaE2fCy9QAAB+d9D0en6go4MHSgAAAAAAAAAAAAAAXLQgnM3SRSs+HkYO1AAAAAAAAAAaE2fD+XchFsbv6gAAA0/UFHB97YAAAAAAAAAAAAAAFy0IJzN0kUrPhy9L0AAAAAAAAAA0Js+HNjoa/jRAAAafqCjBOAAAAAAAAAAAAAAAuWhBOZukilc5SAAAAAAAAAADf0OdpAAAANnWw8wAAAAAAAAAAAAAAAae6IJzN0kUoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE5m6SKUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACCczdJFKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBOZukilAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgnM3S+Y8QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPVjUM3SeY/YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeJfpm6QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADN0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGbpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM3SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZukAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzdIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABm6QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADN0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGbpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM3SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZukAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzdIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABm6QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADN0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGbpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM3SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZukAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzdIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABm6QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADN0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGbpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM3SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOd4EDvuiZukAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfyD/J4H9Yf179zdIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADk8/GX8Tqu3/j/+Uf07pUfE8N/Vv9gfc3SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxuYxtT1JvdB/IH8o9X+kZknA8z/Vv9gfc3SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP5A/lAD+r/6++5ukAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcf+WgfqXYMzTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/8QAGQEBAQEBAQEAAAAAAAAAAAAAAAECAwQF/9oACAECEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbvMAAAAAAAAAAAAAAAADWQAAAAAAAAAAAAAAA0AAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABqBQAAAAAAAAAAAAAAZAagFigAAAAAAAAAAAAAZAaAAAAAAAAAAAAAAABkAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAEAagoAAAAAAAAAAAAAAGQGoFQFECxYpAssVAUAAAAAAAZAaAAAAAAAAAAAAAAABkBoAAAAAAAAAAAAAAAGQFAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADWwYyAAAAAAAAAAAAAAAD6lxDb5YAAAAAAAAAAAAAAAPT1Ic/MAAAAAAAAAAAAAAKEAAAAAAAAAAAAAAAAb3ozzyAAAAAAAAAAAAAAABvdJygAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABoAAAAAAAIoAAAAAAAZAahQAAAAAAIoAAAAAAAZAaSgAAAAAAIqKAAAAAABkBQAAAAAAASgAAAAAABAAAAAAAAAAAAAAAAAABVAAAAAigAigAAAAAABkBoAAAAAlAAigAAAAAABkBoAAAAAligCKAAAAAAAGQFAAAAAJQAIoAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9XQ8mAAAAAAAAAAAAAAAA1uY+hJ18HIAAAAAAAAAAAAAAAA9XQ8mAAAAAAAAAAAAAAAAAD//EABsBAQADAQEBAQAAAAAAAAAAAAABAgMEBQYH/9oACAEDEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcWXoWAAAAAAAAAAAAAAAADOm4AAAAAAAAAAAAAAAcgAA6LgAAAAAAAAAAAAAAAAAAAAAAAAAAAAKgAAAAAAAAAAAAAAAmQMgQAAAAAAAAAAAAAADSwGQAIAAAAAAAAAAAAABpYDKAAAAAAAAAAAAAAAA0sAAAAAAAAAAAAAAAAAAVgAAAAAAAAAAAAAAALSBkEAAAAAAAAAAAAAAAaWAyAJgACJAAiQiQQAAAAAAANLAZQAAAAAAAAAAAAAAAGlgMoAAAAAAAAAAAAAAADSwFYAAAAAAAAAAAAAAAC0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLjB29gAAAAAAAAAAAAAAAR+Rx3TNuGv63IAAAAAAAAAAAAAAAfJ+YaVn0fqwAAAAAAAAAAAAABjBOwAAAAAAAAAAAAAAADix42nR1dIAAAAAAAAAAAAAAAHLy5S07tgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADOAAAAAAABNqAAAAAAABewGQQAAAAAACb5yQAAAAAABpYDIQAAAAAABN85IAAAAAAA0sBSAAAAAAABNqAAAAAAABeQAAAAAAAAAAAAAAAAADKJBAAAAGucAAm9AQAAAAAAGsgZAQAAAA1zgAJXzAAAAAAADSwGSAAAAANcxABN8wAAAAAAA0sBWAAAAAFogAEzUAAAAAAALSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+R80+x7wAAAAAAAAAAAAAAA4uee/8AOGvkfontAAAAAAAAAAAAAAAAHyPnH2HeAAAAAAAAAAAAAAAAAf/EADcQAAAFBAAEBAQFBAMBAQEAAAACAwQFAQYUFgcQERUSIDNwEyMkMhcYIUBgCDAxQyJBUDdRNf/aAAgBAQABCACDNTtbZSvudBGp2VqX3PgjU7K1L7nwRqdlal53Bc8BaLA0tcdpcRrIvr4xbUE1cUPb9Yysz55XjJwwg5isDKIrouEU10OV5XpDWPGoSMxF3cvJvoNt5mNxQ0jKzMEzt2bWnWbh049tII1OytS87mQSk+Pdhx0q/uS0XHFu10JG95iSXkb2fxN/IOb2tPgndMjxFuGZbXZDcNI9KT4j14cX5lfiDMLcTomdbx8/LU4TPLnl7Jlp5HiEeyTNbruiQtS3uGoupw5j7TuJ0y4PQUItwdt5srPravbHDqwOHcdSc4ecSLdtBRmleL/hPcV/n4psFLppwNm3SrqShuL1p28VmleN02BdPFVVOenM/wDpxJSTmZdPiNxXYksak2x4VfiFcFvXDPwl82C1bcO7TIy4x8R3FI++boYcJn71u5aXDwsuuxC0j4q5bxf8XzK8I7hkro4cWtNy3tjBGp2VqXne9gtLxrEyKMPwtmtkhbqvh7wZf5N0t4OR4UHfcPrVs0lw8NZCYXt+4WMFaz5nDSsbcbXgdHNOFT3hrS4eGEdOWTBWghCcMpyLu+Mvu4bCjIO7eLl18T4JUhFk1EVfwdn41FxCWvJ8Kbfd2fB2rG25w7fsLg2u7mXCrD4Z3Bw+Etw0pJseG7MPLMo64gRd9C6TxkIheNnWw64YvJm0OHqCUPwqkGUndk1Lp8MY5Xha34YSTThLchpmzp2eRsN7HcQ3t8RLDhDGEseXsiUiOGcsSfiJ687fsLsB7/UNw/tPRbRh7V9soI1OytS+58EanZWpfc+CNTsrUvufBGp2VqX3PgjU7K1L/fcu2zNBRy8ZvWb9sR4w8lX7MkijFqfvE3jRR0uyTFP0/wA//n71ZdFskdddtKxTtmrItynKoUpi/wDgOnjNggdy+rKxVGHdKspmHkUFnMdHT8DLKGRi6ysUm+JGn/eOXTZkgo5eJykWozpJJCrltRerWrN42kGqD1mZwgRdNsdo9aPUjqsv3zWUi3i6zRm5mIxois4X/ZQRqdlal/v3djkmrN7o7nFmDG+zxS60+yg7l6yclMW8re7SPWXnmUBdNa3GZVhjRDaEeLyE7Zr53+8jHTdtxMvPIuqSO5mL1O7k5Z/KupU87NyModjbi5od5R/Exj0n7u927F1DJIvpH4DyOv5k96pqOouHWYOpWdlLDI8JOPVLii3bMj5ytRKUXRfXI+nX7lP95c2MS6LR7nIvE14C9kW6GU4nLsXkYCIm5VnZTleH1fVnWyF/wX95ejZo6ShCqymG/tu8Elrxk1meelEtkETXy0kFCPJR7A2kGdGclcdgy1UpJ+5Qj2Tt8dxWPimMdaz1OQgGDgn7u6M7W56sYqWBxeHmuuY9s3ibzTRlJCXt9vfTRgsrPMoG5qCPa1Ys0Wtf78EanZWpf77hs3eInbO2sdGsG1WbBGGhm7ZVk2M1afGWc0ThYVu3XZNncbGv8c71GPYN6t6tv3kladrTLrNl3FvQLvBM6vKyFbsWT6Hta312UYwfUpSlOn7xZFFwkdBdrFxbBudowVg4RVq3ZK4jb46LkGhojJM9DmzaO51KVdHjYxR8nJqfvHTRo+bqNXxIqLSZ0j0lGjNdX46ySKaCREUDxMSd6WTP+8ctmrxBRq8TjItBiaNQXhYRcqBVTx7A6rVYy8NDOmabBwvHx61WlVlIyMVarMVFomKcsyRzhJFJBIiKH7xCLimjld2zUiopVRwserZp8RwqVGGhm7ZVk2/YQRqdlal/9Tp19m4I1OytSgyhymqUZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScGdH6V6eLj+PHx/Hj4/jx8fx4+P48fH8ePj+PHx/Hj4/jx8fx4+P48fH8ePj+PHx/Hj4/jx8fx4+P48fH8ePj+PHx/Hj4/jx8fx4+P48fH8ePj+PHx/Hj4/jx8fx4+P48fH8ePj+PHx/FsKXkVitufxzeHpSjg9KdBknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScJnMf7x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am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq5xJ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AXzHXy1t/wDd1/s/vv8A1rb/AO7r/Z/ff+tbf/d15AX3/rW3/wB3XkDfP+vbP/d15A3z96vWzG6VUVmysEqaT/LjJloPSgAgq2JL7yqSJ1U2otCqkqqqkZmS5IzJdZGS61Vdar5tS9GKwqdFNb0prelNb0prelNb0prelNb0prelNb0prelYSv4r5ualRf2dNDvSmt6U1vSmt6U1vSmt6U1vSmt6U1vSmt6VhOvzpWCToprelNb0prelNb0prelNb0prelNb0prelNb0prelNb0prelBjHSq0uVTo8+r99N/Cmt6U1vSmt6U1vSmt6U1vSmt6UO9KVETp8+pejFYVOimt6U1vSmt6U1vSmt6U1vSmt6U1vSmt6UO9KVETp/xb0o5+KepzqTv1S8/W5+dfvI6Uc/FPU51J36pefrc/Ov3kdKOfinqc6k79UvP1ufnXz6sU5upzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3Uuscfxz5lxqzTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFOb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qb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41LZkNo4oKbLguIioiZTI5wuvzXaGw+GjpNuvgBjkUVMiq9VX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NT48lG9DT5B0HNHSzjOiq4zjzXOJHcVtDQHngAsKqpnBLzaqv1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV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",
+ "description": "Allows to create or edit gateway configuration.",
+ "descriptor": {
+ "type": "latest",
+ "sizeX": 11,
+ "sizeY": 8,
+ "resources": [],
+ "templateHtml": " ",
+ "templateCss": "",
+ "controllerScript": "self.onInit = function() {\n if (self.ctx.datasources && self.ctx.datasources.length) {\n self.ctx.$scope.entityId = self.ctx.datasources[0].entity.id;\n }\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}",
+ "settingsSchema": "{}",
+ "dataKeySettingsSchema": "{}\n",
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Gateway configuration\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":500},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}"
+ }
+ },
+ {
+ "alias": "gateway_connector",
+ "name": "Gateway connector",
"image": null,
- "description": null,
+ "description": "Allows to create or edit gateway connectors.",
"descriptor": {
- "type": "static",
- "sizeX": 7.5,
- "sizeY": 3,
+ "type": "latest",
+ "sizeX": 11,
+ "sizeY": 8,
"resources": [],
- "templateHtml": " ",
- "templateCss": " .tb-entity-table {\r\n padding: 0 !important;\r\n }",
- "controllerScript": "self.onInit = function() {\n}\n",
+ "templateHtml": " ",
+ "templateCss": "",
+ "controllerScript": "self.onInit = function() {\n if (self.ctx.datasources && self.ctx.datasources.length) {\n self.ctx.$scope.entityId = self.ctx.datasources[0].entity.id;\n }\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}",
"settingsSchema": "{}",
- "dataKeySettingsSchema": "{}",
- "settingsDirective": "",
- "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"cardHtml\":\"
HTML code here
\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"Gateway list\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}"
+ "dataKeySettingsSchema": "{}\n",
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Gateway connector\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":500},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}"
}
}
]
diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts
index 4018fd491f..4bde4ab41d 100644
--- a/ui-ngx/src/app/core/utils.ts
+++ b/ui-ngx/src/app/core/utils.ts
@@ -205,6 +205,13 @@ export function base64toObj(b64Encoded: string): any {
return JSON.parse(json);
}
+export function stringToBase64(value: string): string {
+ return btoa(encodeURIComponent(value).replace(/%([0-9A-F]{2})/g,
+ function toSolidBytes(match, p1) {
+ return String.fromCharCode(Number('0x' + p1));
+ }));
+}
+
const scrollRegex = /(auto|scroll)/;
function parentNodes(node: Node, nodes: Node[]): Node[] {
diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts
index f7ef860562..f71adb36fa 100644
--- a/ui-ngx/src/app/modules/common/modules-map.ts
+++ b/ui-ngx/src/app/modules/common/modules-map.ts
@@ -296,6 +296,11 @@ import * as QueueFormComponent from '@home/components/queue/queue-form.component
import * as AssetProfileComponent from '@home/components/profile/asset-profile.component';
import * as AssetProfileDialogComponent from '@home/components/profile/asset-profile-dialog.component';
import * as AssetProfileAutocompleteComponent from '@home/components/profile/asset-profile-autocomplete.component';
+import * as DeviceGatewayCommandComponent from '@shared/components/device/device-gateway-command.component';
+import * as GatewayRemoteConfigurationDialogComponent from '@shared/components/dialog/gateway-remote-configuration-dialog';
+import * as GatewayConfigurationComponent from '@shared/components/device/gateway-configuration.component';
+import * as GatewayConnectorComponent from '@shared/components/device/gateway-connectors.component';
+
import { IModulesMap } from '@modules/common/modules-map.models';
@@ -398,6 +403,10 @@ class ModulesMap implements IModulesMap {
'@shared/components/help.component': HelpComponent,
'@shared/components/help-markdown.component': HelpMarkdownComponent,
'@shared/components/help-popup.component': HelpPopupComponent,
+ '@shared/components/device/device-gateway-command.component': DeviceGatewayCommandComponent,
+ '@shared/components/device/gateway-configuration.component': GatewayConfigurationComponent,
+ '@shared/components/device/gateway-connectors.component': GatewayConnectorComponent,
+ '@shared/components/device/gateway-remote-configuration-dialog': GatewayRemoteConfigurationDialogComponent,
'@shared/components/tb-checkbox.component': TbCheckboxComponent,
'@shared/components/toast.directive': TbToast,
'@shared/components/tb-error.component': TbErrorComponent,
diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials.module.ts b/ui-ngx/src/app/modules/home/components/device/device-credentials.module.ts
index 7023f09a40..58c39a3699 100644
--- a/ui-ngx/src/app/modules/home/components/device/device-credentials.module.ts
+++ b/ui-ngx/src/app/modules/home/components/device/device-credentials.module.ts
@@ -1,17 +1,32 @@
///
-/// Copyright © 2016-2023 The Thingsboard Authors
+/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
///
-/// 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
+/// Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
///
-/// http://www.apache.org/licenses/LICENSE-2.0
+/// NOTICE: All information contained herein is, and remains
+/// the property of ThingsBoard, Inc. and its suppliers,
+/// if any. The intellectual and technical concepts contained
+/// herein are proprietary to ThingsBoard, Inc.
+/// and its suppliers and may be covered by U.S. and Foreign Patents,
+/// patents in process, and are protected by trade secret or copyright law.
///
-/// 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.
+/// Dissemination of this information or reproduction of this material is strictly forbidden
+/// unless prior written permission is obtained from COMPANY.
+///
+/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
+/// managers or contractors who have executed Confidentiality and Non-disclosure agreements
+/// explicitly covering such access.
+///
+/// The copyright notice above does not evidence any actual or intended publication
+/// or disclosure of this source code, which includes
+/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
+/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
+/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
+/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
+/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
+/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
+/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
+/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
///
import { NgModule } from '@angular/core';
@@ -22,7 +37,7 @@ import { DeviceCredentialsComponent } from '@home/components/device/device-crede
import { DeviceCredentialsLwm2mComponent } from '@home/components/device/device-credentials-lwm2m.component';
import { DeviceCredentialsLwm2mServerComponent } from '@home/components/device/device-credentials-lwm2m-server.component';
import { DeviceCredentialsMqttBasicComponent } from '@home/components/device/device-credentials-mqtt-basic.component';
-import {DeviceGatewayCommandComponent} from "@home/components/device/device-gateway-command.component";
+import { DeviceExampleCommandComponent } from '@home/components/device/device-example-command.component';
@NgModule({
declarations: [
@@ -31,7 +46,7 @@ import {DeviceGatewayCommandComponent} from "@home/components/device/device-gate
DeviceCredentialsLwm2mComponent,
DeviceCredentialsLwm2mServerComponent,
DeviceCredentialsMqttBasicComponent,
- DeviceGatewayCommandComponent
+ DeviceExampleCommandComponent
],
imports: [
CommonModule,
@@ -43,7 +58,7 @@ import {DeviceGatewayCommandComponent} from "@home/components/device/device-gate
DeviceCredentialsLwm2mComponent,
DeviceCredentialsLwm2mServerComponent,
DeviceCredentialsMqttBasicComponent,
- DeviceGatewayCommandComponent
+ DeviceExampleCommandComponent
]
})
export class DeviceCredentialsModule { }
diff --git a/ui-ngx/src/app/modules/home/components/device/device-example-command.component.html b/ui-ngx/src/app/modules/home/components/device/device-example-command.component.html
new file mode 100644
index 0000000000..0663d12a14
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/device/device-example-command.component.html
@@ -0,0 +1,101 @@
+
+
+
+
+ MQTT
+
+
+ COAP
+
+
+ HTTP
+
+
+
In order to publish telemetry data to (ThingsBoard device ) with credentials of the current device you can use the following commands.
+
+
+
MQTT
+
1. Setup:
+
2. Send command:
+
+
+
+
COAP
+
1. Setup:
+
2. Send command: (based on CoAP cli)
+
+
{{ coapCode }}
+
+
+
+
JSON example: {{'[{"temperature": 40}, {"isRainy": true}]'}}
+
+
+
+
HTTP
+
1. Setup:
+
2. Send command:
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/device/device-example-command.component.scss b/ui-ngx/src/app/modules/home/components/device/device-example-command.component.scss
new file mode 100644
index 0000000000..e3fc24d4dc
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/device/device-example-command.component.scss
@@ -0,0 +1,56 @@
+/**
+ * ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
+ *
+ * Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
+ *
+ * NOTICE: All information contained herein is, and remains
+ * the property of ThingsBoard, Inc. and its suppliers,
+ * if any. The intellectual and technical concepts contained
+ * herein are proprietary to ThingsBoard, Inc.
+ * and its suppliers and may be covered by U.S. and Foreign Patents,
+ * patents in process, and are protected by trade secret or copyright law.
+ *
+ * Dissemination of this information or reproduction of this material is strictly forbidden
+ * unless prior written permission is obtained from COMPANY.
+ *
+ * Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
+ * managers or contractors who have executed Confidentiality and Non-disclosure agreements
+ * explicitly covering such access.
+ *
+ * The copyright notice above does not evidence any actual or intended publication
+ * or disclosure of this source code, which includes
+ * information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
+ * ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
+ * OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
+ * THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
+ * AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
+ * THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
+ * DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
+ * OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
+ */
+:host {
+
+ mat-button-toggle-group, .protocol-title {
+ margin-bottom: 20px;
+ }
+
+ span {
+ margin-bottom: 10px;
+ }
+
+ .protocol-toggle {
+ width: fit-content;
+ }
+
+ .help-title {
+ padding-bottom: 5px;
+ }
+
+ .protocol-send-command {
+ width: 100%;
+ }
+
+ .pre-wrap {
+ white-space: pre-wrap;
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/device/device-example-command.component.ts b/ui-ngx/src/app/modules/home/components/device/device-example-command.component.ts
new file mode 100644
index 0000000000..853084ae32
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/device/device-example-command.component.ts
@@ -0,0 +1,113 @@
+///
+/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
+///
+/// Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
+///
+/// NOTICE: All information contained herein is, and remains
+/// the property of ThingsBoard, Inc. and its suppliers,
+/// if any. The intellectual and technical concepts contained
+/// herein are proprietary to ThingsBoard, Inc.
+/// and its suppliers and may be covered by U.S. and Foreign Patents,
+/// patents in process, and are protected by trade secret or copyright law.
+///
+/// Dissemination of this information or reproduction of this material is strictly forbidden
+/// unless prior written permission is obtained from COMPANY.
+///
+/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
+/// managers or contractors who have executed Confidentiality and Non-disclosure agreements
+/// explicitly covering such access.
+///
+/// The copyright notice above does not evidence any actual or intended publication
+/// or disclosure of this source code, which includes
+/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
+/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
+/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
+/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
+/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
+/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
+/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
+/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
+///
+
+import { Component, Input, OnInit } from '@angular/core';
+import { FormControl } from '@angular/forms';
+import { Router } from '@angular/router';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { TranslateService } from '@ngx-translate/core';
+import { ActionNotificationShow } from '@core/notification/notification.actions';
+import { helpBaseUrl } from '@shared/models/constants';
+
+@Component({
+ selector: 'tb-device-example-command',
+ templateUrl: './device-example-command.component.html',
+ styleUrls: ['./device-example-command.component.scss']
+})
+
+export class DeviceExampleCommandComponent implements OnInit {
+
+ @Input()
+ token: string;
+
+ helpLink: string = helpBaseUrl + '/docs/reference/protocols/';
+
+ mqttCode: string;
+
+ coapCode: string;
+
+ httpCode: string;
+
+ mqttSetup: string;
+
+ coapSetup: string;
+
+ httpSetup: string;
+
+ protocolCtrl: FormControl;
+
+ constructor(protected router: Router,
+ protected store: Store,
+ private translate: TranslateService) {
+ }
+
+
+ ngOnInit(): void {
+ const HOST = window.location.hostname;
+ this.mqttCode = `mosquitto_pub -d -q 1 -h ${HOST} -t "v1/devices/me/telemetry" -u "${this.token}" -m "{"temperature":42}"`;
+ this.coapCode = `cat telemetry-data.json | coap post coap://${HOST}/api/v1/${this.token}/telemetry`;
+ this.httpCode = `curl -v -X POST --data "{"temperature":42,"humidity":73}" http://${HOST}/api/v1/${this.token}/telemetry --header "Content-Type:application/json"`;
+
+ this.protocolCtrl = new FormControl('mqtt');
+ const mqttLink = ' Mqtt Guide ';
+
+ // @ts-ignore
+ const platform = window.navigator?.userAgentData?.platform || window.navigator.platform,
+ macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'],
+ windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'];
+ if (macosPlatforms.indexOf(platform) !== -1) {
+ this.mqttSetup = 'brew install mosquitto-clients';
+ this.coapSetup = 'npm install coap-cli -g';
+ this.httpSetup = 'brew install curl';
+ } else if (windowsPlatforms.indexOf(platform) !== -1) {
+ this.mqttSetup = mqttLink;
+ this.coapSetup = 'npm install coap-cli -g';
+ this.httpSetup = 'not required, available by default in windows 10+';
+ } else if (/Linux/.test(platform)) {
+ this.mqttSetup = 'sudo apt-get install mosquitto-clients';
+ this.coapSetup = 'npm install coap-cli -g';
+ this.httpSetup = 'sudo apt-get install curl';
+ }
+ }
+
+ onDockerCodeCopied() {
+ this.store.dispatch(new ActionNotificationShow(
+ {
+ message: this.translate.instant('gateway.command-copied-message'),
+ type: 'success',
+ target: 'dockerCommandDialogContent',
+ duration: 1200,
+ verticalPosition: 'bottom',
+ horizontalPosition: 'left'
+ }));
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.html b/ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.html
deleted file mode 100644
index 64c60ba8fa..0000000000
--- a/ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.html
+++ /dev/null
@@ -1,55 +0,0 @@
-
-
-
In order to run ThingsBoard IoT gateway (ThingsBoard IoT gateway ) in docker with credentials for this device you can use the following commands.
-
-
- gateway.windows
- gateway.linux-macos
-
-
-
-
-
-
{{ linuxCode }}
-
-
-
-
-
-
-
-
{{ windowsCode }}
-
-
-
-
-
-
diff --git a/ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.ts b/ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.ts
deleted file mode 100644
index 40c7375571..0000000000
--- a/ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-///
-/// Copyright © 2016-2022 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, Input, OnInit} from '@angular/core';
-import {
- FormControl,
-} from '@angular/forms';
-import {Router} from "@angular/router";
-import {Store} from "@ngrx/store";
-import {AppState} from "@core/core.state";
-import {TranslateService} from "@ngx-translate/core";
-import {ActionNotificationShow} from "@core/notification/notification.actions";
-import {DeviceService} from "@core/http/device.service";
-import {helpBaseUrl} from "@shared/models/constants";
-
-enum OsType {
- linux = 'linux',
- macos = 'macos',
- windows = 'win'
-}
-
-@Component({
- selector: 'tb-gateway-command',
- templateUrl: './device-gateway-command.component.html',
- styleUrls: []
-})
-
-export class DeviceGatewayCommandComponent implements OnInit {
-
- @Input()
- token: string;
-
- @Input()
- deviceId: string;
-
- linuxCode: string;
- windowsCode: string;
- selectedOSControll: FormControl;
- osTypes = OsType;
- helpLink: string = helpBaseUrl + '/docs/iot-gateway/install/docker-installation/';
-
- constructor(protected router: Router,
- protected store: Store,
- private translate: TranslateService,
- private deviceService: DeviceService) {
- }
-
-
- ngOnInit(): void {
- const HOST = window.location.hostname;
- if (this.deviceId) {
- this.deviceService.getDeviceCredentials(this.deviceId).subscribe(credentials=>{
- this.token = credentials.credentialsId;
- this.createRunCode(HOST);
- })
- }
- this.selectedOSControll = new FormControl('');
- // @ts-ignore
- const platform = window.navigator?.userAgentData?.platform || window.navigator.platform,
- macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'],
- windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'];
- if (macosPlatforms.indexOf(platform) !== -1) {
- this.selectedOSControll.setValue(OsType.macos);
- } else if (windowsPlatforms.indexOf(platform) !== -1) {
- this.selectedOSControll.setValue(OsType.windows);
- } else if (/Linux/.test(platform)) {
- this.selectedOSControll.setValue(OsType.linux);
- }
- this.createRunCode(HOST);
- }
-
- createRunCode(HOST) {
- this.linuxCode = "docker run -it -v ~/.tb-gateway/logs:/thingsboard_gateway/logs -v " +
- "~/.tb-gateway/extensions:/thingsboard_gateway/extensions -v ~/.tb-gateway/config:/thingsboard_gateway/config --name tb-gateway -e host=" +
- HOST +
- " -e port=1883 -e accessToken=" +
- this.token +
- " --restart always thingsboard/tb-gateway";
- this.windowsCode = "docker run -it -v %HOMEPATH%/tb-gateway/config:/thingsboard_gateway/config -v " +
- "%HOMEPATH%/tb-gateway/extensions:/thingsboard_gateway/extensions -v %HOMEPATH%/tb-gateway/logs:/thingsboard_gateway/logs " +
- "--name tb-gateway -e host=" +
- HOST +
- " -e port=1883 -e accessToken=" +
- this.token +
- " --restart always thingsboard/tb-gateway";
- }
-
- goToDocumentationPage() {
- window.open(this.helpLink, '_blank');
- }
-
- onDockerCodeCopied() {
- this.store.dispatch(new ActionNotificationShow(
- {
- message: this.translate.instant('gateway.command-copied-message'),
- type: 'success',
- target: 'dockerCommandDialogContent',
- duration: 1200,
- verticalPosition: 'bottom',
- horizontalPosition: 'left'
- }));
- }
-}
diff --git a/ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.html b/ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.html
deleted file mode 100644
index 6318b74537..0000000000
--- a/ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.html
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
diff --git a/ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.ts b/ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.ts
deleted file mode 100644
index 60bf9c1e21..0000000000
--- a/ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-///
-/// Copyright © 2016-2022 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, Inject, OnInit} from '@angular/core';
-import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
-import {Store} from '@ngrx/store';
-import {AppState} from '@core/core.state';
-import {Router} from '@angular/router';
-import {DialogComponent} from '@app/shared/components/dialog.component';
-import {TranslateService} from '@ngx-translate/core';
-import {Device, DeviceCredentials} from "@shared/models/device.models";
-
-export interface GatewayCommandDialogData {
- device: Device,
- credentials: DeviceCredentials
-}
-
-enum OsType {
- linux = 'linux',
- macos = 'macos',
- windows = 'win'
-}
-
-@Component({
- selector: 'tb-gateway-command-dialog',
- templateUrl: './gateway-command-dialog.component.html',
- styleUrls: []
-})
-export class GatewayCommandDialogComponent extends DialogComponent implements OnInit {
-
- constructor(protected router: Router,
- protected store: Store,
- @Inject(MAT_DIALOG_DATA) public data: GatewayCommandDialogData,
- public dialogRef: MatDialogRef,) {
- super(store, router, dialogRef);
- }
-
- ngOnInit() {
- }
-
- close(): void {
- this.dialogRef.close();
- }
-
-}
diff --git a/ui-ngx/src/app/modules/home/components/gateway/gateway-list-table-config.ts b/ui-ngx/src/app/modules/home/components/gateway/gateway-list-table-config.ts
deleted file mode 100644
index 00ff31343e..0000000000
--- a/ui-ngx/src/app/modules/home/components/gateway/gateway-list-table-config.ts
+++ /dev/null
@@ -1,130 +0,0 @@
-///
-/// Copyright © 2016-2022 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 {
- DateEntityTableColumn,
- EntityTableColumn,
- EntityTableConfig
-} from '@home/models/entity/entities-table-config.models';
-import {EntityType, EntityTypeResource} from '@shared/models/entity-type.models';
-import {TranslateService} from '@ngx-translate/core';
-import {DatePipe} from '@angular/common';
-import {Direction} from '@shared/models/page/sort-order';
-import {MatDialog} from '@angular/material/dialog';
-import {TimePageLink} from '@shared/models/page/page-link';
-import {Observable} from 'rxjs';
-import {emptyPageData, PageData} from '@shared/models/page/page-data';
-import {UtilsService} from '@core/services/utils.service';
-import {DeviceService} from "@core/http/device.service";
-import {AttributeService} from "@core/http/attribute.service";
-import {Device} from "@shared/models/device.models";
-import {
- GatewayCommandDialogComponent,
- GatewayCommandDialogData
-} from "@home/components/gateway/gateway-command-dialog.component";
-import {ActionNotificationShow} from "@core/notification/notification.actions";
-import {Store} from "@ngrx/store";
-import {AppState} from "@core/core.state";
-import {map} from "rxjs/operators";
-import {EntityService} from "@core/http/entity.service";
-
-export class GatewayListTableConfig extends EntityTableConfig {
-
- constructor(protected store: Store,
- private deviceService: DeviceService,
- private attributeService: AttributeService,
- private entityService: EntityService,
- private datePipe: DatePipe,
- private translate: TranslateService,
- private utils: UtilsService,
- private dialog: MatDialog,
- updateOnInit = true) {
- super();
- this.loadDataOnInit = updateOnInit;
- this.tableTitle = 'Gateway list';
- this.useTimePageLink = false;
- this.pageMode = false;
- this.displayPagination = false;
- this.detailsPanelEnabled = false;
- this.selectionEnabled = false;
- this.searchEnabled = false;
- this.addEnabled = false;
- this.entitiesDeleteEnabled = false;
- this.actionsColumnTitle = '';
- this.entityTranslations = {
- noEntities: 'gateway.no-gateway-found',
- search: 'gateway.gateway-search'
- };
- this.entityResources = {} as EntityTypeResource;
-
- this.entitiesFetchFunction = () => this.fetchGateways();
-
- this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
-
- this.columns.push(
- new DateEntityTableColumn('createdTime', 'gateway.created-time', this.datePipe, '150px'));
-
- this.columns.push(
- new EntityTableColumn('entityName', 'gateway.gateway-name', '20%',
- (entity => this.utils.customTranslation(entity.name, entity.name))
- )
- );
-
- this.cellActionDescriptors.push(
- {
- name: this.translate.instant('gateway.command'),
- icon: 'vpn_key',
- isEnabled: () => true,
- onAction: ($event, entity) => this.showGatewayDockerCommand(entity)
- }
- );
- }
-
- showGatewayDockerCommand(entity: Device) {
- this.deviceService.getDeviceCredentials(entity.id.id).subscribe(credentials => {
- this.dialog.open
- (GatewayCommandDialogComponent,
- {
- disableClose: true,
- panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
- data: {
- credentials: credentials,
- device: entity
- }
- }).afterClosed().subscribe(
- (res) => {
- if (res) {
- this.updateData();
- }
- }
- );
- }, error => {
- const messageToShow = `${error}
`;
- this.store.dispatch(new ActionNotificationShow({message: messageToShow, type: 'error'}));
- })
- }
-
- fetchGateways(): Observable> {
- return this.entityService.getEntitiesByNameFilter(EntityType.DEVICE, "", -1).pipe(
- map((array: Array) => {
- const pageData = emptyPageData();
- pageData.data = array.filter(device => device.additionalInfo?.gateway);
- pageData.totalPages = 1;
- pageData.totalElements = array.length;
- return pageData;
- }))
- }
-}
diff --git a/ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.html b/ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.html
deleted file mode 100644
index 7190c87ead..0000000000
--- a/ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.html
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
diff --git a/ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.scss b/ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.scss
deleted file mode 100644
index 09f06ae6b5..0000000000
--- a/ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.scss
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Copyright © 2016-2022 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.
- */
-:host {
- width: 100%;
- height: 100%;
- display: block;
- .tb-table-widget {
- .table-container {
- position: relative;
- }
- .mat-table {
- .mat-row {
- &.invisible {
- visibility: hidden;
- }
- }
- }
- span.no-data-found {
- position: absolute;
- top: 60px;
- bottom: 0;
- left: 0;
- right: 0;
- }
- }
-}
-
diff --git a/ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.ts b/ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.ts
deleted file mode 100644
index 14e95234a2..0000000000
--- a/ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-///
-/// Copyright © 2016-2022 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, OnInit} from '@angular/core';
-import {UtilsService} from '@core/services/utils.service';
-import {TranslateService} from '@ngx-translate/core';
-import {DeviceService} from '@core/http/device.service';
-import {AttributeService} from '@core/http/attribute.service';
-import {GatewayListTableConfig} from "@home/components/gateway/gateway-list-table-config";
-import {DatePipe} from "@angular/common";
-import {MatDialog} from "@angular/material/dialog";
-import {Store} from "@ngrx/store";
-import {AppState} from "@core/core.state";
-import {EntityService} from "@core/http/entity.service";
-
-@Component({
- selector: 'tb-gateway-list',
- templateUrl: './gateway-list.component.html',
- styleUrls: ['./gateway-list.component.scss']
-})
-
-
-export class GatewayListComponent implements OnInit {
- gatewayListTableConfig: GatewayListTableConfig;
-
- constructor(
- protected store: Store,
- private utils: UtilsService,
- private translate: TranslateService,
- private datePipe: DatePipe,
- private deviceService: DeviceService,
- private entityService: EntityService,
- private attributeService: AttributeService,
- private dialog: MatDialog,
- ) {
- }
-
- ngOnInit(): void {
- this.gatewayListTableConfig = new GatewayListTableConfig(
- this.store,
- this.deviceService,
- this.attributeService,
- this.entityService,
- this.datePipe,
- this.translate,
- this.utils,
- this.dialog
- );
- }
-}
diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts
index 2b9a446e20..7a91645e31 100644
--- a/ui-ngx/src/app/modules/home/components/home-components.module.ts
+++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts
@@ -178,7 +178,6 @@ import { modulesMap } from '@modules/common/modules-map';
import { AlarmAssigneePanelComponent } from '@home/components/alarm/alarm-assignee-panel.component';
import { RouterTabsComponent } from '@home/components/router-tabs.component';
import { SendNotificationButtonComponent } from '@home/components/notification/send-notification-button.component';
-import { GatewayListComponent } from "@home/components/gateway/gateway-list.component";
@NgModule({
declarations:
@@ -203,7 +202,6 @@ import { GatewayListComponent } from "@home/components/gateway/gateway-list.comp
AlarmTableHeaderComponent,
AlarmTableComponent,
AlarmAssigneePanelComponent,
- GatewayListComponent,
AttributeTableComponent,
AddAttributeDialogComponent,
EditAttributeValuePanelComponent,
@@ -354,7 +352,6 @@ import { GatewayListComponent } from "@home/components/gateway/gateway-list.comp
RelationFiltersComponent,
AlarmTableComponent,
AlarmAssigneePanelComponent,
- GatewayListComponent,
AttributeTableComponent,
AliasesEntitySelectComponent,
AliasesEntityAutocompleteComponent,
diff --git a/ui-ngx/src/app/modules/home/components/shared-home-components.module.ts b/ui-ngx/src/app/modules/home/components/shared-home-components.module.ts
index 912fcfb52e..45ee713d10 100644
--- a/ui-ngx/src/app/modules/home/components/shared-home-components.module.ts
+++ b/ui-ngx/src/app/modules/home/components/shared-home-components.module.ts
@@ -18,7 +18,6 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@app/shared/shared.module';
import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-details-dialog.component';
-import { GatewayCommandDialogComponent } from "@home/components/gateway/gateway-command-dialog.component";
import { SHARED_HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
import { DeviceCredentialsModule } from "@home/components/device/device-credentials.module";
import { AlarmCommentComponent } from '@home/components/alarm/alarm-comment.component';
@@ -35,7 +34,6 @@ import { AlarmAssigneeComponent } from '@home/components/alarm/alarm-assignee.co
AlarmCommentComponent,
AlarmCommentDialogComponent,
AlarmAssigneeComponent
- GatewayCommandDialogComponent
],
imports: [
CommonModule,
@@ -47,7 +45,6 @@ import { AlarmAssigneeComponent } from '@home/components/alarm/alarm-assignee.co
AlarmCommentComponent,
AlarmCommentDialogComponent,
AlarmAssigneeComponent
- GatewayCommandDialogComponent
]
})
export class SharedHomeComponentsModule { }
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html
index c1485cf450..52a31314e2 100644
--- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html
+++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html
@@ -29,7 +29,7 @@
-
+
check
@@ -178,9 +178,17 @@
+
+
+
+
+
+
-
+
0"
[disabled]="(isLoading$ | async)"
(click)="previousStep()">{{ 'action.back' | translate }}
@@ -195,10 +203,11 @@
{{ 'action.cancel' | translate }}
{{ 'action.add' | translate }}
+ (click)="add()">{{ (deviceCredentials ? 'action.close': 'action.add') | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss
index 0fe18467fd..e3fbd3c7c6 100644
--- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss
+++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss
@@ -37,6 +37,10 @@
}
:host ::ng-deep {
+ .commands-container {
+ padding: 24px;
+ }
+
.mat-mdc-dialog-content {
display: flex;
flex-direction: column;
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts
index 77916d379c..80aa438e2d 100644
--- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts
+++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts
@@ -24,6 +24,8 @@ import { Router } from '@angular/router';
import {
createDeviceProfileConfiguration,
createDeviceProfileTransportConfiguration,
+ Device,
+ DeviceCredentials,
DeviceProfile,
DeviceProfileInfo,
DeviceProfileType,
@@ -57,9 +59,9 @@ import { deepTrim } from '@core/utils';
styleUrls: ['./device-wizard-dialog.component.scss']
})
export class DeviceWizardDialogComponent extends
- DialogComponent
implements OnDestroy, ErrorStateMatcher {
+ DialogComponent implements OnDestroy, ErrorStateMatcher {
- @ViewChild('addDeviceWizardStepper', {static: true}) addDeviceWizardStepper: MatStepper;
+ @ViewChild('addDeviceWizardStepper') addDeviceWizardStepper: MatStepper;
selectedIndex = 0;
@@ -69,7 +71,7 @@ export class DeviceWizardDialogComponent extends
entityType = EntityType;
- deviceTransportTypes = Object.values(DeviceTransportType);
+ deviceTransportTypes = Object.values(DeviceTransportType) as DeviceTransportType[];
deviceTransportTypeTranslations = deviceTransportTypeTranslationMap;
@@ -91,6 +93,10 @@ export class DeviceWizardDialogComponent extends
serviceType = ServiceType.TB_RULE_ENGINE;
+ device: Device;
+
+ deviceCredentials: DeviceCredentials;
+
private subscriptions: Subscription[] = [];
private currentDeviceProfileTransportType = DeviceTransportType.DEFAULT;
@@ -98,7 +104,7 @@ export class DeviceWizardDialogComponent extends
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData>,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
- public dialogRef: MatDialogRef,
+ public dialogRef: MatDialogRef,
private deviceProfileService: DeviceProfileService,
private deviceService: DeviceService,
private breakpointObserver: BreakpointObserver,
@@ -263,13 +269,15 @@ export class DeviceWizardDialogComponent extends
}
add(): void {
- if (this.allValid()) {
+ if (this.deviceCredentials) {
+ this.dialogRef.close(this.device);
+ } else if (this.allValid()) {
this.createDeviceProfile().pipe(
mergeMap(profileId => this.createDevice(profileId)),
- mergeMap(device => this.saveCredentials(device))
+ mergeMap(device => this.saveCredentials(device)),
).subscribe(
- (created) => {
- this.dialogRef.close(created);
+ (device) => {
+ this.device = device;
}
);
}
@@ -326,7 +334,7 @@ export class DeviceWizardDialogComponent extends
}
}
- private createDevice(profileId): Observable> {
+ private createDevice(profileId): Observable {
const device = {
name: this.deviceWizardFormGroup.get('name').value,
label: this.deviceWizardFormGroup.get('label').value,
@@ -349,10 +357,10 @@ export class DeviceWizardDialogComponent extends
this.addDeviceWizardStepper.selectedIndex = 0;
return throwError(e);
})
- );
+ ) as Observable;
}
- private saveCredentials(device: BaseData): Observable {
+ private saveCredentials(device: Device): Observable {
if (this.credentialsFormGroup.get('setCredential').value) {
return this.deviceService.getDeviceCredentials(device.id.id).pipe(
mergeMap(
@@ -370,9 +378,16 @@ export class DeviceWizardDialogComponent extends
);
}
),
- map(() => true));
+ map(() => device));
+ } else {
+ return this.deviceService.getDeviceCredentials(device.id.id).pipe(
+ map((deviceCredentials) => {
+ this.deviceCredentials = deviceCredentials;
+ return device;
+ })
+ );
}
- return of(true);
+ return of(device);
}
allValid(): boolean {
diff --git a/ui-ngx/src/app/shared/components/device/device-gateway-command.component.html b/ui-ngx/src/app/shared/components/device/device-gateway-command.component.html
new file mode 100644
index 0000000000..711897ce4d
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/device/device-gateway-command.component.html
@@ -0,0 +1,70 @@
+
+
+
In order to run ThingsBoard IoT gateway (ThingsBoard IoT gateway ) in docker with credentials for this device you can use the following commands.
+
+
+ gateway.windows
+ gateway.linux-macos
+
+
+
+
+
+
{{ linuxCode }}
+
+
+
+
+
+
+
+
{{ windowsCode }}
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/shared/components/device/device-gateway-command.component.ts b/ui-ngx/src/app/shared/components/device/device-gateway-command.component.ts
new file mode 100644
index 0000000000..ab0548e273
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/device/device-gateway-command.component.ts
@@ -0,0 +1,125 @@
+///
+/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
+///
+/// Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
+///
+/// NOTICE: All information contained herein is, and remains
+/// the property of ThingsBoard, Inc. and its suppliers,
+/// if any. The intellectual and technical concepts contained
+/// herein are proprietary to ThingsBoard, Inc.
+/// and its suppliers and may be covered by U.S. and Foreign Patents,
+/// patents in process, and are protected by trade secret or copyright law.
+///
+/// Dissemination of this information or reproduction of this material is strictly forbidden
+/// unless prior written permission is obtained from COMPANY.
+///
+/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
+/// managers or contractors who have executed Confidentiality and Non-disclosure agreements
+/// explicitly covering such access.
+///
+/// The copyright notice above does not evidence any actual or intended publication
+/// or disclosure of this source code, which includes
+/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
+/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
+/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
+/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
+/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
+/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
+/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
+/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
+///
+
+import { Component, Input, OnInit } from '@angular/core';
+import { FormControl } from '@angular/forms';
+import { Router } from '@angular/router';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { TranslateService } from '@ngx-translate/core';
+import { ActionNotificationShow } from '@core/notification/notification.actions';
+import { DeviceService } from '@core/http/device.service';
+import { helpBaseUrl } from '@shared/models/constants';
+
+enum OsType {
+ linux = 'linux',
+ macos = 'macos',
+ windows = 'win'
+}
+
+@Component({
+ selector: 'tb-gateway-command',
+ templateUrl: './device-gateway-command.component.html',
+ styleUrls: []
+})
+
+export class DeviceGatewayCommandComponent implements OnInit {
+
+ @Input()
+ token: string;
+
+ @Input()
+ deviceId: string;
+
+ linuxCode: string;
+ windowsCode: string;
+ selectedOSCControl: FormControl;
+ osTypes = OsType;
+ helpLink: string = helpBaseUrl + '/docs/iot-gateway/install/docker-installation/';
+
+ constructor(protected router: Router,
+ protected store: Store,
+ private translate: TranslateService,
+ private deviceService: DeviceService) {
+ }
+
+
+ ngOnInit(): void {
+ const HOST = window.location.hostname;
+ if (this.deviceId) {
+ this.deviceService.getDeviceCredentials(this.deviceId).subscribe(credentials => {
+ this.token = credentials.credentialsId;
+ this.createRunCode(HOST);
+ });
+ }
+ this.selectedOSCControl = new FormControl('');
+ // @ts-ignore
+ const platform = window.navigator?.userAgentData?.platform || window.navigator.platform,
+ macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'],
+ windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'];
+ if (macosPlatforms.indexOf(platform) !== -1) {
+ this.selectedOSCControl.setValue(OsType.macos);
+ } else if (windowsPlatforms.indexOf(platform) !== -1) {
+ this.selectedOSCControl.setValue(OsType.windows);
+ } else if (/Linux/.test(platform)) {
+ this.selectedOSCControl.setValue(OsType.linux);
+ }
+ this.createRunCode(HOST);
+ }
+
+ createRunCode(HOST) {
+ this.linuxCode = 'docker run -it -v ~/.tb-gateway/logs:/thingsboard_gateway/logs -v ' +
+ '~/.tb-gateway/extensions:/thingsboard_gateway/extensions -v ~/.tb-gateway/config:/thingsboard_gateway/config --name tb-gateway -e host=' +
+ HOST +
+ ' -e port=1883 -e accessToken=' +
+ this.token +
+ ' --restart always thingsboard/tb-gateway';
+ this.windowsCode = 'docker run -it -v %HOMEPATH%/tb-gateway/config:/thingsboard_gateway/config -v ' +
+ '%HOMEPATH%/tb-gateway/extensions:/thingsboard_gateway/extensions -v %HOMEPATH%/tb-gateway/logs:/thingsboard_gateway/logs ' +
+ '--name tb-gateway -e host=' +
+ HOST +
+ ' -e port=1883 -e accessToken=' +
+ this.token +
+ ' --restart always thingsboard/tb-gateway';
+ }
+
+ onDockerCodeCopied() {
+ this.store.dispatch(new ActionNotificationShow(
+ {
+ message: this.translate.instant('gateway.command-copied-message'),
+ type: 'success',
+ target: 'dockerCommandDialogContent',
+ duration: 1200,
+ verticalPosition: 'bottom',
+ horizontalPosition: 'left'
+ }));
+ }
+}
diff --git a/ui-ngx/src/app/shared/components/device/gateway-configuration.component.html b/ui-ngx/src/app/shared/components/device/gateway-configuration.component.html
new file mode 100644
index 0000000000..6c1300ece4
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/device/gateway-configuration.component.html
@@ -0,0 +1,683 @@
+
+
diff --git a/ui-ngx/src/app/shared/components/device/gateway-configuration.component.scss b/ui-ngx/src/app/shared/components/device/gateway-configuration.component.scss
new file mode 100644
index 0000000000..d6bcdf6e8a
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/device/gateway-configuration.component.scss
@@ -0,0 +1,166 @@
+/**
+ * ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
+ *
+ * Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
+ *
+ * NOTICE: All information contained herein is, and remains
+ * the property of ThingsBoard, Inc. and its suppliers,
+ * if any. The intellectual and technical concepts contained
+ * herein are proprietary to ThingsBoard, Inc.
+ * and its suppliers and may be covered by U.S. and Foreign Patents,
+ * patents in process, and are protected by trade secret or copyright law.
+ *
+ * Dissemination of this information or reproduction of this material is strictly forbidden
+ * unless prior written permission is obtained from COMPANY.
+ *
+ * Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
+ * managers or contractors who have executed Confidentiality and Non-disclosure agreements
+ * explicitly covering such access.
+ *
+ * The copyright notice above does not evidence any actual or intended publication
+ * or disclosure of this source code, which includes
+ * information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
+ * ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
+ * OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
+ * THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
+ * AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
+ * THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
+ * DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
+ * OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
+ */
+:host {
+ width: 100%;
+ height: 100%;
+ display: block;
+
+ .mat-toolbar {
+ background: transparent;
+ color: rgba(0, 0, 0, .87) !important;
+ }
+
+ .mat-content {
+ .expansion-panel-header {
+ font-weight: 600;
+ color: rgba(0, 0, 0, .87) !important;
+ }
+
+ mat-slide-toggle {
+ margin-bottom: 16px;
+ }
+
+ mat-form-field {
+ margin-right: 15px;
+ }
+
+ .block-title {
+ font-size: 20px;
+ font-weight: 400;
+ padding-top: 16px;
+ }
+
+ .hover-cursor {
+ cursor: pointer;
+ }
+ }
+
+ .security-toggle-group {
+ padding-bottom: 0;
+ background: transparent !important;
+ margin-bottom: 15px;
+ border: none;
+
+ .mat-button-toggle {
+ border-radius: 15px;
+ border: 1px solid rgba(0, 0, 0, 0);
+ background: rgba(0, 0, 0, .05);
+ margin: 5px;
+ color: rgba(0, 0, 0, .2);
+ font-weight: 500;
+ }
+
+ .mat-button-toggle-checked {
+ border-radius: 15px;
+ border: 1px solid #305680;
+ background: white;
+ color: #305680;
+ }
+
+ ::ng-deep span {
+ text-transform: capitalize;
+ line-height: 20px;
+ font-size: 13px;
+ padding: 5px 25px;
+ }
+
+ ::ng-deep .mat-button-toggle-ripple {
+ border-radius: 15px;
+ }
+
+ ::ng-deep .mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay {
+ opacity: 0 !important;
+ }
+ }
+
+ .logs-label {
+ font-weight: 500;
+ margin-bottom: 10px;
+ }
+
+ .logs-toggle-group {
+ border: none;
+ border-radius: 15px;
+ background: rgba(0, 0, 0, .05);
+ color: rgba(0, 0, 0, .2);
+ font-weight: 500;
+
+ .mat-button-toggle {
+ border: none;
+ background: rgba(0, 0, 0, .0);
+ color: rgba(0, 0, 0, .2);
+ font-weight: 500;
+ }
+
+ .mat-button-toggle-checked {
+ border-radius: 15px;
+ color: white;
+ background: #305680;
+ }
+
+ ::ng-deep span {
+ line-height: 20px;
+ font-size: 13px;
+ padding: 5px 25px;
+ }
+
+ ::ng-deep .mat-button-toggle-ripple {
+ border-radius: 15px;
+ }
+
+ ::ng-deep .mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay {
+ opacity: 0 !important;
+ }
+ }
+
+ .first-capital {
+ text-transform: capitalize;
+ }
+
+ ::ng-deep.mat-tab-label-active {
+ color: white;
+ opacity: 1;
+ }
+
+ ::ng-deep.mat-tab-label, ::ng-deep.mat-tab-label-active {
+ min-width: 50px !important;
+ padding: 3px !important;
+ margin: 3px !important;
+ flex-grow: 1;
+ }
+
+ ::ng-deep.mat-ink-bar {
+ height: 100%;
+ z-index: -10;
+ border-radius: 5px;
+ }
+}
+
diff --git a/ui-ngx/src/app/shared/components/device/gateway-configuration.component.ts b/ui-ngx/src/app/shared/components/device/gateway-configuration.component.ts
new file mode 100644
index 0000000000..2fd02f60b7
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/device/gateway-configuration.component.ts
@@ -0,0 +1,521 @@
+///
+/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
+///
+/// Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
+///
+/// NOTICE: All information contained herein is, and remains
+/// the property of ThingsBoard, Inc. and its suppliers,
+/// if any. The intellectual and technical concepts contained
+/// herein are proprietary to ThingsBoard, Inc.
+/// and its suppliers and may be covered by U.S. and Foreign Patents,
+/// patents in process, and are protected by trade secret or copyright law.
+///
+/// Dissemination of this information or reproduction of this material is strictly forbidden
+/// unless prior written permission is obtained from COMPANY.
+///
+/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
+/// managers or contractors who have executed Confidentiality and Non-disclosure agreements
+/// explicitly covering such access.
+///
+/// The copyright notice above does not evidence any actual or intended publication
+/// or disclosure of this source code, which includes
+/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
+/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
+/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
+/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
+/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
+/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
+/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
+/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
+///
+
+import { Component, Input, OnInit } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { Router } from '@angular/router';
+import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
+import { EntityId } from '@shared/models/id/entity-id';
+import { MatDialog, MatDialogRef } from '@angular/material/dialog';
+import { base64toObj, base64toString, objToBase64, stringToBase64 } from '@core/utils';
+import { AttributeService } from '@core/http/attribute.service';
+import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
+import {
+ GatewayRemoteConfigurationDialogComponent,
+ GatewayRemoteConfigurationDialogData
+} from '@shared/components/dialog/gateway-remote-configuration-dialog';
+import { DeviceService } from '@core/http/device.service';
+
+export enum StorageTypes {
+ MEMORY = 'memory',
+ FILE = 'file',
+ SQLITE = 'sqlite'
+}
+
+export enum GatewayLogLevel {
+ none = 'NONE',
+ critical = 'CRITICAL',
+ error = 'ERROR',
+ warning = 'WARNING',
+ info = 'INFO',
+ debug = 'DEBUG'
+}
+
+export enum LogSavingPeriod {
+ days = 'd',
+ hours = 'h',
+ minutes = 'm',
+ seconds = 's'
+}
+
+export enum LocalLogsConfigs {
+ service = 'service',
+ connector = 'connector',
+ converter = 'converter',
+ tb_connection = 'tb_connection',
+ storage = 'storage',
+ extension = 'extension'
+}
+
+export const logSavingPeriodTranslations = new Map(
+ [
+ [LogSavingPeriod.days, 'gateway.logs.days'],
+ [LogSavingPeriod.hours, 'gateway.logs.hours'],
+ [LogSavingPeriod.minutes, 'gateway.logs.minutes'],
+ [LogSavingPeriod.seconds, 'gateway.logs.seconds']
+ ]
+);
+
+export const storageTypesTranslations = new Map(
+ [
+ [StorageTypes.MEMORY, 'gateway.storage-types.memory-storage'],
+ [StorageTypes.FILE, 'gateway.storage-types.file-storage'],
+ [StorageTypes.SQLITE, 'gateway.storage-types.sqlite']
+ ]
+);
+
+export enum SecurityTypes {
+ ACCESS_TOKEN = 'accessToken',
+ USERNAME_PASSWORD = 'usernamePassword',
+ TLS_ACCESS_TOKEN = 'tlsAccessToken',
+ TLS_PRIVATE_KEY = 'tlsPrivateKey'
+}
+
+export const securityTypesTranslationsMap = new Map(
+ [
+ [SecurityTypes.ACCESS_TOKEN, 'gateway.security-types.access-token'],
+ [SecurityTypes.USERNAME_PASSWORD, 'gateway.security-types.username-password'],
+ [SecurityTypes.TLS_ACCESS_TOKEN, 'gateway.security-types.tls-access-token'],
+ [SecurityTypes.TLS_PRIVATE_KEY, 'gateway.security-types.tls-private-key'],
+ ]
+);
+
+@Component({
+ selector: 'tb-gateway-configuration',
+ templateUrl: './gateway-configuration.component.html',
+ styleUrls: ['./gateway-configuration.component.scss']
+})
+export class GatewayConfigurationComponent implements OnInit {
+
+ gatewayConfigGroup: FormGroup;
+
+ storageTypes = storageTypesTranslations;
+
+ logSavingPeriods = logSavingPeriodTranslations;
+
+ securityTypes = securityTypesTranslationsMap;
+
+ gatewayLogLevel = Object.values(GatewayLogLevel);
+
+ localLogsConfigs = Object.values(LocalLogsConfigs);
+
+ @Input()
+ device: EntityId;
+
+ @Input()
+ dialogRef: MatDialogRef;
+
+ logSelector: FormControl;
+
+
+ constructor(protected router: Router,
+ protected store: Store,
+ protected fb: FormBuilder,
+ protected attributeService: AttributeService,
+ protected deviceService: DeviceService,
+ public dialog: MatDialog) {
+ }
+
+ ngOnInit() {
+ this.gatewayConfigGroup = this.fb.group({
+ thingsboard: this.fb.group({
+ host: ['thingsboard.cloud', [Validators.required]],
+ port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
+ remoteShell: [false, []],
+ remoteConfiguration: [true, []],
+ statistics: this.fb.group({
+ enable: [true, []],
+ statsSendPeriodInSeconds: [3600, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]],
+ checkConnectorsConfigurationInSeconds: [60, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
+ handleDeviceRenaming: [false, []],
+ commands: this.fb.array([], [])
+ }),
+ maxPayloadSizeBytes: [1024, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
+ minPackSendDelayMS: [200, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]],
+ minPackSizeToSend: [500, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
+ handleDeviceRenaming: [true, []],
+ checkingDeviceActivity: this.fb.group({
+ checkDeviceInactivity: [false, []],
+ inactivityTimeoutSeconds: [200, [Validators.min(1)]],
+ inactivityCheckPeriodSeconds: [500, [Validators.min(1)]]
+ }),
+ security: this.fb.group({
+ type: [SecurityTypes.ACCESS_TOKEN, [Validators.required]],
+ accessToken: [null, [Validators.required]],
+ clientId: [null, []],
+ username: [null, []],
+ password: [null, []],
+ caCert: [null, []],
+ cert: [null, []],
+ privateKey: [null, []],
+ }),
+ qos: [1, [Validators.min(0), Validators.max(2), Validators.required]]
+ }),
+ storage: this.fb.group({
+ type: [StorageTypes.MEMORY, [Validators.required]],
+ read_records_count: [100, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]],
+ max_records_count: [100000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]],
+ data_folder_path: ['./data/', []],
+ max_file_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
+ max_read_records_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
+ max_records_per_file: [10000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
+ data_file_path: ['./data/data.db', []],
+ messages_ttl_check_in_hours: [1, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
+ messages_ttl_in_days: [7, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
+
+ }),
+ grpc: this.fb.group({
+ enabled: [false, []],
+ serverPort: [9595, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
+ keepAliveTimeMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
+ keepAliveTimeoutMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
+ keepalivePermitWithoutCalls: [true, []],
+ maxPingsWithoutData: [0, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]],
+ minTimeBetweenPingsMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
+ minPingIntervalWithoutDataMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
+ }),
+ connectors: this.fb.array([]),
+ logs: this.fb.group({
+ dateFormat: ['%Y-%m-%d %H:%M:%S', [Validators.required]],
+ logFormat: ['%(asctime)s - |%(levelname)s| - [%(filename)s] - %(module)s - %(funcName)s - %(lineno)d - %(message)s',
+ [Validators.required]],
+ type: ['remote', [Validators.required]],
+ remote: this.fb.group({
+ enabled: [false],
+ logLevel: [GatewayLogLevel.info, [Validators.required]],
+ }),
+ local: this.fb.group({})
+ })
+ });
+
+ this.gatewayConfigGroup.get('thingsboard.remoteConfiguration').valueChanges.subscribe(enabled => {
+ if (!enabled) {
+ this.openConfigurationConfirmDialog();
+ }
+ });
+
+ this.logSelector = this.fb.control(LocalLogsConfigs.service);
+
+ for (const localLogsConfigsKey of Object.keys(LocalLogsConfigs)) {
+ this.addLocalLogConfig(localLogsConfigsKey, {});
+ }
+
+ const checkingDeviceActivityGroup = this.gatewayConfigGroup.get('thingsboard.checkingDeviceActivity') as FormGroup;
+ checkingDeviceActivityGroup.get('checkDeviceInactivity').valueChanges.subscribe(enabled => {
+ checkingDeviceActivityGroup.updateValueAndValidity();
+ if (enabled) {
+ checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setValidators([Validators.min(1), Validators.required]);
+ checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setValidators([Validators.min(1), Validators.required]);
+ } else {
+ checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').clearValidators();
+ checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setErrors(null);
+ checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').clearValidators();
+ checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setErrors(null);
+ }
+ });
+
+ const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup;
+ securityGroup.get('type').valueChanges.subscribe(type => {
+ this.removeAllSecurityValidators();
+ if (type === SecurityTypes.ACCESS_TOKEN) {
+ securityGroup.get('accessToken').addValidators([Validators.required]);
+ securityGroup.get('accessToken').updateValueAndValidity();
+ } else if (type === SecurityTypes.TLS_PRIVATE_KEY) {
+ securityGroup.get('caCert').addValidators([Validators.required]);
+ securityGroup.get('caCert').updateValueAndValidity();
+ securityGroup.get('privateKey').addValidators([Validators.required]);
+ securityGroup.get('privateKey').updateValueAndValidity();
+ securityGroup.get('cert').addValidators([Validators.required]);
+ securityGroup.get('cert').updateValueAndValidity();
+ } else if (type === SecurityTypes.TLS_ACCESS_TOKEN) {
+ securityGroup.get('accessToken').addValidators([Validators.required]);
+ securityGroup.get('accessToken').updateValueAndValidity();
+ securityGroup.get('caCert').addValidators([Validators.required]);
+ securityGroup.get('caCert').updateValueAndValidity();
+ } else if (type === SecurityTypes.USERNAME_PASSWORD) {
+ securityGroup.get('clientId').addValidators([Validators.required]);
+ securityGroup.get('clientId').updateValueAndValidity();
+ securityGroup.get('username').addValidators([Validators.required]);
+ securityGroup.get('username').updateValueAndValidity();
+ securityGroup.get('password').addValidators([Validators.required]);
+ securityGroup.get('password').updateValueAndValidity();
+ }
+ securityGroup.updateValueAndValidity();
+ });
+
+ const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup;
+ storageGroup.get('type').valueChanges.subscribe(type => {
+ this.removeAllStorageValidators();
+ if (type === StorageTypes.MEMORY) {
+ storageGroup.get('read_records_count').addValidators(
+ [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
+ storageGroup.get('max_records_count').addValidators(
+ [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
+ } else if (type === StorageTypes.FILE) {
+ storageGroup.get('data_folder_path').addValidators([Validators.required]);
+ storageGroup.get('max_file_count').addValidators(
+ [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
+ storageGroup.get('max_read_records_count').addValidators(
+ [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
+ storageGroup.get('max_records_per_file').addValidators(
+ [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
+ } else if (type === StorageTypes.SQLITE) {
+ storageGroup.get('data_file_path').addValidators([Validators.required]);
+ storageGroup.get('messages_ttl_check_in_hours').addValidators(
+ [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
+ storageGroup.get('messages_ttl_in_days').addValidators(
+ [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
+ }
+ });
+
+ this.fetchConfigAttribute(this.device);
+ }
+
+ fetchConfigAttribute(entityId: EntityId) {
+ this.attributeService.getEntityAttributes(entityId, AttributeScope.SHARED_SCOPE,
+ ['configuration', 'RemoteLoggingLevel']).subscribe(attributes => {
+ if (attributes.length) {
+ const configuration = attributes.find(attribute => attribute.key === 'configuration').value;
+ const remoteLoggingLevel = attributes.find(attribute => attribute.key === 'RemoteLoggingLevel').value;
+ if (configuration) {
+ const configObj = base64toObj(configuration).thingsboard;
+ configObj.logs = this.logsBase64ToObj(configObj.logs);
+ if (configObj.thingsboard.statistics && configObj.thingsboard.statistics.commands) {
+ for (const command of Object.values(configObj.thingsboard.statistics.commands)) {
+ this.addCommand(command);
+ }
+ delete configObj.thingsboard.statistics.commands;
+ }
+ this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false});
+ this.gatewayConfigGroup.markAsDirty();
+ if (!configObj.thingsboard.remoteConfiguration) {
+ this.gatewayConfigGroup.disable({emitEvent: false});
+ }
+ }
+ if (remoteLoggingLevel) {
+ const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote');
+ remoteLogsFormGroup.patchValue({
+ enabled: remoteLoggingLevel !== GatewayLogLevel.none,
+ logLevel: remoteLoggingLevel
+ }, {emitEvent: false});
+ remoteLogsFormGroup.markAsDirty();
+ }
+ }
+ });
+ }
+
+ logsBase64ToObj(logs64: string) {
+ const logsString = base64toString(logs64).split('"').join('');
+ const logsObject = {};
+ logsString.split('logger_').forEach((split, index) => {
+ if (index > 0) {
+ const key = split.split(']')[0];
+ if (LocalLogsConfigs[key]) {
+ const logLevel = split.split('=')[1].replace('}}handlers', '');
+ logsObject[key] = {
+ logLevel
+ };
+ }
+ }
+ });
+ logsString.split('handler_').forEach((split, index) => {
+ if (index > 0) {
+ const key = split.split('Handler]')[0];
+ if (LocalLogsConfigs[key]) {
+ const args = split.split('args=(')[1].split(',');
+ logsObject[key].filePath = args[0].replace(`/${key}.log`, '');
+ logsObject[key].savingPeriod = args[1].replace(' ', '');
+ logsObject[key].savingTime = +args[2];
+ logsObject[key].backupCount = +args[3];
+ }
+ }
+ });
+ const formatArgs = logsString.split('format=')[1].split('}}datefmt=');
+ const logFormat = formatArgs[0];
+ const dateFormat = formatArgs[1];
+
+ return {local: logsObject, logFormat, dateFormat};
+ }
+
+ addCommand(command?): void {
+ const data = command || {};
+ const commandsFormArray = this.commandFormArray();
+ const commandFormGroup = this.fb.group({
+ attributeName: [data.attributeName || null, [Validators.required]],
+ command: [data.command || null, [Validators.required]],
+ timeout: [data.timeout || null, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
+ });
+ commandsFormArray.push(commandFormGroup);
+ }
+
+ addLocalLogConfig(name, config): void {
+ const localLogsFormGroup = this.gatewayConfigGroup.get('logs.local') as FormGroup;
+ const configGroup = this.fb.group({
+ logLevel: [config.logLevel || GatewayLogLevel.info, [Validators.required]],
+ filePath: [config.filePath || './data', [Validators.required]],
+ backupCount: [config.backupCount || 7, [Validators.required, Validators.min(0)]],
+ savingTime: [config.savingTime || 3, [Validators.required, Validators.min(0)]],
+ savingPeriod: [config.savingPeriod || LogSavingPeriod.days, [Validators.required]]
+ });
+ localLogsFormGroup.addControl(name, configGroup);
+ }
+
+ getLogFormGroup(value: string): FormGroup {
+ return this.gatewayConfigGroup.get(`logs.local.${value}`) as FormGroup;
+ }
+
+ commandFormArray(): FormArray {
+ return this.gatewayConfigGroup.get('thingsboard.statistics.commands') as FormArray;
+ }
+
+ removeCommandControl(index: number): void {
+ this.commandFormArray().removeAt(index);
+ }
+
+ removeAllSecurityValidators(): void {
+ const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup;
+ for (const controlsKey in securityGroup.controls) {
+ if (controlsKey !== 'type') {
+ securityGroup.controls[controlsKey].clearValidators();
+ securityGroup.controls[controlsKey].setErrors(null);
+ securityGroup.controls[controlsKey].updateValueAndValidity();
+ }
+ }
+ }
+
+ removeAllStorageValidators(): void {
+ const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup;
+ for (const storageKey in storageGroup.controls) {
+ if (storageKey !== 'type') {
+ storageGroup.controls[storageKey].clearValidators();
+ storageGroup.controls[storageKey].setErrors(null);
+ storageGroup.controls[storageKey].updateValueAndValidity();
+ }
+ }
+ }
+
+ removeEmpty(obj) {
+ return Object.fromEntries(
+ Object.entries(obj)
+ .filter(([_, v]) => v != null)
+ .map(([k, v]) => [k, v === Object(v) ? this.removeEmpty(v) : v])
+ );
+ }
+
+ generateLogsFile(logsObj): string {
+ const line = '[loggers]}}' +
+ 'keys=root, service, connector, converter, tb_connection, storage, extension}}' +
+ '[handlers]}}' +
+ 'keys=consoleHandler, serviceHandler, connectorHandler, converterHandler, tb_connectionHandler, storageHandler, extensionHandler}}' +
+ '[formatters]}}' +
+ 'keys=LogFormatter}}' +
+ '[logger_root]}}' +
+ 'level=ERROR}}' +
+ 'handlers=consoleHandler}}' +
+ '[logger_database]}}' +
+ 'level=INFO}}' +
+ 'handlers=databaseHandler}}' +
+ 'formatter=LogFormatter}}' +
+ 'qualname=database}}';
+
+ let loggerLine = '';
+ let handlerLine = '';
+
+ for (const key of Object.keys(logsObj.local)) {
+ loggerLine += `[logger_${key}]}}` +
+ `level=${logsObj.local[key].logLevel}}}` +
+ `handlers=${key}Handler}}` +
+ `formatter=LogFormatter}}` +
+ `qualname=${key}}}`;
+ handlerLine += `[handler_${key}Handler]}}` +
+ `level=${logsObj.local[key].logLevel}}}` +
+ `class=thingsboard_gateway.tb_utility.tb_logger.TimedRotatingFileHandler}}` +
+ `formatter=LogFormatter}}` +
+ `args=("${logsObj.local[key].filePath}/${key}.log", ${logsObj.local[key].savingPeriod},` +
+ ` ${logsObj.local[key].savingTime}, ${logsObj.local[key].backupCount},)}}`;
+ }
+
+ const logerEnding = `[formatter_LogFormatter]}}` +
+ `format="${logsObj.logFormat}"}}` +
+ `datefmt="${logsObj.dateFormat}"`;
+
+ return line + loggerLine + handlerLine + logerEnding;
+ }
+
+ saveConfig(): void {
+ const value = this.removeEmpty(this.gatewayConfigGroup.value);
+ const attributes = [];
+ attributes.push({
+ key: 'RemoteLoggingLevel',
+ value: value.logs.remote.logLevel
+ });
+ delete value.connectors;
+ value.logs = stringToBase64(this.generateLogsFile(value.logs));
+ const configuration = objToBase64({thingsboard: value});
+ attributes.push({
+ key: 'configuration',
+ value: configuration
+ });
+ this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => {
+ if (this.dialogRef) {
+ this.dialogRef.close();
+ }
+ });
+ }
+
+ cancel(): void {
+ if (this.dialogRef) {
+ this.dialogRef.close();
+ }
+ }
+
+ private openConfigurationConfirmDialog(): void {
+ this.deviceService.getDevice(this.device.id).subscribe(gateway => {
+ this.dialog.open
+ (GatewayRemoteConfigurationDialogComponent, {
+ disableClose: true,
+ panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
+ data: {
+ gatewayName: gateway.name
+ }
+ }).afterClosed().subscribe(
+ (res) => {
+ if (!res) {
+ this.gatewayConfigGroup.get('thingsboard.remoteConfiguration').setValue(true, {emitEvent: false});
+ }
+ }
+ );
+ });
+
+
+ }
+}
diff --git a/ui-ngx/src/app/shared/components/device/gateway-connectors.component.html b/ui-ngx/src/app/shared/components/device/gateway-connectors.component.html
new file mode 100644
index 0000000000..f14ae4294d
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/device/gateway-connectors.component.html
@@ -0,0 +1,143 @@
+
+
+
+ gateway.connectors
+
+
+ close
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'gateway.connector-add' | translate }}
+
+
+
+
+
+
+
+ {{ 'action.cancel' | translate }}
+
+
+ {{ 'action.save' | translate }}
+
+
+
+
diff --git a/ui-ngx/src/app/shared/components/device/gateway-connectors.component.scss b/ui-ngx/src/app/shared/components/device/gateway-connectors.component.scss
new file mode 100644
index 0000000000..9f683170c2
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/device/gateway-connectors.component.scss
@@ -0,0 +1,73 @@
+/**
+ * ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
+ *
+ * Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
+ *
+ * NOTICE: All information contained herein is, and remains
+ * the property of ThingsBoard, Inc. and its suppliers,
+ * if any. The intellectual and technical concepts contained
+ * herein are proprietary to ThingsBoard, Inc.
+ * and its suppliers and may be covered by U.S. and Foreign Patents,
+ * patents in process, and are protected by trade secret or copyright law.
+ *
+ * Dissemination of this information or reproduction of this material is strictly forbidden
+ * unless prior written permission is obtained from COMPANY.
+ *
+ * Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
+ * managers or contractors who have executed Confidentiality and Non-disclosure agreements
+ * explicitly covering such access.
+ *
+ * The copyright notice above does not evidence any actual or intended publication
+ * or disclosure of this source code, which includes
+ * information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
+ * ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
+ * OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
+ * THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
+ * AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
+ * THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
+ * DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
+ * OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
+ */
+:host {
+ width: 100%;
+ height: 100%;
+ display: block;
+
+ .mat-toolbar {
+ background: transparent;
+ color: rgba(0, 0, 0, .87) !important;
+ }
+
+ .mat-mdc-form-field {
+ margin-right: 15px;
+ }
+
+ mat-expansion-panel {
+ border: 1px solid #305680;
+ }
+
+ mat-card {
+ padding-left: 10px;
+ }
+
+ .mat-card-selected {
+ background-color: white;
+ }
+
+ .mat-mdc-slide-toggle {
+ margin: 15px;
+ }
+
+ ::ng-deep tb-json-object-edit > div {
+ flex-grow: 1;
+ }
+
+ .mat-accordion .mat-expansion-panel {
+ border-radius: 5px;
+ }
+
+ .action-btns {
+ margin: 10px 10px 0;
+ }
+}
+
diff --git a/ui-ngx/src/app/shared/components/device/gateway-connectors.component.ts b/ui-ngx/src/app/shared/components/device/gateway-connectors.component.ts
new file mode 100644
index 0000000000..0abf9ede4a
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/device/gateway-connectors.component.ts
@@ -0,0 +1,195 @@
+///
+/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
+///
+/// Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
+///
+/// NOTICE: All information contained herein is, and remains
+/// the property of ThingsBoard, Inc. and its suppliers,
+/// if any. The intellectual and technical concepts contained
+/// herein are proprietary to ThingsBoard, Inc.
+/// and its suppliers and may be covered by U.S. and Foreign Patents,
+/// patents in process, and are protected by trade secret or copyright law.
+///
+/// Dissemination of this information or reproduction of this material is strictly forbidden
+/// unless prior written permission is obtained from COMPANY.
+///
+/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
+/// managers or contractors who have executed Confidentiality and Non-disclosure agreements
+/// explicitly covering such access.
+///
+/// The copyright notice above does not evidence any actual or intended publication
+/// or disclosure of this source code, which includes
+/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
+/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
+/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
+/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
+/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
+/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
+/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
+/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
+///
+
+import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { Router } from '@angular/router';
+import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
+import { EntityId } from '@shared/models/id/entity-id';
+import { MatDialog, MatDialogRef } from '@angular/material/dialog';
+import { AttributeService } from '@core/http/attribute.service';
+import { DeviceService } from '@core/http/device.service';
+import { TranslateService } from '@ngx-translate/core';
+import { forkJoin } from 'rxjs';
+import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
+
+
+export interface gatewayConnector {
+ name: string;
+ type: string;
+ configuration: string;
+ jsonConfiguration: string;
+ key: string;
+}
+
+@Component({
+ selector: 'tb-gateway-connector',
+ templateUrl: './gateway-connectors.component.html',
+ styleUrls: ['./gateway-connectors.component.scss']
+})
+export class GatewayConnectorComponent implements OnInit {
+
+ gatewayConnectorsGroup: FormGroup;
+
+ gatewayConnectorDefaultTypes: Array =
+ ['mqtt',
+ 'modbus',
+ 'grpc',
+ 'opcua',
+ 'opcua_asyncio',
+ 'ble',
+ 'request',
+ 'can',
+ 'bacnet',
+ 'odbc',
+ 'rest',
+ 'snmp',
+ 'ftp',
+ 'socket',
+ 'xmpp',
+ 'ocpp'
+ ];
+
+ selectedConnector: number;
+
+ @Input()
+ device: EntityId;
+
+ @Input()
+ dialogRef: MatDialogRef;
+
+ logSelector: FormControl;
+
+
+ constructor(protected router: Router,
+ protected store: Store,
+ protected fb: FormBuilder,
+ protected translate: TranslateService,
+ protected attributeService: AttributeService,
+ protected deviceService: DeviceService,
+ private cd: ChangeDetectorRef,
+ public dialog: MatDialog) {
+ }
+
+ ngOnInit() {
+ this.gatewayConnectorsGroup = this.fb.group({
+ connectors: this.fb.array([], [Validators.required])
+ });
+ this.getConnectorsData();
+ }
+
+ cancel(): void {
+ if (this.dialogRef) {
+ this.dialogRef.close();
+ }
+ }
+
+ getConnectorsData(): void {
+ forkJoin([
+ this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE,['implementedConnectors']),
+ this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE,['implementedConnectors']),
+ this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE,['connectorTypes'])
+ ]).subscribe(attributes=>{
+ if (attributes[0].length) {
+ attributes[0][0].value.forEach(connector=>this.addConnector(true, connector));
+ }
+ if (attributes[1].length) {
+ attributes[1][0].value.forEach(connector=>this.addConnector(false, connector));
+ }
+ if(attributes[2].length) {
+ attributes[1][0].value.forEach(type=> {
+ if (this.gatewayConnectorDefaultTypes.indexOf(type) === -1) {
+ this.gatewayConnectorDefaultTypes.push(type);
+ }
+ });
+ }
+ this.cd.detectChanges();
+ });
+ }
+
+ saveConnectors(): void {
+ const connectors = this.gatewayConnectorsGroup.value.connectors;
+ const activeConnectors = connectors.filter(connector=>connector.active);
+ const inactiveConnectors = connectors.filter(connector=>!connector.active);
+ forkJoin([
+ this.attributeService.saveEntityAttributes(this.device, AttributeScope.SERVER_SCOPE,
+ [{key: 'implementedConnectors', value: inactiveConnectors}]),
+ this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE,
+ [{key: 'implementedConnectors', value: activeConnectors}])
+ ]).subscribe(_=> {
+ if (this.dialogRef) {
+ this.dialogRef.close();
+ }
+ });
+ }
+
+ connectorsFormArray(): FormArray {
+ return this.gatewayConnectorsGroup.get('connectors') as FormArray;
+ }
+
+ addConnector(active?: boolean,connector?: gatewayConnector) {
+ const newConnector = this.fb.group({
+ active: [!!active],
+ name: [connector?.name || '', [Validators.required]],
+ type: [connector?.type || '', [Validators.required]],
+ configuration: [connector?.configuration || '', [Validators.required]],
+ jsonConfiguration: [connector?.jsonConfiguration || {}, [Validators.required]],
+ key: [connector?.key || 'auto', [Validators.required]],
+ });
+ const connectorsFormArray = this.connectorsFormArray();
+ connectorsFormArray.push(newConnector);
+ this.selectConnector(connectorsFormArray.length-1);
+ }
+
+ removeConnector(index: number): void {
+ this.connectorsFormArray().removeAt(index);
+ }
+
+ selectConnector(index: number): void {
+ this.selectedConnector = index;
+ }
+
+ getJsonControl(selectedConnector: number): FormControl {
+ return this.connectorsFormArray().at(selectedConnector).get('jsonConfiguration') as FormControl;
+ }
+
+ generatePanelTitle(connectorControl): string{
+ const connectorValues = connectorControl.value;
+ const activeTxt = this.translate.instant(connectorValues.active ? 'gateway.connectors-active' : 'gateway.connectors-inactive');
+ let title = `${connectorValues.name} | ${connectorValues.type} `;
+ if (connectorValues.type === 'grpc') {
+ title += `| ${connectorValues.key} `;
+ }
+ title += `| ${activeTxt}`;
+ return title;
+ }
+}
diff --git a/ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.html b/ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.html
new file mode 100644
index 0000000000..cc7bb69f6b
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.html
@@ -0,0 +1,72 @@
+
+
+
+ warning
+ gateway.configuration-delete-dialog-header
+
+
+ close
+
+
+
+
+
+
+ gateway.configuration-delete-dialog-input
+
+
+ {{'gateway.configuration-delete-dialog-input-required' | translate }}
+
+
+
+
+
+
+ {{ 'action.cancel' | translate }}
+
+
+ {{ 'gateway.configuration-delete-dialog-confirm' | translate }}
+
+
+
diff --git a/ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.ts b/ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.ts
new file mode 100644
index 0000000000..086e12a68a
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.ts
@@ -0,0 +1,76 @@
+///
+/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
+///
+/// Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
+///
+/// NOTICE: All information contained herein is, and remains
+/// the property of ThingsBoard, Inc. and its suppliers,
+/// if any. The intellectual and technical concepts contained
+/// herein are proprietary to ThingsBoard, Inc.
+/// and its suppliers and may be covered by U.S. and Foreign Patents,
+/// patents in process, and are protected by trade secret or copyright law.
+///
+/// Dissemination of this information or reproduction of this material is strictly forbidden
+/// unless prior written permission is obtained from COMPANY.
+///
+/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
+/// managers or contractors who have executed Confidentiality and Non-disclosure agreements
+/// explicitly covering such access.
+///
+/// The copyright notice above does not evidence any actual or intended publication
+/// or disclosure of this source code, which includes
+/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
+/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
+/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
+/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
+/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
+/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
+/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
+/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
+///
+
+import { Component, Inject, OnInit } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { DialogComponent } from '@shared/components/dialog.component';
+import { Router } from '@angular/router';
+import { FormBuilder, FormControl } from '@angular/forms';
+
+export interface GatewayRemoteConfigurationDialogData {
+ gatewayName: string;
+}
+
+@Component({
+ selector: 'tb-activation-link-dialog',
+ templateUrl: './gateway-remote-configuration-dialog.html'
+})
+
+export class GatewayRemoteConfigurationDialogComponent extends DialogComponent implements OnInit {
+
+ gatewayName: string;
+
+ gatewayForm: FormControl;
+
+ constructor(protected store: Store,
+ protected router: Router,
+ @Inject(MAT_DIALOG_DATA) public data: GatewayRemoteConfigurationDialogData,
+ public dialogRef: MatDialogRef,
+ private fb: FormBuilder) {
+ super(store, router, dialogRef);
+ this.gatewayName = this.data.gatewayName;
+ this.gatewayForm = this.fb.control(null);
+ }
+
+ ngOnInit(): void {
+ }
+
+ close(): void {
+ this.dialogRef.close();
+ }
+
+ turnOff(): void {
+ this.dialogRef.close(true);
+ }
+}
diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts
index c82ae61644..7b053007f1 100644
--- a/ui-ngx/src/app/shared/shared.module.ts
+++ b/ui-ngx/src/app/shared/shared.module.ts
@@ -176,10 +176,13 @@ import { NotificationComponent } from '@shared/components/notification/notificat
import { TemplateAutocompleteComponent } from '@shared/components/notification/template-autocomplete.component';
import { SlackConversationAutocompleteComponent } from '@shared/components/slack-conversation-autocomplete.component';
import { DateAgoPipe } from '@shared/pipe/date-ago.pipe';
+import { DeviceGatewayCommandComponent } from '@shared/components/device/device-gateway-command.component';
+import { GatewayConfigurationComponent } from '@shared/components/device/gateway-configuration.component';
+import { GatewayRemoteConfigurationDialogComponent } from '@shared/components/dialog/gateway-remote-configuration-dialog';
+import { GatewayConnectorComponent } from '@shared/components/device/gateway-connectors.component';
+
+export const MarkedOptionsFactory = (markedOptionsService: MarkedOptionsService) => markedOptionsService;
-export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
- return markedOptionsService;
-}
@NgModule({
providers: [
@@ -241,6 +244,10 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
HelpComponent,
HelpMarkdownComponent,
HelpPopupComponent,
+ DeviceGatewayCommandComponent,
+ GatewayConfigurationComponent,
+ GatewayConnectorComponent,
+ GatewayRemoteConfigurationDialogComponent,
TbCheckboxComponent,
TbSnackBarComponent,
TbErrorComponent,
@@ -407,6 +414,10 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
HelpComponent,
HelpMarkdownComponent,
HelpPopupComponent,
+ DeviceGatewayCommandComponent,
+ GatewayConfigurationComponent,
+ GatewayConnectorComponent,
+ GatewayRemoteConfigurationDialogComponent,
TbCheckboxComponent,
TbErrorComponent,
TbCheatSheetComponent,
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 d810da019c..e25d2667e6 100644
--- a/ui-ngx/src/assets/locale/locale.constant-en_US.json
+++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json
@@ -1186,6 +1186,7 @@
"device-details": "Device details",
"add-device-text": "Add new device",
"credentials": "Credentials",
+ "commands": "Commands",
"manage-credentials": "Manage credentials",
"delete": "Delete device",
"assign-devices": "Assign devices",
@@ -1278,6 +1279,7 @@
"copyId": "Copy device Id",
"copyAccessToken": "Copy access token",
"copy-mqtt-authentication": "Copy MQTT credentials",
+ "transportCommandCopiedMessage": "Transport Command has been copied to clipboard",
"idCopiedMessage": "Device Id has been copied to clipboard",
"accessTokenCopiedMessage": "Device access token has been copied to clipboard",
"mqtt-authentication-copied-message": "Device MQTT authentication has been copied to clipboard",
@@ -2466,44 +2468,164 @@
},
"gateway": {
"add-entry": "Add configuration",
+ "advanced": "Advanced",
+ "checking-device-activity": "Checking Device Activity:",
"command": "Docker commands",
"command-copied-message": "Docker command has been copied to clipboard",
+ "configuration": "Configuration",
+ "connector-json": "Connector JSON",
"connector-add": "Add new connector",
"connector-enabled": "Enable connector",
"connector-name": "Connector name",
"connector-name-required": "Connector name is required.",
+ "connector-key": "Connector key",
+ "connector-key-required": "Connector key is required.",
+ "connector-configuration": "Configuration file name",
+ "connector-configuration-required": "Configuration file name is required.",
"connector-type": "Connector type",
"connector-type-required": "Connector type is required.",
+ "connector-types": {
+ "mqtt": "MQTT Broker Connector",
+ "modbus": "Modbus Connector",
+ "modbus_serial": "Modbus Connector (serial)",
+ "opcua": "OPC-UA Connector",
+ "opcua_asyncio": "OPC-UA Connector (asyncio)",
+ "ble": "BLE Connector",
+ "request": "REQUEST Connector",
+ "can": "CAN Connector",
+ "bacnet": "BACnet Connector",
+ "odbc": "ODBC Connector",
+ "rest": "REST Connector",
+ "snmp": "SNMP Connector",
+ "ftp": "FTP Connector",
+ "socket": "Socket TCP/UDP Connector",
+ "xmpp": "XMPP Connector",
+ "ocpp": "OCPP Connector"
+ },
"connectors": "Connectors configuration",
+ "connectors-active": "Connector active",
+ "connectors-inactive": "Connector inactive",
+ "select-connector": "Select connector",
+ "gateway-configuration": "Gateway Configuration",
"copy-command": "Copy docker command",
"create-new-gateway": "Create a new gateway",
"create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?",
"created-time": "Created time",
+ "configuration-delete-dialog-header": "Configurations will be deleted",
+ "configuration-delete-dialog-body": "Turning off Remote Configuration is possible only if there is physical access to the Gateway. All previous configurations will be deleted. \nTo turn off configuration, enter gateway name below.",
+ "configuration-delete-dialog-input": "Gateway name",
+ "configuration-delete-dialog-input-required": "Gateway name is mandatory",
+ "configuration-delete-dialog-confirm": "Turn Off",
"delete": "Delete configuration",
"download-tip": "Download configuration file",
+ "drop-file": "Drop file here or",
"gateway": "Gateway",
"gateway-exists": "Device with same name is already exists.",
"gateway-name": "Gateway name",
"gateway-name-required": "Gateway name is required.",
"gateway-saved": "Gateway configuration successfully saved.",
"gateway-search": "Gateway search",
+ "grpc": "GRPC",
+ "grpc-keep-alive-timeout": "Keep alive timeout (in ms)",
+ "grpc-keep-alive-timeout-required": "Keep alive timeout is required",
+ "grpc-keep-alive-timeout-min": "Keep alive timeout can not be less then 1",
+ "grpc-keep-alive-timeout-pattern": "Keep alive timeout is not valid",
+ "grpc-keep-alive": "Keep alive (in ms)",
+ "grpc-keep-alive-required": "Keep alive is required",
+ "grpc-keep-alive-min": "Keep alive can not be less then 1",
+ "grpc-keep-alive-pattern": "Keep alive is not valid",
+ "grpc-min-time-between-pings": "Min time between pings (in ms)",
+ "grpc-min-time-between-pings-required": "Min time between pings is required",
+ "grpc-min-time-between-pings-min": "Min time between pings can not be less then 1",
+ "grpc-min-time-between-pings-pattern": "Min time between pings is not valid",
+ "grpc-min-ping-interval-without-data": "Min ping interval without data (in ms)",
+ "grpc-min-ping-interval-without-data-required": "Min ping interval without data is required",
+ "grpc-min-ping-interval-without-data-min": "Min ping interval without data can not be less then 1",
+ "grpc-min-ping-interval-without-data-pattern": "Min ping interval without data is not valid",
+ "grpc-max-pings-without-data": "Max pings without data",
+ "grpc-max-pings-without-data-required": "Max pings without data is required",
+ "grpc-max-pings-without-data-min": "Max pings without data can not be less then 1",
+ "grpc-max-pings-without-data-pattern": "Max pings without data is not valid",
+ "handle-device-renaming": "Handle device renaming",
+ "inactivity-check-period-seconds": "Inactivity check period (in sec)",
+ "inactivity-check-period-seconds-required": "Inactivity check period is required",
+ "inactivity-check-period-seconds-min": "Inactivity check period can not be less then 1",
+ "inactivity-timeout-seconds": "Inactivity timeout (in sec)",
+ "inactivity-timeout-seconds-required": "Inactivity timeout is required",
+ "inactivity-timeout-seconds-min": "Inactivity timeout can not be less then 1",
"json-parse": "Not valid JSON.",
"json-required": "Field cannot be empty.",
"linux-macos": "Linux/MacOS",
+ "logs": {
+ "logs": "Logs",
+ "days": "days",
+ "hours": "hours",
+ "minutes": "minutes",
+ "seconds": "seconds",
+ "date-format": "Date format",
+ "date-format-required": "Date format required",
+ "log-format": "Log format",
+ "log-type": "Log type",
+ "log-format-required": "Log format required",
+ "remote": "Remote logging",
+ "remote-logs": "Remote logs",
+ "local": "Local logging",
+ "level": "Log level",
+ "file-path": "File path",
+ "file-path-required": "File path required",
+ "saving-period": "Log saving period",
+ "saving-period-min": "Log saving period can not be less then 0",
+ "saving-period-required": "Log saving period required",
+ "backup-count": "Backup count",
+ "backup-count-min": "Backup count can not be less then 0",
+ "backup-count-required": "Backup count required"
+ },
+ "min-pack-send-delay": "Min pack send delay (in ms)",
+ "min-pack-send-delay-required": "Min pack send delay is required",
+ "min-pack-send-delay-min": "Min pack send delay can not be less then 1",
"no-connectors": "No connectors",
"no-data": "No configurations",
"no-gateway-found": "No gateway found.",
"no-gateway-matching": " '{{item}}' not found.",
"path-logs": "Path to log files",
"path-logs-required": "Path is required.",
+ "permit-without-calls": "Keep alive permit without calls",
"remote": "Remote configuration",
"remote-logging-level": "Logging level",
"remove-entry": "Remove configuration",
+ "remote-shell": "Remote shell",
+ "remote-configuration": "Remote Configuration",
+ "other": "Other",
"save-tip": "Save configuration file",
"security-type": "Security type",
"security-types": {
"access-token": "Access Token",
- "tls": "TLS"
+ "tls": "TLS",
+ "tls-access-token": "TLS + Access Token",
+ "tls-private-key": "TLS + Private Key"
+ },
+ "server-port": "Server port",
+ "stats-send-period-in-sec": "Stats send period in seconds",
+ "statistics": {
+ "statistics": "Statistics",
+ "send-period": "Statistic send period (in ms)",
+ "send-period-required": "Statistic send period is required",
+ "send-period-min": "Statistic send period can not be less then 0",
+ "send-period-pattern": "Statistic send period is not valid",
+ "check-connectors-configuration": "Check connectors configuration (in ms)",
+ "check-connectors-configuration-required": "Check connectors configuration is required",
+ "check-connectors-configuration-min": "Check connectors configuration can not be less then 0",
+ "check-connectors-configuration-pattern": "Check connectors configuration is not valid",
+ "add": "Add command",
+ "timeout": "Timeout",
+ "timeout-required": "Timeout is required",
+ "timeout-min": "Timeout can not be less then 0",
+ "timeout-pattern": "Timeout is not valid",
+ "attribute-name": "Attribute name",
+ "attribute-name-required": "Attribute name is required",
+ "command": "Command",
+ "command-required": "Command is required",
+ "remove": "Remove command"
},
"storage": "Storage",
"storage-max-file-records": "Maximum records in file",
@@ -2515,6 +2637,16 @@
"storage-max-records-min": "Minimum number of records is 1.",
"storage-max-records-pattern": "Number is not valid.",
"storage-max-records-required": "Maximum records is required.",
+ "storage-read-record-count": "Read record count in storage",
+ "storage-read-record-count-min": "Minimum number of records is 1.",
+ "storage-read-record-count-pattern": "Number is not valid.",
+ "storage-read-record-count-required": "Read record count is required.",
+ "storage-max-read-record-count": "Max read record count in storage",
+ "storage-max-read-record-count-min": "Minimum number of records is 1.",
+ "storage-max-read-record-count-pattern": "Number is not valid.",
+ "storage-max-read-record-count-required": "Max Read record count is required.",
+ "storage-data-folder-path": "Data folder path",
+ "storage-data-folder-path-required": "Data folder path is required.",
"storage-pack-size": "Maximum event pack size",
"storage-pack-size-min": "Minimum number is 1.",
"storage-pack-size-pattern": "Number is not valid.",
@@ -2524,9 +2656,11 @@
"storage-type": "Storage type",
"storage-types": {
"file-storage": "File storage",
- "memory-storage": "Memory storage"
+ "memory-storage": "Memory storage",
+ "sqlite": "SQLITE"
},
"thingsboard": "ThingsBoard",
+ "thingsboard-general": "General",
"thingsboard-host": "ThingsBoard host",
"thingsboard-host-required": "Host is required.",
"thingsboard-port": "ThingsBoard port",
@@ -2539,6 +2673,17 @@
"title-connectors-json": "Connector {{typeName}} configuration",
"tls-path-ca-certificate": "Path to CA certificate on gateway",
"tls-path-client-certificate": "Path to client certificate on gateway",
+ "messages-ttl-check-in-hours": "Messages TTL check in hours",
+ "messages-ttl-check-in-hours-required": "Messages TTL check in hours is required.",
+ "messages-ttl-check-in-hours-min": "Min number is 1.",
+ "messages-ttl-check-in-hours-pattern": "Number is not valid.",
+ "messages-ttl-in-days": "Messages TTL in days",
+ "messages-ttl-in-days-required": "Messages TTL in days is required.",
+ "messages-ttl-in-days-min": "Min number is 1.",
+ "messages-ttl-in-days-pattern": "Number is not valid.",
+ "mqtt-qos": "QoS",
+ "mqtt-qos-required": "QoS is required",
+ "mqtt-qos-range": "QoS values range is from 0 to 2",
"tls-path-private-key": "Path to private key on gateway",
"toggle-fullscreen": "Toggle fullscreen",
"transformer-json-config": "Configuration JSON*",
@@ -3065,6 +3210,20 @@
},
"security": {
"security": "Security",
+ "access-token": "Access token",
+ "access-token-required": "Access token is required",
+ "clientId": "Client ID",
+ "clientId-required": "Client ID is required",
+ "username": "Username",
+ "username-required": "Username is required",
+ "password": "Password",
+ "password-required": "Password is required",
+ "ca-cert": "CA certificate",
+ "ca-cert-required": "CA certificate is required",
+ "cert": "Certificate",
+ "cert-required": "Certificate is required",
+ "private-key": "Private Key",
+ "private-key-required": "Private Key is required",
"2fa": {
"2fa": "Two-factor authentication",
"2fa-description": "Two-factor authentication protects your account from unauthorized access. All you have to do is enter a security code when you log in.",