Browse Source

Merge remote-tracking branch 'upstream/master'

pull/585/head
Volodymyr Babak 9 years ago
parent
commit
787f9f942e
  1. 10
      application/pom.xml
  2. 2
      application/src/main/data/json/system/widget_bundles/alarm_widgets.json
  3. 2
      application/src/main/data/json/system/widget_bundles/cards.json
  4. 25
      application/src/main/data/json/system/widget_bundles/gateway_widgets.json
  5. 2
      application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
  6. 6
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  7. 21
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  8. 5
      application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
  9. 21
      application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java
  10. 2
      common/data/pom.xml
  11. 2
      common/message/pom.xml
  12. 2
      common/pom.xml
  13. 2
      common/transport/pom.xml
  14. 2
      dao/pom.xml
  15. 6
      dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
  16. 1
      dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java
  17. 20
      dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
  18. 2
      extensions-api/pom.xml
  19. 2
      extensions-core/pom.xml
  20. 8
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
  21. 2
      extensions/extension-kafka/pom.xml
  22. 2
      extensions/extension-mqtt/pom.xml
  23. 2
      extensions/extension-rabbitmq/pom.xml
  24. 2
      extensions/extension-rest-api-call/pom.xml
  25. 81
      extensions/extension-sns/pom.xml
  26. 37
      extensions/extension-sns/src/assembly/extension.xml
  27. 31
      extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionMsg.java
  28. 37
      extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionPayload.java
  29. 45
      extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginAction.java
  30. 30
      extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginActionConfiguration.java
  31. 63
      extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsMessageHandler.java
  32. 79
      extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPlugin.java
  33. 30
      extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPluginConfiguration.java
  34. 30
      extensions/extension-sns/src/main/resources/SnsPluginDescriptor.json
  35. 34
      extensions/extension-sns/src/main/resources/SnsTopicActionDescriptor.json
  36. 81
      extensions/extension-sqs/pom.xml
  37. 37
      extensions/extension-sqs/src/assembly/extension.xml
  38. 31
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionMsg.java
  39. 39
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionPayload.java
  40. 49
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginAction.java
  41. 30
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginActionConfiguration.java
  42. 31
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionMsg.java
  43. 39
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionPayload.java
  44. 46
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginAction.java
  45. 32
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginActionConfiguration.java
  46. 84
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsMessageHandler.java
  47. 78
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPlugin.java
  48. 30
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPluginConfiguration.java
  49. 34
      extensions/extension-sqs/src/main/resources/SqsFifoQueueActionDescriptor.json
  50. 30
      extensions/extension-sqs/src/main/resources/SqsPluginDescriptor.json
  51. 41
      extensions/extension-sqs/src/main/resources/SqsStandardQueueActionDescriptor.json
  52. 69
      extensions/extension-sqs/src/test/java/org/thingsboard/server/extensions/sqs/SqsDemoClient.java
  53. 10
      extensions/extension-sqs/src/test/resources/logback.xml
  54. 4
      extensions/pom.xml
  55. 12
      pom.xml
  56. 18
      resume.bat
  57. 2
      tools/pom.xml
  58. 38
      tools/src/main/java/org/thingsboard/client/tools/RestClient.java
  59. 36
      tools/src/main/python/mqtt-send-telemetry.py
  60. 2
      transport/coap/pom.xml
  61. 2
      transport/http/pom.xml
  62. 2
      transport/mqtt/pom.xml
  63. 2
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  64. 44
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
  65. 27
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
  66. 2
      transport/pom.xml
  67. 1
      ui/package.json
  68. 2
      ui/pom.xml
  69. 16
      ui/src/app/api/login.service.js
  70. 19
      ui/src/app/api/user.service.js
  71. 3
      ui/src/app/api/widget.service.js
  72. 3
      ui/src/app/app.js
  73. 2
      ui/src/app/asset/asset-card.tpl.html
  74. 4
      ui/src/app/asset/assign-to-customer.controller.js
  75. 18
      ui/src/app/common/dashboard-utils.service.js
  76. 47
      ui/src/app/common/types.constant.js
  77. 6
      ui/src/app/components/confirm-on-exit.directive.js
  78. 177
      ui/src/app/components/dashboard.directive.js
  79. 5
      ui/src/app/components/details-sidenav.scss
  80. 2
      ui/src/app/components/js-func.scss
  81. 2
      ui/src/app/components/js-func.tpl.html
  82. 4
      ui/src/app/device/assign-to-customer.controller.js
  83. 2
      ui/src/app/device/device-card.tpl.html
  84. 7
      ui/src/app/device/devices.tpl.html
  85. 4
      ui/src/app/entity/attribute/attribute-table.tpl.html
  86. 4
      ui/src/app/entity/relation/relation-dialog.controller.js
  87. 2
      ui/src/app/entity/relation/relation-dialog.scss
  88. 2
      ui/src/app/entity/relation/relation-dialog.tpl.html
  89. 298
      ui/src/app/extension/extension-dialog.controller.js
  90. 81
      ui/src/app/extension/extension-dialog.tpl.html
  91. 404
      ui/src/app/extension/extension-table.directive.js
  92. 47
      ui/src/app/extension/extension-table.scss
  93. 159
      ui/src/app/extension/extension-table.tpl.html
  94. 147
      ui/src/app/extension/extensions-forms/extension-form-http.directive.js
  95. 311
      ui/src/app/extension/extensions-forms/extension-form-http.tpl.html
  96. 347
      ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js
  97. 860
      ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html
  98. 173
      ui/src/app/extension/extensions-forms/extension-form-opc.directive.js
  99. 557
      ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
  100. 76
      ui/src/app/extension/extensions-forms/extension-form.scss

10
application/pom.xml

@ -496,6 +496,16 @@
<artifactId>extension-mqtt</artifactId> <artifactId>extension-mqtt</artifactId>
<classifier>extension</classifier> <classifier>extension</classifier>
</artifactItem> </artifactItem>
<artifactItem>
<groupId>org.thingsboard.extensions</groupId>
<artifactId>extension-sqs</artifactId>
<classifier>extension</classifier>
</artifactItem>
<artifactItem>
<groupId>org.thingsboard.extensions</groupId>
<artifactId>extension-sns</artifactId>
<classifier>extension</classifier>
</artifactItem>
</artifactItems> </artifactItems>
</configuration> </configuration>
</execution> </execution>

2
application/src/main/data/json/system/widget_bundles/alarm_widgets.json

@ -18,7 +18,7 @@
"controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('alarms-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('alarms-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AlarmTableSettings\",\n \"properties\": {\n \"alarmsTitle\": {\n \"title\": \"Alarms table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSelection\": {\n \"title\": \"Enable alarms selection\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSearch\": {\n \"title\": \"Enable alarms search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayDetails\": {\n \"title\": \"Display alarm details\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowAcknowledgment\": {\n \"title\": \"Allow alarms acknowledgment\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowClear\": {\n \"title\": \"Allow alarms clear\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"-createdTime\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"alarmsTitle\",\n \"enableSelection\",\n \"enableSearch\",\n \"displayDetails\",\n \"allowAcknowledgment\",\n \"allowClear\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AlarmTableSettings\",\n \"properties\": {\n \"alarmsTitle\": {\n \"title\": \"Alarms table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSelection\": {\n \"title\": \"Enable alarms selection\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSearch\": {\n \"title\": \"Enable alarms search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayDetails\": {\n \"title\": \"Display alarm details\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowAcknowledgment\": {\n \"title\": \"Allow alarms acknowledgment\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowClear\": {\n \"title\": \"Allow alarms clear\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"-createdTime\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"alarmsTitle\",\n \"enableSelection\",\n \"enableSearch\",\n \"displayDetails\",\n \"allowAcknowledgment\",\n \"allowClear\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, alarm, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, alarm, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\"},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"18px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5}" "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\"},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5}"
} }
} }
] ]

2
application/src/main/data/json/system/widget_bundles/cards.json

@ -34,7 +34,7 @@
"controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"18px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}]}" "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}]}"
} }
}, },
{ {

25
application/src/main/data/json/system/widget_bundles/gateway_widgets.json

@ -0,0 +1,25 @@
{
"widgetsBundle": {
"alias": "gateway_widgets",
"title": "Gateway widgets",
"image": null
},
"widgetTypes": [
{
"alias": "extension_configuration_widget",
"name": "Extensions table",
"descriptor": {
"type": "latest",
"sizeX": 9,
"sizeY": 6.5,
"resources": [],
"templateHtml": "<tb-extensions-table-widget \n ctx=\"ctx\">\n</tb-extensions-table-widget>",
"templateCss": "#container {\n overflow: auto;\n}",
"controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n}\n\nself.onResize = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"ExtensionTableSettings\",\n \"properties\": {\n \"extensionsTitle\": {\n \"title\": \"Extension table title\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"extensionsTitle\"\n ]\n}",
"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\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{},\"title\":\"Extensions table\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"18px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
}
}
]
}

2
application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java

@ -170,7 +170,7 @@ class RuleActorMessageProcessor extends ComponentMsgProcessor<RuleId> {
Optional<RuleToPluginMsg<?>> ruleToPluginMsgOptional = action.convert(ruleCtx, inMsg, inMsgMd); Optional<RuleToPluginMsg<?>> ruleToPluginMsgOptional = action.convert(ruleCtx, inMsg, inMsgMd);
if (ruleToPluginMsgOptional.isPresent()) { if (ruleToPluginMsgOptional.isPresent()) {
RuleToPluginMsg<?> ruleToPluginMsg = ruleToPluginMsgOptional.get(); RuleToPluginMsg<?> ruleToPluginMsg = ruleToPluginMsgOptional.get();
logger.debug("[{}] Device msg is converter to: {}", entityId, ruleToPluginMsg); logger.debug("[{}] Device msg is converted to: {}", entityId, ruleToPluginMsg);
context.parent().tell(new RuleToPluginMsgWrapper(pluginTenantId, pluginId, tenantId, entityId, ruleToPluginMsg), context.self()); context.parent().tell(new RuleToPluginMsgWrapper(pluginTenantId, pluginId, tenantId, entityId, ruleToPluginMsg), context.self());
if (action.isOneWayAction()) { if (action.isOneWayAction()) {
pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_TWO_WAY_ACTIONS); pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_TWO_WAY_ACTIONS);

6
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -28,6 +28,7 @@ import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
@ -147,6 +148,11 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();
} }
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/static/**");
}
@Override @Override
protected void configure(HttpSecurity http) throws Exception { protected void configure(HttpSecurity http) throws Exception {
http.headers().cacheControl().and().frameOptions().disable() http.headers().cacheControl().and().frameOptions().disable()

21
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -19,8 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -30,7 +28,6 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.exception.ThingsboardErrorCode; import org.thingsboard.server.exception.ThingsboardErrorCode;
import org.thingsboard.server.exception.ThingsboardException; import org.thingsboard.server.exception.ThingsboardException;
import org.thingsboard.server.service.mail.MailService; import org.thingsboard.server.service.mail.MailService;
@ -78,9 +75,10 @@ public class AuthController extends BaseController {
@RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST) @RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void changePassword ( public void changePassword (
@RequestParam(value = "currentPassword") String currentPassword, @RequestBody JsonNode changePasswordRequest) throws ThingsboardException {
@RequestParam(value = "newPassword") String newPassword) throws ThingsboardException {
try { try {
String currentPassword = changePasswordRequest.get("currentPassword").asText();
String newPassword = changePasswordRequest.get("newPassword").asText();
SecurityUser securityUser = getCurrentUser(); SecurityUser securityUser = getCurrentUser();
UserCredentials userCredentials = userService.findUserCredentialsByUserId(securityUser.getId()); UserCredentials userCredentials = userService.findUserCredentialsByUserId(securityUser.getId());
if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) { if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) {
@ -118,9 +116,10 @@ public class AuthController extends BaseController {
@RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST) @RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void requestResetPasswordByEmail ( public void requestResetPasswordByEmail (
@RequestParam(value = "email") String email, @RequestBody JsonNode resetPasswordByEmailRequest,
HttpServletRequest request) throws ThingsboardException { HttpServletRequest request) throws ThingsboardException {
try { try {
String email = resetPasswordByEmailRequest.get("email").asText();
UserCredentials userCredentials = userService.requestPasswordReset(email); UserCredentials userCredentials = userService.requestPasswordReset(email);
String baseUrl = constructBaseUrl(request); String baseUrl = constructBaseUrl(request);
String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl, String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl,
@ -158,10 +157,11 @@ public class AuthController extends BaseController {
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
@ResponseBody @ResponseBody
public JsonNode activateUser( public JsonNode activateUser(
@RequestParam(value = "activateToken") String activateToken, @RequestBody JsonNode activateRequest,
@RequestParam(value = "password") String password,
HttpServletRequest request) throws ThingsboardException { HttpServletRequest request) throws ThingsboardException {
try { try {
String activateToken = activateRequest.get("activateToken").asText();
String password = activateRequest.get("password").asText();
String encodedPassword = passwordEncoder.encode(password); String encodedPassword = passwordEncoder.encode(password);
UserCredentials credentials = userService.activateUserCredentials(activateToken, encodedPassword); UserCredentials credentials = userService.activateUserCredentials(activateToken, encodedPassword);
User user = userService.findUserById(credentials.getUserId()); User user = userService.findUserById(credentials.getUserId());
@ -194,10 +194,11 @@ public class AuthController extends BaseController {
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
@ResponseBody @ResponseBody
public JsonNode resetPassword( public JsonNode resetPassword(
@RequestParam(value = "resetToken") String resetToken, @RequestBody JsonNode resetPasswordRequest,
@RequestParam(value = "password") String password,
HttpServletRequest request) throws ThingsboardException { HttpServletRequest request) throws ThingsboardException {
try { try {
String resetToken = resetPasswordRequest.get("resetToken").asText();
String password = resetPasswordRequest.get("password").asText();
UserCredentials userCredentials = userService.findUserCredentialsByResetToken(resetToken); UserCredentials userCredentials = userService.findUserCredentialsByResetToken(resetToken);
if (userCredentials != null) { if (userCredentials != null) {
String encodedPassword = passwordEncoder.encode(password); String encodedPassword = passwordEncoder.encode(password);

5
application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java

@ -221,7 +221,10 @@ public abstract class AbstractControllerTest {
doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken) doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken)
.andExpect(status().isSeeOther()) .andExpect(status().isSeeOther())
.andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken)); .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", "activateToken", TestMailService.currentActivateToken, "password", password).andExpect(status().isOk()), JsonNode.class); JsonNode activateRequest = new ObjectMapper().createObjectNode()
.put("activateToken", TestMailService.currentActivateToken)
.put("password", password);
JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class);
validateAndSetJwtToken(tokenInfo, user.getEmail()); validateAndSetJwtToken(tokenInfo, user.getEmail());
return savedUser; return savedUser;
} }

21
application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java

@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
@ -73,7 +74,11 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
.andExpect(status().isSeeOther()) .andExpect(status().isSeeOther())
.andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken)); .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", "activateToken", TestMailService.currentActivateToken, "password", "testPassword").andExpect(status().isOk()), JsonNode.class); JsonNode activateRequest = new ObjectMapper().createObjectNode()
.put("activateToken", TestMailService.currentActivateToken)
.put("password", "testPassword");
JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class);
validateAndSetJwtToken(tokenInfo, email); validateAndSetJwtToken(tokenInfo, email);
doGet("/api/auth/user") doGet("/api/auth/user")
@ -117,13 +122,21 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
User savedUser = createUserAndLogin(user, "testPassword1"); User savedUser = createUserAndLogin(user, "testPassword1");
logout(); logout();
doPost("/api/noauth/resetPasswordByEmail", "email", email)
JsonNode resetPasswordByEmailRequest = new ObjectMapper().createObjectNode()
.put("email", email);
doPost("/api/noauth/resetPasswordByEmail", resetPasswordByEmailRequest)
.andExpect(status().isOk()); .andExpect(status().isOk());
doGet("/api/noauth/resetPassword?resetToken={resetToken}", TestMailService.currentResetPasswordToken) doGet("/api/noauth/resetPassword?resetToken={resetToken}", TestMailService.currentResetPasswordToken)
.andExpect(status().isSeeOther()) .andExpect(status().isSeeOther())
.andExpect(header().string(HttpHeaders.LOCATION, "/login/resetPassword?resetToken=" + TestMailService.currentResetPasswordToken)); .andExpect(header().string(HttpHeaders.LOCATION, "/login/resetPassword?resetToken=" + TestMailService.currentResetPasswordToken));
JsonNode tokenInfo = readResponse(doPost("/api/noauth/resetPassword", "resetToken", TestMailService.currentResetPasswordToken, "password", "testPassword2").andExpect(status().isOk()), JsonNode.class); JsonNode resetPasswordRequest = new ObjectMapper().createObjectNode()
.put("resetToken", TestMailService.currentResetPasswordToken)
.put("password", "testPassword2");
JsonNode tokenInfo = readResponse(doPost("/api/noauth/resetPassword", resetPasswordRequest).andExpect(status().isOk()), JsonNode.class);
validateAndSetJwtToken(tokenInfo, email); validateAndSetJwtToken(tokenInfo, email);
doGet("/api/auth/user") doGet("/api/auth/user")

2
common/data/pom.xml

@ -28,7 +28,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Thingsboard Server Common Data</name> <name>Thingsboard Server Common Data</name>
<url>http://thingsboard.org</url> <url>https://thingsboard.io</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

2
common/message/pom.xml

@ -28,7 +28,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Thingsboard Server Common Messages</name> <name>Thingsboard Server Common Messages</name>
<url>http://thingsboard.org</url> <url>https://thingsboard.io</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

2
common/pom.xml

@ -28,7 +28,7 @@
<packaging>pom</packaging> <packaging>pom</packaging>
<name>Thingsboard Server Commons</name> <name>Thingsboard Server Commons</name>
<url>http://thingsboard.org</url> <url>https://thingsboard.io</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

2
common/transport/pom.xml

@ -28,7 +28,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Thingsboard Server Common Transport components</name> <name>Thingsboard Server Common Transport components</name>
<url>http://thingsboard.org</url> <url>https://thingsboard.io</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

2
dao/pom.xml

@ -28,7 +28,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Thingsboard Server DAO Layer</name> <name>Thingsboard Server DAO Layer</name>
<url>http://thingsboard.org</url> <url>https://thingsboard.io</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

6
dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java

@ -384,7 +384,7 @@ public class BaseRelationService implements RelationService {
Set<EntityRelation> children = new HashSet<>(findRelations(rootId, direction).get()); Set<EntityRelation> children = new HashSet<>(findRelations(rootId, direction).get());
Set<EntityId> childrenIds = new HashSet<>(); Set<EntityId> childrenIds = new HashSet<>();
for (EntityRelation childRelation : children) { for (EntityRelation childRelation : children) {
log.info("Found Relation: {}", childRelation); log.trace("Found Relation: {}", childRelation);
EntityId childId; EntityId childId;
if (direction == EntitySearchDirection.FROM) { if (direction == EntitySearchDirection.FROM) {
childId = childRelation.getTo(); childId = childRelation.getTo();
@ -392,9 +392,9 @@ public class BaseRelationService implements RelationService {
childId = childRelation.getFrom(); childId = childRelation.getFrom();
} }
if (uniqueMap.putIfAbsent(childId, Boolean.TRUE) == null) { if (uniqueMap.putIfAbsent(childId, Boolean.TRUE) == null) {
log.info("Adding Relation: {}", childId); log.trace("Adding Relation: {}", childId);
if (childrenIds.add(childId)) { if (childrenIds.add(childId)) {
log.info("Added Relation: {}", childId); log.trace("Added Relation: {}", childId);
} }
} }
} }

1
dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java

@ -22,6 +22,7 @@ import javax.annotation.PreDestroy;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
public abstract class JpaAbstractDaoListeningExecutorService { public abstract class JpaAbstractDaoListeningExecutorService {
protected ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10)); protected ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
@PreDestroy @PreDestroy

20
dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java

@ -17,9 +17,7 @@ package org.thingsboard.server.dao.sql.timeseries;
import com.google.common.base.Function; import com.google.common.base.Function;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.*;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
@ -36,10 +34,12 @@ import org.thingsboard.server.dao.timeseries.TimeseriesDao;
import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.util.SqlDao;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.annotation.PreDestroy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
@ -50,6 +50,8 @@ import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
@SqlDao @SqlDao
public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService implements TimeseriesDao { public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService implements TimeseriesDao {
private ListeningExecutorService insertService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
@Autowired @Autowired
private TsKvRepository tsKvRepository; private TsKvRepository tsKvRepository;
@ -232,7 +234,8 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
entity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null)); entity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null));
entity.setLongValue(tsKvEntry.getLongValue().orElse(null)); entity.setLongValue(tsKvEntry.getLongValue().orElse(null));
entity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null)); entity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null));
return service.submit(() -> { log.trace("Saving entity: " + entity);
return insertService.submit(() -> {
tsKvRepository.save(entity); tsKvRepository.save(entity);
return null; return null;
}); });
@ -240,7 +243,7 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
@Override @Override
public ListenableFuture<Void> savePartition(EntityId entityId, long tsKvEntryTs, String key, long ttl) { public ListenableFuture<Void> savePartition(EntityId entityId, long tsKvEntryTs, String key, long ttl) {
return service.submit(() -> null); return insertService.submit(() -> null);
} }
@Override @Override
@ -254,10 +257,15 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
latestEntity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null)); latestEntity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null));
latestEntity.setLongValue(tsKvEntry.getLongValue().orElse(null)); latestEntity.setLongValue(tsKvEntry.getLongValue().orElse(null));
latestEntity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null)); latestEntity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null));
return service.submit(() -> { return insertService.submit(() -> {
tsKvLatestRepository.save(latestEntity); tsKvLatestRepository.save(latestEntity);
return null; return null;
}); });
} }
@PreDestroy
void onDestroy() {
insertService.shutdown();
}
} }

2
extensions-api/pom.xml

@ -28,7 +28,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Thingsboard Server Extensions API</name> <name>Thingsboard Server Extensions API</name>
<url>http://thingsboard.org</url> <url>https://thingsboard.io</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

2
extensions-core/pom.xml

@ -28,7 +28,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Thingsboard Server Core Extensions</name> <name>Thingsboard Server Core Extensions</name>
<url>http://thingsboard.org</url> <url>https://thingsboard.io</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

8
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java

@ -128,12 +128,16 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
Optional<Long> interval = request.getLongParamValue("interval"); Optional<Long> interval = request.getLongParamValue("interval");
Optional<Integer> limit = request.getIntParamValue("limit"); Optional<Integer> limit = request.getIntParamValue("limit");
// If some of these params are specified, they all must be
if (startTs.isPresent() || endTs.isPresent() || interval.isPresent() || limit.isPresent()) { if (startTs.isPresent() || endTs.isPresent() || interval.isPresent() || limit.isPresent()) {
if (!startTs.isPresent() || !endTs.isPresent() || !interval.isPresent()) { if (!startTs.isPresent() || !endTs.isPresent() || !interval.isPresent() || interval.get() < 0) {
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
return; return;
} }
Aggregation agg = Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name()));
// If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted
Aggregation agg = (interval.isPresent() && interval.get() == 0) ? Aggregation.valueOf(Aggregation.NONE.name()) :
Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name()));
List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), interval.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg)) List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), interval.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg))
.collect(Collectors.toList()); .collect(Collectors.toList());

2
extensions/extension-kafka/pom.xml

@ -30,7 +30,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Thingsboard Server Kafka Extension</name> <name>Thingsboard Server Kafka Extension</name>
<url>http://thingsboard.org</url> <url>https://thingsboard.io</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

2
extensions/extension-mqtt/pom.xml

@ -30,7 +30,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Thingsboard Server MQTT Extension</name> <name>Thingsboard Server MQTT Extension</name>
<url>http://thingsboard.org</url> <url>https://thingsboard.io</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

2
extensions/extension-rabbitmq/pom.xml

@ -28,7 +28,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Thingsboard Server RabbitMQ Extension</name> <name>Thingsboard Server RabbitMQ Extension</name>
<url>http://thingsboard.org</url> <url>https://thingsboard.io</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

2
extensions/extension-rest-api-call/pom.xml

@ -30,7 +30,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Thingsboard Server REST API Call Extension</name> <name>Thingsboard Server REST API Call Extension</name>
<url>http://thingsboard.org</url> <url>https://thingsboard.io</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

81
extensions/extension-sns/pom.xml

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright © 2016-2017 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>extensions</artifactId>
<groupId>org.thingsboard</groupId>
<version>1.4.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.thingsboard.extensions</groupId>
<artifactId>extension-sns</artifactId>
<packaging>jar</packaging>
<name>Thingsboard Server SNS Extension</name>
<url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<main.dir>${basedir}/../..</main.dir>
<aws.sdk.version>1.11.229</aws.sdk.version>
</properties>
<dependencies>
<dependency>
<groupId>org.thingsboard</groupId>
<artifactId>extensions-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.thingsboard</groupId>
<artifactId>extensions-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sns</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptors>
<descriptor>src/assembly/extension.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

37
extensions/extension-sns/src/assembly/extension.xml

@ -0,0 +1,37 @@
<!--
Copyright © 2016-2017 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.
-->
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
<id>extension</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<useProjectArtifact>true</useProjectArtifact>
<unpack>true</unpack>
<scope>runtime</scope>
<excludes>
</excludes>
</dependencySet>
</dependencySets>
</assembly>

31
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionMsg.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.sns.action;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
/**
* Created by Valerii Sosliuk on 11/15/2017.
*/
public class SnsTopicActionMsg extends AbstractRuleToPluginMsg<SnsTopicActionPayload> {
public SnsTopicActionMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, SnsTopicActionPayload payload) {
super(tenantId, customerId, deviceId, payload);
}
}

37
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionPayload.java

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.sns.action;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.msg.session.MsgType;
import java.io.Serializable;
/**
* Created by Valerii Sosliuk on 11/15/2017.
*/
@Data
@Builder
public class SnsTopicActionPayload implements Serializable {
private final String topicArn;
private final String msgBody;
private final Integer requestId;
private final MsgType msgType;
private final boolean sync;
}

45
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginAction.java

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.sns.action;
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
import org.thingsboard.server.extensions.api.component.Action;
import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
import org.thingsboard.server.extensions.api.rules.RuleContext;
import org.thingsboard.server.extensions.core.action.template.AbstractTemplatePluginAction;
import java.util.Optional;
/**
* Created by Valerii Sosliuk on 11/15/2017.
*/
@Action(name = "SNS Topic Action", descriptor = "SnsTopicActionDescriptor.json", configuration = SnsTopicPluginActionConfiguration.class)
public class SnsTopicPluginAction extends AbstractTemplatePluginAction<SnsTopicPluginActionConfiguration> {
@Override
protected Optional<RuleToPluginMsg> buildRuleToPluginMsg(RuleContext ctx, ToDeviceActorMsg msg, FromDeviceRequestMsg payload) {
SnsTopicActionPayload.SnsTopicActionPayloadBuilder builder = SnsTopicActionPayload.builder();
builder.msgType(payload.getMsgType());
builder.requestId(payload.getRequestId());
builder.topicArn(configuration.getTopicArn());
builder.msgBody(getMsgBody(ctx, msg));
return Optional.of(new SnsTopicActionMsg(msg.getTenantId(),
msg.getCustomerId(),
msg.getDeviceId(),
builder.build()));
}
}

30
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginActionConfiguration.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.sns.action;
import lombok.Data;
import org.thingsboard.server.extensions.core.action.template.TemplateActionConfiguration;
/**
* Created by Valerii Sosliuk on 11/15/2017.
*/
@Data
public class SnsTopicPluginActionConfiguration implements TemplateActionConfiguration {
private String topicArn;
private String template;
private boolean sync;
}

63
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsMessageHandler.java

@ -0,0 +1,63 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.sns.plugin;
import com.amazonaws.services.sns.AmazonSNS;
import com.amazonaws.services.sns.model.PublishRequest;
import com.amazonaws.services.sns.model.PublishResult;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.model.SendMessageRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.RuleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
import org.thingsboard.server.extensions.api.plugins.PluginContext;
import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
import org.thingsboard.server.extensions.api.plugins.msg.ResponsePluginToRuleMsg;
import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
import org.thingsboard.server.extensions.api.rules.RuleException;
import org.thingsboard.server.extensions.sns.action.SnsTopicActionMsg;
import org.thingsboard.server.extensions.sns.action.SnsTopicActionPayload;
/**
* Created by Valerii Sosliuk on 11/6/2017.
*/
@RequiredArgsConstructor
@Slf4j
public class SnsMessageHandler implements RuleMsgHandler {
private final AmazonSNS sns;
@Override
public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
if (msg instanceof SnsTopicActionMsg) {
SnsTopicActionPayload payload = ((SnsTopicActionMsg) msg).getPayload();
PublishRequest publishRequest = new PublishRequest()
.withTopicArn(payload.getTopicArn())
.withMessage(payload.getMsgBody());
sns.publish(publishRequest);
if (payload.isSync()) {
ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId,
BasicStatusCodeResponse.onSuccess(payload.getMsgType(), payload.getRequestId())));
}
return;
}
throw new RuleException("Unsupported message type " + msg.getClass().getName() + "!");
}
}

79
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPlugin.java

@ -0,0 +1,79 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.sns.plugin;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.sns.AmazonSNS;
import com.amazonaws.services.sns.AmazonSNSClient;
import org.thingsboard.server.extensions.api.component.Plugin;
import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
import org.thingsboard.server.extensions.api.plugins.PluginContext;
import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
import org.thingsboard.server.extensions.sns.action.SnsTopicPluginAction;
/**
* Created by Valerii Sosliuk on 11/15/2017.
*/
@Plugin(name = "SNS Plugin", actions = {SnsTopicPluginAction.class},
descriptor = "SnsPluginDescriptor.json", configuration = SnsPluginConfiguration.class)
public class SnsPlugin extends AbstractPlugin<SnsPluginConfiguration> {
private SnsMessageHandler snsMessageHandler;
private SnsPluginConfiguration configuration;
@Override
public void init(SnsPluginConfiguration configuration) {
this.configuration = configuration;
init();
}
private void init() {
AWSCredentials awsCredentials = new BasicAWSCredentials(configuration.getAccessKeyId(), configuration.getSecretAccessKey());
AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials);
AmazonSNS sns = AmazonSNSClient.builder()
.withCredentials(credProvider)
.withRegion(configuration.getRegion())
.build();
this.snsMessageHandler = new SnsMessageHandler(sns);
}
private void destroy() {
this.snsMessageHandler = null;
}
@Override
protected RuleMsgHandler getRuleMsgHandler() {
return snsMessageHandler;
}
@Override
public void resume(PluginContext ctx) {
init();
}
@Override
public void suspend(PluginContext ctx) {
destroy();
}
@Override
public void stop(PluginContext ctx) {
destroy();
}
}

30
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPluginConfiguration.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.sns.plugin;
import lombok.Data;
/**
* Created by Valerii Sosliuk on 11/5/2017.
*/
@Data
public class SnsPluginConfiguration {
private String accessKeyId;
private String secretAccessKey;
private String region;
}

30
extensions/extension-sns/src/main/resources/SnsPluginDescriptor.json

@ -0,0 +1,30 @@
{
"schema": {
"title": "SNS Plugin Configuration",
"type": "object",
"properties": {
"accessKeyId": {
"title": "Access Key ID",
"type": "string"
},
"secretAccessKey": {
"title": "Secret Access Key",
"type": "string"
},
"region": {
"title": "Region",
"type": "string"
}
},
"required": [
"accessKeyId",
"secretAccessKey",
"region"
]
},
"form": [
"accessKeyId",
"secretAccessKey",
"region"
]
}

34
extensions/extension-sns/src/main/resources/SnsTopicActionDescriptor.json

@ -0,0 +1,34 @@
{
"schema": {
"title": "SNS Topic Action Configuration",
"type": "object",
"properties": {
"sync": {
"title": "Requires delivery confirmation",
"type": "boolean"
},
"topicArn": {
"title": "Topic ARN",
"type": "string"
},
"template": {
"title": "Body Template",
"type": "string"
}
},
"required": [
"sync",
"topicArn",
"template"
]
},
"form": [
"sync",
"topicArn",
{
"key": "template",
"type": "textarea",
"rows": 5
}
]
}

81
extensions/extension-sqs/pom.xml

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright © 2016-2017 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>extensions</artifactId>
<groupId>org.thingsboard</groupId>
<version>1.4.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.thingsboard.extensions</groupId>
<artifactId>extension-sqs</artifactId>
<packaging>jar</packaging>
<name>Thingsboard Server SQS Extension</name>
<url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<main.dir>${basedir}/../..</main.dir>
<aws.sdk.version>1.11.229</aws.sdk.version>
</properties>
<dependencies>
<dependency>
<groupId>org.thingsboard</groupId>
<artifactId>extensions-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.thingsboard</groupId>
<artifactId>extensions-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sqs</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptors>
<descriptor>src/assembly/extension.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

37
extensions/extension-sqs/src/assembly/extension.xml

@ -0,0 +1,37 @@
<!--
Copyright © 2016-2017 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.
-->
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
<id>extension</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<useProjectArtifact>true</useProjectArtifact>
<unpack>true</unpack>
<scope>runtime</scope>
<excludes>
</excludes>
</dependencySet>
</dependencySets>
</assembly>

31
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionMsg.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.sqs.action.fifo;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
/**
* Created by Valerii Sosliuk on 11/10/2017.
*/
public class SqsFifoQueueActionMsg extends AbstractRuleToPluginMsg<SqsFifoQueueActionPayload> {
public SqsFifoQueueActionMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, SqsFifoQueueActionPayload payload) {
super(tenantId, customerId, deviceId, payload);
}
}

39
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionPayload.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.sqs.action.fifo;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.msg.session.MsgType;
import java.io.Serializable;
/**
* Created by Valerii Sosliuk on 11/10/2017.
*/
@Data
@Builder
public class SqsFifoQueueActionPayload implements Serializable {
private final String queue;
private final String msgBody;
private final String deviceId;
private final Integer requestId;
private final MsgType msgType;
private final boolean sync;
}

49
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginAction.java

@ -0,0 +1,49 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.sqs.action.fifo;
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
import org.thingsboard.server.extensions.api.component.Action;
import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
import org.thingsboard.server.extensions.api.rules.RuleContext;
import org.thingsboard.server.extensions.core.action.template.AbstractTemplatePluginAction;
import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionMsg;
import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionPayload;
import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueuePluginActionConfiguration;
import java.util.Optional;
/**
* Created by Valerii Sosliuk on 11/5/2017.
*/
@Action(name = "SQS Fifo Queue Action", descriptor = "SqsFifoQueueActionDescriptor.json", configuration = SqsFifoQueuePluginActionConfiguration.class)
public class SqsFifoQueuePluginAction extends AbstractTemplatePluginAction<SqsFifoQueuePluginActionConfiguration> {
@Override
protected Optional<RuleToPluginMsg> buildRuleToPluginMsg(RuleContext ctx, ToDeviceActorMsg msg, FromDeviceRequestMsg payload) {
SqsFifoQueueActionPayload.SqsFifoQueueActionPayloadBuilder builder = SqsFifoQueueActionPayload.builder();
builder.msgType(payload.getMsgType());
builder.requestId(payload.getRequestId());
builder.queue(configuration.getQueue());
builder.deviceId(msg.getDeviceId().toString());
builder.msgBody(getMsgBody(ctx, msg));
return Optional.of(new SqsFifoQueueActionMsg(msg.getTenantId(),
msg.getCustomerId(),
msg.getDeviceId(),
builder.build()));
}
}

30
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginActionConfiguration.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.sqs.action.fifo;
import lombok.Data;
import org.thingsboard.server.extensions.core.action.template.TemplateActionConfiguration;
/**
* Created by Valerii Sosliuk on 11/10/2017.
*/
@Data
public class SqsFifoQueuePluginActionConfiguration implements TemplateActionConfiguration {
private String queue;
private String template;
private boolean sync;
}

31
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionMsg.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.sqs.action.standard;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
/**
* Created by Valerii Sosliuk on 11/6/2017.
*/
public class SqsStandardQueueActionMsg extends AbstractRuleToPluginMsg<SqsStandardQueueActionPayload> {
public SqsStandardQueueActionMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, SqsStandardQueueActionPayload payload) {
super(tenantId, customerId, deviceId, payload);
}
}

39
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionPayload.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.sqs.action.standard;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.msg.session.MsgType;
import java.io.Serializable;
/**
* Created by Valerii Sosliuk on 11/6/2017.
*/
@Data
@Builder
public class SqsStandardQueueActionPayload implements Serializable {
private final String queue;
private final String msgBody;
private final int delaySeconds;
private final Integer requestId;
private final MsgType msgType;
private final boolean sync;
}

46
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginAction.java

@ -0,0 +1,46 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.sqs.action.standard;
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
import org.thingsboard.server.extensions.api.component.Action;
import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
import org.thingsboard.server.extensions.api.rules.RuleContext;
import org.thingsboard.server.extensions.core.action.template.AbstractTemplatePluginAction;
import java.util.Optional;
/**
* Created by Valerii Sosliuk on 11/5/2017.
*/
@Action(name = "SQS Standard Queue Action", descriptor = "SqsStandardQueueActionDescriptor.json", configuration = SqsStandardQueuePluginActionConfiguration.class)
public class SqsStandardQueuePluginAction extends AbstractTemplatePluginAction<SqsStandardQueuePluginActionConfiguration> {
@Override
protected Optional<RuleToPluginMsg> buildRuleToPluginMsg(RuleContext ctx, ToDeviceActorMsg msg, FromDeviceRequestMsg payload) {
SqsStandardQueueActionPayload.SqsStandardQueueActionPayloadBuilder builder = SqsStandardQueueActionPayload.builder();
builder.msgType(payload.getMsgType());
builder.requestId(payload.getRequestId());
builder.queue(configuration.getQueue());
builder.delaySeconds(configuration.getDelaySeconds());
builder.msgBody(getMsgBody(ctx, msg));
return Optional.of(new SqsStandardQueueActionMsg(msg.getTenantId(),
msg.getCustomerId(),
msg.getDeviceId(),
builder.build()));
}
}

32
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginActionConfiguration.java

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.sqs.action.standard;
import lombok.Data;
import org.thingsboard.server.extensions.core.action.template.TemplateActionConfiguration;
/**
* Created by Valerii Sosliuk on 11/6/2017.
*/
@Data
public class SqsStandardQueuePluginActionConfiguration implements TemplateActionConfiguration {
private String queue;
private int delaySeconds;
private boolean sync;
private String template;
}

84
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsMessageHandler.java

@ -0,0 +1,84 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.sqs.plugin;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.model.SendMessageRequest;
import com.amazonaws.services.sqs.model.SendMessageResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.RuleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
import org.thingsboard.server.extensions.api.plugins.PluginContext;
import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
import org.thingsboard.server.extensions.api.plugins.msg.ResponsePluginToRuleMsg;
import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
import org.thingsboard.server.extensions.api.rules.RuleException;
import org.thingsboard.server.extensions.sqs.action.fifo.SqsFifoQueueActionMsg;
import org.thingsboard.server.extensions.sqs.action.fifo.SqsFifoQueueActionPayload;
import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionMsg;
import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionPayload;
/**
* Created by Valerii Sosliuk on 11/15/2017.
*/
@RequiredArgsConstructor
@Slf4j
public class SqsMessageHandler implements RuleMsgHandler {
private final AmazonSQS sqs;
@Override
public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
if (msg instanceof SqsStandardQueueActionMsg) {
sendMessageToStandardQueue(ctx, tenantId, ruleId, msg);
return;
}
if (msg instanceof SqsFifoQueueActionMsg) {
sendMessageToFifoQueue(ctx, tenantId, ruleId, msg);
return;
}
throw new RuleException("Unsupported message type " + msg.getClass().getName() + "!");
}
private void sendMessageToStandardQueue(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) {
SqsStandardQueueActionPayload payload = ((SqsStandardQueueActionMsg) msg).getPayload();
SendMessageRequest sendMsgRequest = new SendMessageRequest()
.withDelaySeconds(payload.getDelaySeconds())
.withQueueUrl(payload.getQueue())
.withMessageBody(payload.getMsgBody());
sqs.sendMessage(sendMsgRequest);
if (payload.isSync()) {
ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId,
BasicStatusCodeResponse.onSuccess(payload.getMsgType(), payload.getRequestId())));
}
}
private void sendMessageToFifoQueue(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) {
SqsFifoQueueActionPayload payload = ((SqsFifoQueueActionMsg) msg).getPayload();
SendMessageRequest sendMsgRequest = new SendMessageRequest()
.withQueueUrl(payload.getQueue())
.withMessageBody(payload.getMsgBody())
.withMessageGroupId(payload.getDeviceId());
sqs.sendMessage(sendMsgRequest);
if (payload.isSync()) {
ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId,
BasicStatusCodeResponse.onSuccess(payload.getMsgType(), payload.getRequestId())));
}
}
}

78
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPlugin.java

@ -0,0 +1,78 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.sqs.plugin;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
import org.thingsboard.server.extensions.api.component.Plugin;
import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
import org.thingsboard.server.extensions.api.plugins.PluginContext;
import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
import org.thingsboard.server.extensions.sqs.action.fifo.SqsFifoQueuePluginAction;
import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueuePluginAction;
/**
* Created by Valerii Sosliuk on 11/6/2017.
*/
@Plugin(name = "SQS Plugin", actions = {SqsStandardQueuePluginAction.class, SqsFifoQueuePluginAction.class},
descriptor = "SqsPluginDescriptor.json", configuration = SqsPluginConfiguration.class)
public class SqsPlugin extends AbstractPlugin<SqsPluginConfiguration> {
private SqsMessageHandler sqsMessageHandler;
private SqsPluginConfiguration configuration;
@Override
public void init(SqsPluginConfiguration configuration) {
this.configuration = configuration;
init();
}
private void init() {
AWSCredentials awsCredentials = new BasicAWSCredentials(configuration.getAccessKeyId(), configuration.getSecretAccessKey());
AmazonSQS sqs = AmazonSQSClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.withRegion(Regions.fromName(configuration.getRegion())).build();
this.sqsMessageHandler = new SqsMessageHandler(sqs);
}
private void destroy() {
this.sqsMessageHandler = null;
}
@Override
protected RuleMsgHandler getRuleMsgHandler() {
return sqsMessageHandler;
}
@Override
public void resume(PluginContext ctx) {
init();
}
@Override
public void suspend(PluginContext ctx) {
destroy();
}
@Override
public void stop(PluginContext ctx) {
destroy();
}
}

30
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPluginConfiguration.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.sqs.plugin;
import lombok.Data;
/**
* Created by Valerii Sosliuk on 11/5/2017.
*/
@Data
public class SqsPluginConfiguration {
private String accessKeyId;
private String secretAccessKey;
private String region;
}

34
extensions/extension-sqs/src/main/resources/SqsFifoQueueActionDescriptor.json

@ -0,0 +1,34 @@
{
"schema": {
"title": "SQS FIFO Queue Action Configuration",
"type": "object",
"properties": {
"sync": {
"title": "Requires delivery confirmation",
"type": "boolean"
},
"queue": {
"title": "Queue URL",
"type": "string"
},
"template": {
"title": "Body Template",
"type": "string"
}
},
"required": [
"sync",
"queue",
"template"
]
},
"form": [
"sync",
"queue",
{
"key": "template",
"type": "textarea",
"rows": 5
}
]
}

30
extensions/extension-sqs/src/main/resources/SqsPluginDescriptor.json

@ -0,0 +1,30 @@
{
"schema": {
"title": "SQS Plugin Configuration",
"type": "object",
"properties": {
"accessKeyId": {
"title": "Access Key ID",
"type": "string"
},
"secretAccessKey": {
"title": "Secret Access Key",
"type": "string"
},
"region": {
"title": "Region",
"type": "string"
}
},
"required": [
"accessKeyId",
"secretAccessKey",
"region"
]
},
"form": [
"accessKeyId",
"secretAccessKey",
"region"
]
}

41
extensions/extension-sqs/src/main/resources/SqsStandardQueueActionDescriptor.json

@ -0,0 +1,41 @@
{
"schema": {
"title": "SQS Standard Queue Action Configuration",
"type": "object",
"properties": {
"sync": {
"title": "Requires delivery confirmation",
"type": "boolean"
},
"queue": {
"title": "Queue URL",
"type": "string"
},
"delaySeconds": {
"title": "Delay Seconds",
"type": "integer",
"default": 0
},
"template": {
"title": "Body Template",
"type": "string"
}
},
"required": [
"sync",
"queue",
"delaySeconds",
"template"
]
},
"form": [
"sync",
"queue",
"delaySeconds",
{
"key": "template",
"type": "textarea",
"rows": 5
}
]
}

69
extensions/extension-sqs/src/test/java/org/thingsboard/server/extensions/sqs/SqsDemoClient.java

@ -0,0 +1,69 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.sqs;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
import com.amazonaws.services.sqs.model.DeleteMessageRequest;
import com.amazonaws.services.sqs.model.Message;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
/**
* Created by Valerii Sosliuk on 11/10/2017.
*/
@Slf4j
public class SqsDemoClient {
private static final String ACCESS_KEY_ID = "$ACCES_KEY_ID";
private static final String SECRET_ACCESS_KEY = "$SECRET_ACCESS_KEY";
private static final String QUEUE_URL = "$QUEUE_URL";
private static final String REGION = "us-east-1";
public static void main(String[] args) {
log.info("Starting SQS Demo Clinent...");
AWSCredentials awsCredentials = new BasicAWSCredentials(ACCESS_KEY_ID, SECRET_ACCESS_KEY);
AmazonSQS sqs = AmazonSQSClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.withRegion(Regions.fromName(REGION)).build();
SqsDemoClient client = new SqsDemoClient();
client.pollMessages(sqs);
}
private void pollMessages(AmazonSQS sqs) {
log.info("Polling messages");
while (true) {
List<Message> messages = sqs.receiveMessage(QUEUE_URL).getMessages();
messages.forEach(m -> {
log.info("Message Received: " + m.getBody());
System.out.println(m.getBody());
DeleteMessageRequest deleteMessageRequest = new DeleteMessageRequest(QUEUE_URL, m.getReceiptHandle());
sqs.deleteMessage(deleteMessageRequest);
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
}
}

10
extensions/extension-sqs/src/test/resources/logback.xml

@ -0,0 +1,10 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

4
extensions/pom.xml

@ -28,7 +28,7 @@
<packaging>pom</packaging> <packaging>pom</packaging>
<name>Thingsboard Extensions</name> <name>Thingsboard Extensions</name>
<url>http://thingsboard.org</url> <url>https://thingsboard.io</url>
<properties> <properties>
<main.dir>${basedir}/..</main.dir> <main.dir>${basedir}/..</main.dir>
@ -39,6 +39,8 @@
<module>extension-rest-api-call</module> <module>extension-rest-api-call</module>
<module>extension-kafka</module> <module>extension-kafka</module>
<module>extension-mqtt</module> <module>extension-mqtt</module>
<module>extension-sqs</module>
<module>extension-sns</module>
</modules> </modules>
</project> </project>

12
pom.xml

@ -350,6 +350,18 @@
<classifier>extension</classifier> <classifier>extension</classifier>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.thingsboard.extensions</groupId>
<artifactId>extension-sqs</artifactId>
<classifier>extension</classifier>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.thingsboard.extensions</groupId>
<artifactId>extension-sns</artifactId>
<classifier>extension</classifier>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>
<artifactId>data</artifactId> <artifactId>data</artifactId>

18
resume.bat

@ -0,0 +1,18 @@
@REM
@REM Copyright © 2016-2017 The Thingsboard Authors
@REM
@REM Licensed under the Apache License, Version 2.0 (the "License");
@REM you may not use this file except in compliance with the License.
@REM You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing, software
@REM distributed under the License is distributed on an "AS IS" BASIS,
@REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@REM See the License for the specific language governing permissions and
@REM limitations under the License.
@REM
mvn clean install -rf :application

2
tools/pom.xml

@ -28,7 +28,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Thingsboard Server Tools</name> <name>Thingsboard Server Tools</name>
<url>http://thingsboard.org</url> <url>https://thingsboard.io</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

38
tools/src/main/java/org/thingsboard/client/tools/RestClient.java

@ -26,9 +26,16 @@ import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.support.HttpRequestWrapper; import org.springframework.http.client.support.HttpRequestWrapper;
import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import java.io.IOException; import java.io.IOException;
@ -71,18 +78,40 @@ public class RestClient implements ClientHttpRequestInterceptor {
} }
} }
public Device createDevice(String name) { public Customer createCustomer(String title) {
Customer customer = new Customer();
customer.setTitle(title);
return restTemplate.postForEntity(baseURL + "/api/customer", customer, Customer.class).getBody();
}
public Device createDevice(String name, String type) {
Device device = new Device(); Device device = new Device();
device.setName(name); device.setName(name);
device.setType(type);
return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody(); return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody();
} }
public Asset createAsset(String name, String type) {
Asset asset = new Asset();
asset.setName(name);
asset.setType(type);
return restTemplate.postForEntity(baseURL + "/api/asset", asset, Asset.class).getBody();
}
public Alarm createAlarm(Alarm alarm) {
return restTemplate.postForEntity(baseURL + "/api/alarm", alarm, Alarm.class).getBody();
}
public Device assignDevice(CustomerId customerId, DeviceId deviceId) { public Device assignDevice(CustomerId customerId, DeviceId deviceId) {
return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/device/{deviceId}", null, Device.class, return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/device/{deviceId}", null, Device.class,
customerId.toString(), deviceId.toString()).getBody(); customerId.toString(), deviceId.toString()).getBody();
} }
public Asset assignAsset(CustomerId customerId, AssetId assetId) {
return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/asset/{assetId}", null, Asset.class,
customerId.toString(), assetId.toString()).getBody();
}
public DeviceCredentials getCredentials(DeviceId id) { public DeviceCredentials getCredentials(DeviceId id) {
return restTemplate.getForEntity(baseURL + "/api/device/" + id.getId().toString() + "/credentials", DeviceCredentials.class).getBody(); return restTemplate.getForEntity(baseURL + "/api/device/" + id.getId().toString() + "/credentials", DeviceCredentials.class).getBody();
} }
@ -91,11 +120,14 @@ public class RestClient implements ClientHttpRequestInterceptor {
return restTemplate; return restTemplate;
} }
public String getToken() {
return token;
}
@Override @Override
public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException { public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException {
HttpRequest wrapper = new HttpRequestWrapper(request); HttpRequest wrapper = new HttpRequestWrapper(request);
wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token); wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token);
return execution.execute(wrapper, bytes); return execution.execute(wrapper, bytes);
} }
}
}

36
tools/src/main/python/mqtt-send-telemetry.py

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2016-2017 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 paho.mqtt.client as mqtt
from time import sleep
import random
broker="test.mosquitto.org"
topic_pub='v1/devices/me/telemetry'
client = mqtt.Client()
client.username_pw_set("TEST_TOKEN")
client.connect('127.0.0.1', 1883, 1)
for i in range(5):
x = random.randrange(20, 100)
print x
msg = '{"windSpeed":"'+ str(x) + '"}'
client.publish(topic_pub, msg)
sleep(0.1)

2
transport/coap/pom.xml

@ -28,7 +28,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Thingsboard COAP Transport</name> <name>Thingsboard COAP Transport</name>
<url>http://thingsboard.org</url> <url>https://thingsboard.io</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

2
transport/http/pom.xml

@ -28,7 +28,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Thingsboard HTTP Transport</name> <name>Thingsboard HTTP Transport</name>
<url>http://thingsboard.org</url> <url>https://thingsboard.io</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

2
transport/mqtt/pom.xml

@ -28,7 +28,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Thingsboard MQTT Transport</name> <name>Thingsboard MQTT Transport</name>
<url>http://thingsboard.org</url> <url>https://thingsboard.io</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

2
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -229,6 +229,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
} else if (topicName.equals(DEVICE_ATTRIBUTES_RESPONSES_TOPIC)) { } else if (topicName.equals(DEVICE_ATTRIBUTES_RESPONSES_TOPIC)) {
deviceSessionCtx.setAllowAttributeResponses(); deviceSessionCtx.setAllowAttributeResponses();
grantedQoSList.add(getMinSupportedQos(reqQoS)); grantedQoSList.add(getMinSupportedQos(reqQoS));
} else if (topicName.equals(GATEWAY_ATTRIBUTES_TOPIC)) {
grantedQoSList.add(getMinSupportedQos(reqQoS));
} else { } else {
log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topicName, reqQoS); log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topicName, reqQoS);
grantedQoSList.add(FAILURE.value()); grantedQoSList.add(FAILURE.value());

44
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java

@ -16,6 +16,7 @@
package org.thingsboard.server.transport.mqtt.session; package org.thingsboard.server.transport.mqtt.session;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
@ -24,6 +25,7 @@ import io.netty.buffer.UnpooledByteBufAllocator;
import io.netty.handler.codec.mqtt.*; import io.netty.handler.codec.mqtt.*;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.SessionId; import org.thingsboard.server.common.data.id.SessionId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.msg.core.*; import org.thingsboard.server.common.msg.core.*;
import org.thingsboard.server.common.msg.kv.AttributesKVMsg; import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
@ -35,6 +37,7 @@ import org.thingsboard.server.transport.mqtt.MqttTopics;
import org.thingsboard.server.transport.mqtt.MqttTransportHandler; import org.thingsboard.server.transport.mqtt.MqttTransportHandler;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -83,7 +86,7 @@ public class GatewayDeviceSessionCtx extends DeviceAwareSessionContext {
if (responseMsg.isSuccess()) { if (responseMsg.isSuccess()) {
MsgType requestMsgType = responseMsg.getRequestMsgType(); MsgType requestMsgType = responseMsg.getRequestMsgType();
Integer requestId = responseMsg.getRequestId(); Integer requestId = responseMsg.getRequestId();
if (requestMsgType == MsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == MsgType.POST_TELEMETRY_REQUEST) { if (requestId >= 0 && requestMsgType == MsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == MsgType.POST_TELEMETRY_REQUEST) {
return Optional.of(MqttTransportHandler.createMqttPubAckMsg(requestId)); return Optional.of(MqttTransportHandler.createMqttPubAckMsg(requestId));
} }
} }
@ -135,40 +138,43 @@ public class GatewayDeviceSessionCtx extends DeviceAwareSessionContext {
if (responseData.isPresent()) { if (responseData.isPresent()) {
AttributesKVMsg msg = responseData.get(); AttributesKVMsg msg = responseData.get();
if (msg.getClientAttributes() != null) { if (msg.getClientAttributes() != null) {
msg.getClientAttributes().forEach(v -> addValueToJson(result, "value", v)); addValues(result, msg.getClientAttributes());
} }
if (msg.getSharedAttributes() != null) { if (msg.getSharedAttributes() != null) {
msg.getSharedAttributes().forEach(v -> addValueToJson(result, "value", v)); addValues(result, msg.getSharedAttributes());
} }
} }
return createMqttPublishMsg(topic, result); return createMqttPublishMsg(topic, result);
} }
private void addValues(JsonObject result, List<AttributeKvEntry> kvList) {
if (kvList.size() == 1) {
addValueToJson(result, "value", kvList.get(0));
} else {
JsonObject values;
if (result.has("values")) {
values = result.get("values").getAsJsonObject();
} else {
values = new JsonObject();
result.add("values", values);
}
kvList.forEach(value -> addValueToJson(values, value.getKey(), value));
}
}
private void addValueToJson(JsonObject json, String name, KvEntry entry) { private void addValueToJson(JsonObject json, String name, KvEntry entry) {
switch (entry.getDataType()) { switch (entry.getDataType()) {
case BOOLEAN: case BOOLEAN:
Optional<Boolean> booleanValue = entry.getBooleanValue(); entry.getBooleanValue().ifPresent(aBoolean -> json.addProperty(name, aBoolean));
if (booleanValue.isPresent()) {
json.addProperty(name, booleanValue.get());
}
break; break;
case STRING: case STRING:
Optional<String> stringValue = entry.getStrValue(); entry.getStrValue().ifPresent(aString -> json.addProperty(name, aString));
if (stringValue.isPresent()) {
json.addProperty(name, stringValue.get());
}
break; break;
case DOUBLE: case DOUBLE:
Optional<Double> doubleValue = entry.getDoubleValue(); entry.getDoubleValue().ifPresent(aDouble -> json.addProperty(name, aDouble));
if (doubleValue.isPresent()) {
json.addProperty(name, doubleValue.get());
}
break; break;
case LONG: case LONG:
Optional<Long> longValue = entry.getLongValue(); entry.getLongValue().ifPresent(aLong -> json.addProperty(name, aLong));
if (longValue.isPresent()) {
json.addProperty(name, longValue.get());
}
break; break;
} }
} }

27
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java

@ -41,10 +41,7 @@ import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.transport.mqtt.MqttTransportHandler; import org.thingsboard.server.transport.mqtt.MqttTransportHandler;
import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor; import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor;
import java.util.Collections; import java.util.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor.validateJsonPayload; import static org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor.validateJsonPayload;
@ -186,24 +183,34 @@ public class GatewaySessionCtx {
} }
} }
public void onDeviceAttributesRequest(MqttPublishMessage mqttMsg) throws AdaptorException { public void onDeviceAttributesRequest(MqttPublishMessage msg) throws AdaptorException {
JsonElement json = validateJsonPayload(gatewaySessionId, mqttMsg.payload()); JsonElement json = validateJsonPayload(gatewaySessionId, msg.payload());
if (json.isJsonObject()) { if (json.isJsonObject()) {
JsonObject jsonObj = json.getAsJsonObject(); JsonObject jsonObj = json.getAsJsonObject();
int requestId = jsonObj.get("id").getAsInt(); int requestId = jsonObj.get("id").getAsInt();
String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString();
boolean clientScope = jsonObj.get("client").getAsBoolean(); boolean clientScope = jsonObj.get("client").getAsBoolean();
String key = jsonObj.get("key").getAsString(); Set<String> keys;
if (jsonObj.has("key")) {
keys = Collections.singleton(jsonObj.get("key").getAsString());
} else {
JsonArray keysArray = jsonObj.get("keys").getAsJsonArray();
keys = new HashSet<>();
for (JsonElement keyObj : keysArray) {
keys.add(keyObj.getAsString());
}
}
BasicGetAttributesRequest request; BasicGetAttributesRequest request;
if (clientScope) { if (clientScope) {
request = new BasicGetAttributesRequest(requestId, Collections.singleton(key), null); request = new BasicGetAttributesRequest(requestId, keys, null);
} else { } else {
request = new BasicGetAttributesRequest(requestId, null, Collections.singleton(key)); request = new BasicGetAttributesRequest(requestId, null, keys);
} }
GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName); GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(), processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request))); new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
ack(msg);
} else { } else {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
} }
@ -251,7 +258,7 @@ public class GatewaySessionCtx {
} }
private void ack(MqttPublishMessage msg) { private void ack(MqttPublishMessage msg) {
if(msg.variableHeader().messageId() > 0) { if (msg.variableHeader().messageId() > 0) {
writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(msg.variableHeader().messageId())); writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(msg.variableHeader().messageId()));
} }
} }

2
transport/pom.xml

@ -28,7 +28,7 @@
<packaging>pom</packaging> <packaging>pom</packaging>
<name>Thingsboard Server Transport Modules</name> <name>Thingsboard Server Transport Modules</name>
<url>http://thingsboard.org</url> <url>https://thingsboard.io</url>
<properties> <properties>
<main.dir>${basedir}/..</main.dir> <main.dir>${basedir}/..</main.dir>

1
ui/package.json

@ -30,6 +30,7 @@
"angular-material": "1.1.1", "angular-material": "1.1.1",
"angular-material-data-table": "^0.10.9", "angular-material-data-table": "^0.10.9",
"angular-material-icons": "^0.7.1", "angular-material-icons": "^0.7.1",
"angular-material-expansion-panel": "^0.7.2",
"angular-messages": "1.5.8", "angular-messages": "1.5.8",
"angular-route": "1.5.8", "angular-route": "1.5.8",
"angular-sanitize": "1.5.8", "angular-sanitize": "1.5.8",

2
ui/pom.xml

@ -28,7 +28,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Thingsboard Server UI</name> <name>Thingsboard Server UI</name>
<url>http://thingsboard.org</url> <url>https://thingsboard.io</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

16
ui/src/app/api/login.service.js

@ -65,8 +65,8 @@ function LoginService($http, $q) {
function sendResetPasswordLink(email) { function sendResetPasswordLink(email) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/noauth/resetPasswordByEmail?email=' + email; var url = '/api/noauth/resetPasswordByEmail';
$http.post(url, null).then(function success(response) { $http.post(url, {email: email}).then(function success(response) {
deferred.resolve(response); deferred.resolve(response);
}, function fail() { }, function fail() {
deferred.reject(); deferred.reject();
@ -76,8 +76,8 @@ function LoginService($http, $q) {
function resetPassword(resetToken, password) { function resetPassword(resetToken, password) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/noauth/resetPassword?resetToken=' + resetToken + '&password=' + password; var url = '/api/noauth/resetPassword';
$http.post(url, null).then(function success(response) { $http.post(url, {resetToken: resetToken, password: password}).then(function success(response) {
deferred.resolve(response); deferred.resolve(response);
}, function fail() { }, function fail() {
deferred.reject(); deferred.reject();
@ -87,8 +87,8 @@ function LoginService($http, $q) {
function activate(activateToken, password) { function activate(activateToken, password) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/noauth/activate?activateToken=' + activateToken + '&password=' + password; var url = '/api/noauth/activate';
$http.post(url, null).then(function success(response) { $http.post(url, {activateToken: activateToken, password: password}).then(function success(response) {
deferred.resolve(response); deferred.resolve(response);
}, function fail() { }, function fail() {
deferred.reject(); deferred.reject();
@ -98,8 +98,8 @@ function LoginService($http, $q) {
function changePassword(currentPassword, newPassword) { function changePassword(currentPassword, newPassword) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/auth/changePassword?currentPassword=' + currentPassword + '&newPassword=' + newPassword; var url = '/api/auth/changePassword';
$http.post(url, null).then(function success(response) { $http.post(url, {currentPassword: currentPassword, newPassword: newPassword}).then(function success(response) {
deferred.resolve(response); deferred.resolve(response);
}, function fail() { }, function fail() {
deferred.reject(); deferred.reject();

19
ui/src/app/api/user.service.js

@ -302,7 +302,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
$rootScope.forceFullscreen = true; $rootScope.forceFullscreen = true;
fetchAllowedDashboardIds(); fetchAllowedDashboardIds();
} else if (currentUser.userId) { } else if (currentUser.userId) {
getUser(currentUser.userId).then( getUser(currentUser.userId, true).then(
function success(user) { function success(user) {
currentUserDetails = user; currentUserDetails = user;
updateUserLang(); updateUserLang();
@ -319,6 +319,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
}, },
function fail() { function fail() {
deferred.reject(); deferred.reject();
logout();
} }
) )
} else { } else {
@ -414,19 +415,19 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
} }
$http.post(url, user).then(function success(response) { $http.post(url, user).then(function success(response) {
deferred.resolve(response.data); deferred.resolve(response.data);
}, function fail(response) { }, function fail() {
deferred.reject(response.data); deferred.reject();
}); });
return deferred.promise; return deferred.promise;
} }
function getUser(userId) { function getUser(userId, ignoreErrors) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/user/' + userId; var url = '/api/user/' + userId;
$http.get(url).then(function success(response) { $http.get(url, { ignoreErrors: ignoreErrors }).then(function success(response) {
deferred.resolve(response.data); deferred.resolve(response.data);
}, function fail(response) { }, function fail() {
deferred.reject(response.data); deferred.reject();
}); });
return deferred.promise; return deferred.promise;
} }
@ -436,8 +437,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
var url = '/api/user/' + userId; var url = '/api/user/' + userId;
$http.delete(url).then(function success() { $http.delete(url).then(function success() {
deferred.resolve(); deferred.resolve();
}, function fail(response) { }, function fail() {
deferred.reject(response.data); deferred.reject();
}); });
return deferred.promise; return deferred.promise;
} }

3
ui/src/app/api/widget.service.js

@ -21,6 +21,7 @@ import thingsboardLedLight from '../components/led-light.directive';
import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget'; import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget';
import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget'; import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget';
import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget'; import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget';
import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget';
import thingsboardRpcWidgets from '../widget/lib/rpc'; import thingsboardRpcWidgets from '../widget/lib/rpc';
@ -42,7 +43,7 @@ import thingsboardTypes from '../common/types.constant';
import thingsboardUtils from '../common/utils.service'; import thingsboardUtils from '../common/utils.service';
export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget, export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils]) thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardExtensionsTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils])
.factory('widgetService', WidgetService) .factory('widgetService', WidgetService)
.name; .name;

3
ui/src/app/app.js

@ -39,6 +39,7 @@ import uiRouter from 'angular-ui-router';
import angularJwt from 'angular-jwt'; import angularJwt from 'angular-jwt';
import 'angular-drag-and-drop-lists'; import 'angular-drag-and-drop-lists';
import mdDataTable from 'angular-material-data-table'; import mdDataTable from 'angular-material-data-table';
import 'angular-material-expansion-panel';
import ngTouch from 'angular-touch'; import ngTouch from 'angular-touch';
import 'angular-carousel'; import 'angular-carousel';
import 'clipboard'; import 'clipboard';
@ -82,6 +83,7 @@ import 'md-color-picker/dist/mdColorPicker.min.css';
import 'mdPickers/dist/mdPickers.min.css'; import 'mdPickers/dist/mdPickers.min.css';
import 'angular-hotkeys/build/hotkeys.min.css'; import 'angular-hotkeys/build/hotkeys.min.css';
import 'angular-carousel/dist/angular-carousel.min.css'; import 'angular-carousel/dist/angular-carousel.min.css';
import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css';
import '../scss/main.scss'; import '../scss/main.scss';
import AppConfig from './app.config'; import AppConfig from './app.config';
@ -103,6 +105,7 @@ angular.module('thingsboard', [
angularJwt, angularJwt,
'dndLists', 'dndLists',
mdDataTable, mdDataTable,
'material.components.expansionPanels',
ngTouch, ngTouch,
'angular-carousel', 'angular-carousel',
'ngclipboard', 'ngclipboard',

2
ui/src/app/asset/asset-card.tpl.html

@ -16,7 +16,7 @@
--> -->
<div flex layout="column" style="margin-top: -10px;"> <div flex layout="column" style="margin-top: -10px;">
<div flex style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div> <div style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'asset.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div> <div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'asset.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
<div class="tb-small" ng-show="vm.isPublic()">{{'asset.public' | translate}}</div> <div class="tb-small" ng-show="vm.isPublic()">{{'asset.public' | translate}}</div>
</div> </div>

4
ui/src/app/asset/assign-to-customer.controller.js

@ -77,8 +77,8 @@ export default function AssignAssetToCustomerController(customerService, assetSe
function assign() { function assign() {
var tasks = []; var tasks = [];
for (var assetId in assetIds) { for (var i=0;i<assetIds.length;i++) {
tasks.push(assetService.assignAssetToCustomer(vm.customers.selection.id.id, assetIds[assetId])); tasks.push(assetService.assignAssetToCustomer(vm.customers.selection.id.id, assetIds[i]));
} }
$q.all(tasks).then(function () { $q.all(tasks).then(function () {
$mdDialog.hide(); $mdDialog.hide();

18
ui/src/app/common/dashboard-utils.service.js

@ -425,12 +425,26 @@ function DashboardUtils(types, utils, timeService) {
var prevColumns = prevGridSettings ? prevGridSettings.columns : 24; var prevColumns = prevGridSettings ? prevGridSettings.columns : 24;
var ratio = gridSettings.columns / prevColumns; var ratio = gridSettings.columns / prevColumns;
layout.gridSettings = gridSettings; layout.gridSettings = gridSettings;
var maxRow = 0;
for (var w in layout.widgets) { for (var w in layout.widgets) {
var widget = layout.widgets[w]; var widget = layout.widgets[w];
maxRow = Math.max(maxRow, widget.row + widget.sizeY);
}
var newMaxRow = Math.round(maxRow * ratio);
for (w in layout.widgets) {
widget = layout.widgets[w];
if (widget.row + widget.sizeY == maxRow) {
widget.row = Math.round(widget.row * ratio);
widget.sizeY = newMaxRow - widget.row;
} else {
widget.row = Math.round(widget.row * ratio);
widget.sizeY = Math.round(widget.sizeY * ratio);
}
widget.sizeX = Math.round(widget.sizeX * ratio); widget.sizeX = Math.round(widget.sizeX * ratio);
widget.sizeY = Math.round(widget.sizeY * ratio);
widget.col = Math.round(widget.col * ratio); widget.col = Math.round(widget.col * ratio);
widget.row = Math.round(widget.row * ratio); if (widget.col + widget.sizeX > gridSettings.columns) {
widget.sizeX = gridSettings.columns - widget.col;
}
} }
} }

47
ui/src/app/common/types.constant.js

@ -317,6 +317,53 @@ export default angular.module('thingsboard.types', [])
name: "event.type-stats" name: "event.type-stats"
} }
}, },
extensionType: {
http: "HTTP",
mqtt: "MQTT",
opc: "OPC UA"
},
extensionValueType: {
string: 'value.string',
long: 'value.long',
double: 'value.double',
boolean: 'value.boolean'
},
extensionTransformerType: {
toDouble: 'extension.to-double',
custom: 'extension.custom'
},
mqttConverterTypes: {
json: 'extension.converter-json',
custom: 'extension.custom'
},
mqttCredentialTypes: {
anonymous: {
value: "anonymous",
name: "extension.anonymous"
},
basic: {
value: "basic",
name: "extension.basic"
},
pem: {
value: "cert.PEM",
name: "extension.pem"
}
},
extensionOpcSecurityTypes: {
Basic128Rsa15: "Basic128Rsa15",
Basic256: "Basic256",
Basic256Sha256: "Basic256Sha256",
None: "None"
},
extensionIdentityType: {
anonymous: "extension.anonymous",
username: "extension.username"
},
extensionKeystoreType: {
PKCS12: "PKCS12",
JKS: "JKS"
},
latestTelemetry: { latestTelemetry: {
value: "LATEST_TELEMETRY", value: "LATEST_TELEMETRY",
name: "attribute.scope-latest-telemetry", name: "attribute.scope-latest-telemetry",

6
ui/src/app/components/confirm-on-exit.directive.js

@ -18,17 +18,17 @@ export default angular.module('thingsboard.directives.confirmOnExit', [])
.name; .name;
/*@ngInject*/ /*@ngInject*/
function ConfirmOnExit($state, $mdDialog, $window, $filter) { function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
return { return {
link: function ($scope) { link: function ($scope) {
$window.onbeforeunload = function () { $window.onbeforeunload = function () {
if (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty) { if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
return $filter('translate')('confirm-on-exit.message'); return $filter('translate')('confirm-on-exit.message');
} }
} }
$scope.$on('$stateChangeStart', function (event, next, current, params) { $scope.$on('$stateChangeStart', function (event, next, current, params) {
if (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty) { if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
event.preventDefault(); event.preventDefault();
var confirm = $mdDialog.confirm() var confirm = $mdDialog.confirm()
.title($filter('translate')('confirm-on-exit.title')) .title($filter('translate')('confirm-on-exit.title'))

177
ui/src/app/components/dashboard.directive.js

@ -140,6 +140,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
vm.widgetLayoutInfo = { vm.widgetLayoutInfo = {
}; };
vm.widgetIds = [];
vm.widgetItemMap = { vm.widgetItemMap = {
sizeX: 'vm.widgetLayoutInfo[widget.id].sizeX', sizeX: 'vm.widgetLayoutInfo[widget.id].sizeX',
sizeY: 'vm.widgetLayoutInfo[widget.id].sizeY', sizeY: 'vm.widgetLayoutInfo[widget.id].sizeY',
@ -233,73 +235,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
removeResizeListener(gridsterParent[0], onGridsterParentResize); // eslint-disable-line no-undef removeResizeListener(gridsterParent[0], onGridsterParentResize); // eslint-disable-line no-undef
}); });
watchWidgets();
function onGridsterParentResize() { function onGridsterParentResize() {
if (gridsterParent.height() && autofillHeight()) { if (gridsterParent.height() && autofillHeight()) {
updateMobileOpts(); updateMobileOpts();
} }
} }
function watchWidgets() {
$scope.widgetsCollectionWatch = $scope.$watchCollection('vm.widgets', function () {
if (vm.skipInitialWidgetsWatch) {
$timeout(function() { vm.skipInitialWidgetsWatch = false; });
return;
}
var ids = [];
for (var i=0;i<vm.widgets.length;i++) {
var widget = vm.widgets[i];
if (!widget.id) {
widget.id = utils.guid();
}
ids.push(widget.id);
var layoutInfoObject = vm.widgetLayoutInfo[widget.id];
if (!layoutInfoObject) {
layoutInfoObject = {
widget: widget
};
Object.defineProperty(layoutInfoObject, 'sizeX', {
get: function() { return widgetSizeX(this.widget) },
set: function(newSizeX) { setWidgetSizeX(this.widget, newSizeX)}
});
Object.defineProperty(layoutInfoObject, 'sizeY', {
get: function() { return widgetSizeY(this.widget) },
set: function(newSizeY) { setWidgetSizeY(this.widget, newSizeY)}
});
Object.defineProperty(layoutInfoObject, 'row', {
get: function() { return widgetRow(this.widget) },
set: function(newRow) { setWidgetRow(this.widget, newRow)}
});
Object.defineProperty(layoutInfoObject, 'col', {
get: function() { return widgetCol(this.widget) },
set: function(newCol) { setWidgetCol(this.widget, newCol)}
});
vm.widgetLayoutInfo[widget.id] = layoutInfoObject;
}
}
for (var widgetId in vm.widgetLayoutInfo) {
if (ids.indexOf(widgetId) === -1) {
delete vm.widgetLayoutInfo[widgetId];
}
}
$mdUtil.nextTick(function () {
sortWidgets();
if (autofillHeight()) {
updateMobileOpts();
}
});
});
}
function stopWatchWidgets() {
if ($scope.widgetsCollectionWatch) {
$scope.widgetsCollectionWatch();
$scope.widgetsCollectionWatch = null;
}
}
//TODO: widgets visibility //TODO: widgets visibility
/*gridsterParent.scroll(function () { /*gridsterParent.scroll(function () {
updateVisibleRect(); updateVisibleRect();
@ -344,30 +285,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
return isMobileSize; return isMobileSize;
} }
$scope.$watch(function() { return $mdMedia('gt-sm'); }, function() {
updateMobileOpts();
});
$scope.$watch('vm.isMobile', function () {
updateMobileOpts();
});
$scope.$watch('vm.autofillHeight', function () {
updateMobileOpts();
});
$scope.$watch('vm.mobileAutofillHeight', function () {
updateMobileOpts();
});
$scope.$watch('vm.mobileRowHeight', function () {
updateMobileOpts();
});
$scope.$watch('vm.isMobileDisabled', function () {
updateMobileOpts();
});
$scope.$watch('vm.columns', function () { $scope.$watch('vm.columns', function () {
var columns = vm.columns ? vm.columns : 24; var columns = vm.columns ? vm.columns : 24;
if (vm.gridsterOpts.columns != columns) { if (vm.gridsterOpts.columns != columns) {
@ -381,6 +298,19 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
} }
}); });
$scope.$watch(function() {
return $mdMedia('gt-sm') + ',' + vm.isMobile + ',' + vm.isMobileDisabled;
}, function() {
updateMobileOpts();
sortWidgets();
});
$scope.$watch(function() {
return vm.autofillHeight + ',' + vm.mobileAutofillHeight + ',' + vm.mobileRowHeight;
}, function () {
updateMobileOpts();
});
$scope.$watch('vm.margins', function () { $scope.$watch('vm.margins', function () {
var margins = vm.margins ? vm.margins : [10, 10]; var margins = vm.margins ? vm.margins : [10, 10];
if (!angular.equals(vm.gridsterOpts.margins, margins)) { if (!angular.equals(vm.gridsterOpts.margins, margins)) {
@ -407,9 +337,70 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
} }
}); });
$scope.$watchCollection('vm.widgets', function () {
var ids = [];
for (var i=0;i<vm.widgets.length;i++) {
var widget = vm.widgets[i];
if (!widget.id) {
widget.id = utils.guid();
}
ids.push(widget.id);
}
ids.sort(function (id1, id2) {
return id1.localeCompare(id2);
});
if (angular.equals(ids, vm.widgetIds)) {
return;
}
vm.widgetIds = ids;
for (i=0;i<vm.widgets.length;i++) {
widget = vm.widgets[i];
var layoutInfoObject = vm.widgetLayoutInfo[widget.id];
if (!layoutInfoObject) {
layoutInfoObject = {
widget: widget
};
Object.defineProperty(layoutInfoObject, 'sizeX', {
get: function() { return widgetSizeX(this.widget) },
set: function(newSizeX) { setWidgetSizeX(this.widget, newSizeX)}
});
Object.defineProperty(layoutInfoObject, 'sizeY', {
get: function() { return widgetSizeY(this.widget) },
set: function(newSizeY) { setWidgetSizeY(this.widget, newSizeY)}
});
Object.defineProperty(layoutInfoObject, 'row', {
get: function() { return widgetRow(this.widget) },
set: function(newRow) { setWidgetRow(this.widget, newRow)}
});
Object.defineProperty(layoutInfoObject, 'col', {
get: function() { return widgetCol(this.widget) },
set: function(newCol) { setWidgetCol(this.widget, newCol)}
});
vm.widgetLayoutInfo[widget.id] = layoutInfoObject;
}
}
for (var widgetId in vm.widgetLayoutInfo) {
if (ids.indexOf(widgetId) === -1) {
delete vm.widgetLayoutInfo[widgetId];
}
}
sortWidgets();
$mdUtil.nextTick(function () {
if (autofillHeight()) {
updateMobileOpts();
}
});
});
$scope.$watch('vm.widgetLayouts', function () {
updateMobileOpts();
sortWidgets();
});
$scope.$on('gridster-resized', function (event, sizes, theGridster) { $scope.$on('gridster-resized', function (event, sizes, theGridster) {
if (checkIsLocalGridsterElement(theGridster)) { if (checkIsLocalGridsterElement(theGridster)) {
vm.gridster = theGridster; vm.gridster = theGridster;
setupGridster(vm.gridster);
vm.isResizing = false; vm.isResizing = false;
//TODO: widgets visibility //TODO: widgets visibility
//updateVisibleRect(false, true); //updateVisibleRect(false, true);
@ -419,6 +410,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
$scope.$on('gridster-mobile-changed', function (event, theGridster) { $scope.$on('gridster-mobile-changed', function (event, theGridster) {
if (checkIsLocalGridsterElement(theGridster)) { if (checkIsLocalGridsterElement(theGridster)) {
vm.gridster = theGridster; vm.gridster = theGridster;
setupGridster(vm.gridster);
detectRowSize(vm.gridster.isMobile).then( detectRowSize(vm.gridster.isMobile).then(
function(rowHeight) { function(rowHeight) {
if (vm.gridsterOpts.rowHeight != rowHeight) { if (vm.gridsterOpts.rowHeight != rowHeight) {
@ -517,18 +509,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
loadDashboard(); loadDashboard();
function sortWidgets() { function sortWidgets() {
stopWatchWidgets();
vm.widgets.sort(function (widget1, widget2) { vm.widgets.sort(function (widget1, widget2) {
var row1 = widgetOrder(widget1); var row1 = widgetOrder(widget1);
var row2 = widgetOrder(widget2); var row2 = widgetOrder(widget2);
var res = row1 - row2; var res = row1 - row2;
if (res === 0) { if (res === 0) {
res = widget1.col - widget2.col; res = widgetCol(widget1) - widgetCol(widget2);
} }
return res; return res;
}); });
vm.skipInitialWidgetsWatch = true;
watchWidgets();
} }
function reload() { function reload() {
@ -1037,6 +1026,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
$scope.gridsterScopeWatcher = null; $scope.gridsterScopeWatcher = null;
var gridsterScope = gridsterElement.scope(); var gridsterScope = gridsterElement.scope();
vm.gridster = gridsterScope.gridster; vm.gridster = gridsterScope.gridster;
setupGridster(vm.gridster);
if (vm.onInit) { if (vm.onInit) {
vm.onInit({dashboard: vm}); vm.onInit({dashboard: vm});
} }
@ -1046,6 +1036,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
}); });
} }
function setupGridster(gridster) {
if (gridster) {
if (!gridster.origMoveOverlappingItems) {
gridster.origMoveOverlappingItems = gridster.moveOverlappingItems;
gridster.moveOverlappingItems = () => {};
}
}
}
function loading() { function loading() {
return !vm.ignoreLoading && $rootScope.loading; return !vm.ignoreLoading && $rootScope.loading;
} }

5
ui/src/app/components/details-sidenav.scss

@ -16,7 +16,10 @@
@import '../../scss/constants'; @import '../../scss/constants';
.tb-details-title { .tb-details-title {
font-size: 1.600rem; font-size: 1.000rem;
@media (min-width: $layout-breakpoint-gt-sm) {
font-size: 1.600rem;
}
font-weight: 400; font-weight: 400;
text-transform: uppercase; text-transform: uppercase;
margin: 20px 8px 0 0; margin: 20px 8px 0 0;

2
ui/src/app/components/js-func.scss

@ -22,7 +22,7 @@ tb-js-func {
border: 1px solid #C0C0C0; border: 1px solid #C0C0C0;
height: 100%; height: 100%;
#tb-javascript-input { #tb-javascript-input {
min-width: 400px; min-width: 200px;
min-height: 200px; min-height: 200px;
width: 100%; width: 100%;
height: 100%; height: 100%;

2
ui/src/app/components/js-func.tpl.html

@ -19,7 +19,7 @@
<div layout="row" layout-align="start center" style="height: 40px;"> <div layout="row" layout-align="start center" style="height: 40px;">
<span style="font-style: italic;">function({{ functionArgsString }}) {</span> <span style="font-style: italic;">function({{ functionArgsString }}) {</span>
<span flex></span> <span flex></span>
<md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button> <div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div>
</div> </div>
<div flex id="tb-javascript-panel" class="tb-js-func-panel" layout="column"> <div flex id="tb-javascript-panel" class="tb-js-func-panel" layout="column">
<div flex id="tb-javascript-input" <div flex id="tb-javascript-input"

4
ui/src/app/device/assign-to-customer.controller.js

@ -77,8 +77,8 @@ export default function AssignDeviceToCustomerController(customerService, device
function assign() { function assign() {
var tasks = []; var tasks = [];
for (var deviceId in deviceIds) { for (var i=0;i<deviceIds.length;i++) {
tasks.push(deviceService.assignDeviceToCustomer(vm.customers.selection.id.id, deviceIds[deviceId])); tasks.push(deviceService.assignDeviceToCustomer(vm.customers.selection.id.id, deviceIds[i]));
} }
$q.all(tasks).then(function () { $q.all(tasks).then(function () {
$mdDialog.hide(); $mdDialog.hide();

2
ui/src/app/device/device-card.tpl.html

@ -16,7 +16,7 @@
--> -->
<div flex layout="column" style="margin-top: -10px;"> <div flex layout="column" style="margin-top: -10px;">
<div flex style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div> <div style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div> <div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
<div class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div> <div class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div>
</div> </div>

7
ui/src/app/device/devices.tpl.html

@ -67,4 +67,11 @@
entity-type="{{vm.types.entityType.device}}"> entity-type="{{vm.types.entityType.device}}">
</tb-relation-table> </tb-relation-table>
</md-tab> </md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.operatingItem().additionalInfo.gateway" md-on-select="vm.grid.triggerResize()" label="{{ 'extension.extensions' | translate }}">
<tb-extension-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-name="vm.grid.operatingItem().name"
entity-type="{{vm.types.entityType.device}}">
</tb-extension-table>
</md-tab>
</tb-grid> </tb-grid>

4
ui/src/app/entity/attribute/attribute-table.tpl.html

@ -16,7 +16,7 @@
--> -->
<md-content flex class="md-padding tb-absolute-fill" layout="column"> <md-content flex class="md-padding tb-absolute-fill" layout="column">
<section layout="row" ng-show="!disableAttributeScopeSelection"> <section ng-show="!disableAttributeScopeSelection">
<md-input-container class="md-block" style="width: 200px;"> <md-input-container class="md-block" style="width: 200px;">
<label translate>attribute.attributes-scope</label> <label translate>attribute.attributes-scope</label>
<md-select ng-model="attributeScope" ng-disabled="loading() || attributeScopeSelectionReadonly"> <md-select ng-model="attributeScope" ng-disabled="loading() || attributeScopeSelectionReadonly">
@ -26,7 +26,7 @@
</md-select> </md-select>
</md-input-container> </md-input-container>
</section> </section>
<div layout="column" class="md-whiteframe-z1" ng-class="{flex: mode==='widget'}"> <div class="md-whiteframe-z1" ng-class="{flex: mode==='widget'}">
<md-toolbar class="md-table-toolbar md-default" ng-show="mode==='default' <md-toolbar class="md-table-toolbar md-default" ng-show="mode==='default'
&& !selectedAttributes.length && !selectedAttributes.length
&& query.search === null"> && query.search === null">

4
ui/src/app/entity/relation/relation-dialog.controller.js

@ -79,10 +79,8 @@ export default function RelationDialogController($scope, $mdDialog, types, entit
}); });
function updateEditorSize(element) { function updateEditorSize(element) {
var newWidth = 600;
var newHeight = 200; var newHeight = 200;
angular.element('#tb-relation-additional-info', element).height(newHeight.toString() + "px") angular.element('#tb-relation-additional-info', element).height(newHeight.toString() + "px");
.width(newWidth.toString() + "px");
vm.editor.resize(); vm.editor.resize();
} }

2
ui/src/app/entity/relation/relation-dialog.scss

@ -19,7 +19,7 @@
border: 1px solid #C0C0C0; border: 1px solid #C0C0C0;
height: 100%; height: 100%;
#tb-relation-additional-info { #tb-relation-additional-info {
min-width: 600px; min-width: 200px;
min-height: 200px; min-height: 200px;
width: 100%; width: 100%;
height: 100%; height: 100%;

2
ui/src/app/entity/relation/relation-dialog.tpl.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<md-dialog aria-label="{{ (vm.isAdd ? 'relation.add' : 'relation.edit' ) | translate }}" style="min-width: 400px;"> <md-dialog aria-label="{{ (vm.isAdd ? 'relation.add' : 'relation.edit' ) | translate }}" style="min-width: 600px;">
<form name="theForm" ng-submit="vm.save()"> <form name="theForm" ng-submit="vm.save()">
<md-toolbar> <md-toolbar>
<div class="md-toolbar-tools"> <div class="md-toolbar-tools">

298
ui/src/app/extension/extension-dialog.controller.js

@ -0,0 +1,298 @@
/*
* Copyright © 2016-2017 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 beautify from 'js-beautify';
const js_beautify = beautify.js;
/*@ngInject*/
export default function ExtensionDialogController($scope, $mdDialog, $translate, isAdd, allExtensions, entityId, entityType, extension, types, attributeService) {
var vm = this;
vm.types = types;
vm.isAdd = isAdd;
vm.entityType = entityType;
vm.entityId = entityId;
vm.allExtensions = allExtensions;
if (extension) {
vm.extension = angular.copy(extension);
editTransformers(vm.extension);
} else {
vm.extension = {};
}
vm.extensionTypeChange = function () {
if (vm.extension.type === "HTTP") {
vm.extension.configuration = {
"converterConfigurations": []
};
}
if (vm.extension.type === "MQTT") {
vm.extension.configuration = {
"brokers": []
};
}
if (vm.extension.type === "OPC UA") {
vm.extension.configuration = {
"servers": []
};
}
};
vm.cancel = cancel;
function cancel() {
$mdDialog.cancel();
}
vm.save = save;
function save() {
let $errorElement = angular.element('[name=theForm]').find('.ng-invalid');
if ($errorElement.length) {
let $mdDialogScroll = angular.element('md-dialog-content').scrollTop();
let $mdDialogTop = angular.element('md-dialog-content').offset().top;
let $errorElementTop = angular.element('[name=theForm]').find('.ng-invalid').eq(0).offset().top;
if ($errorElementTop !== $mdDialogTop) {
angular.element('md-dialog-content').animate({
scrollTop: $mdDialogScroll + ($errorElementTop - $mdDialogTop) - 50
}, 500);
$errorElement.eq(0).focus();
}
} else {
if(vm.isAdd) {
vm.allExtensions.push(vm.extension);
} else {
var index = vm.allExtensions.indexOf(extension);
if(index > -1) {
vm.allExtensions[index] = vm.extension;
}
}
$mdDialog.hide();
saveTransformers();
var editedValue = angular.toJson(vm.allExtensions);
attributeService
.saveEntityAttributes(
vm.entityType,
vm.entityId,
types.attributesScope.shared.value,
[{key:"configuration", value:editedValue}]
)
.then(function success() {
});
}
}
vm.validateId = function() {
var coincidenceArray = vm.allExtensions.filter(function(ext) {
return ext.id == vm.extension.id;
});
if(coincidenceArray.length) {
if(!vm.isAdd) {
if(coincidenceArray[0].id == extension.id) {
$scope.theForm.extensionId.$setValidity('uniqueIdValidation', true);
} else {
$scope.theForm.extensionId.$setValidity('uniqueIdValidation', false);
}
} else {
$scope.theForm.extensionId.$setValidity('uniqueIdValidation', false);
}
} else {
$scope.theForm.extensionId.$setValidity('uniqueIdValidation', true);
}
};
function saveTransformers() {
if(vm.extension.type == types.extensionType.http) {
var config = vm.extension.configuration.converterConfigurations;
if(config && config.length > 0) {
for(let i=0;i<config.length;i++) {
for(let j=0;j<config[i].converters.length;j++){
for(let k=0;k<config[i].converters[j].attributes.length;k++){
if(config[i].converters[j].attributes[k].transformerType == "toDouble"){
config[i].converters[j].attributes[k].transformer = {type: "intToDouble"};
}
delete config[i].converters[j].attributes[k].transformerType;
}
for(let l=0;l<config[i].converters[j].timeseries.length;l++) {
if(config[i].converters[j].timeseries[l].transformerType == "toDouble"){
config[i].converters[j].timeseries[l].transformer = {type: "intToDouble"};
}
delete config[i].converters[j].timeseries[l].transformerType;
}
}
}
}
}
if(vm.extension.type == types.extensionType.mqtt) {
var brokers = vm.extension.configuration.brokers;
if(brokers && brokers.length > 0) {
for(let i=0;i<brokers.length;i++) {
if(brokers[i].mapping && brokers[i].mapping.length > 0) {
for(let j=0;j<brokers[i].mapping.length;j++) {
if(brokers[i].mapping[j].converterType == "json") {
delete brokers[i].mapping[j].converter.nameExp;
delete brokers[i].mapping[j].converter.typeExp;
}
delete brokers[i].mapping[j].converterType;
}
}
if(brokers[i].connectRequests && brokers[i].connectRequests.length > 0) {
for(let j=0;j<brokers[i].connectRequests.length;j++) {
delete brokers[i].connectRequests[j].nameExp;
}
}
if(brokers[i].disconnectRequests && brokers[i].disconnectRequests.length > 0) {
for(let j=0;j<brokers[i].disconnectRequests.length;j++) {
delete brokers[i].disconnectRequests[j].nameExp;
}
}
if(brokers[i].attributeRequests && brokers[i].attributeRequests.length > 0) {
for(let j=0;j<brokers[i].attributeRequests.length;j++) {
delete brokers[i].attributeRequests[j].nameExp;
}
for(let j=0;j<brokers[i].attributeRequests.length;j++) {
delete brokers[i].attributeRequests[j].attrKey;
}
for(let j=0;j<brokers[i].attributeRequests.length;j++) {
delete brokers[i].attributeRequests[j].requestId;
}
}
}
}
}
}
function editTransformers(extension) {
if(extension.type == types.extensionType.http) {
var config = extension.configuration.converterConfigurations;
for(let i=0;i<config.length;i++) {
for(let j=0;j<config[i].converters.length;j++){
for(let k=0;k<config[i].converters[j].attributes.length;k++){
if(config[i].converters[j].attributes[k].transformer){
if(config[i].converters[j].attributes[k].transformer.type == "intToDouble"){
config[i].converters[j].attributes[k].transformerType = "toDouble";
} else {
config[i].converters[j].attributes[k].transformerType = "custom";
config[i].converters[j].attributes[k].transformer = js_beautify(config[i].converters[j].attributes[k].transformer, {indent_size: 4});
}
}
}
for(let l=0;l<config[i].converters[j].timeseries.length;l++) {
if(config[i].converters[j].timeseries[l].transformer){
if(config[i].converters[j].timeseries[l].transformer.type == "intToDouble"){
config[i].converters[j].timeseries[l].transformerType = "toDouble";
} else {
config[i].converters[j].timeseries[l].transformerType = "custom";
config[i].converters[j].timeseries[l].transformer = js_beautify(config[i].converters[j].timeseries[l].transformer, {indent_size: 4});
}
}
}
}
}
}
if(extension.type == types.extensionType.mqtt) {
var brokers = extension.configuration.brokers;
for(let i=0;i<brokers.length;i++) {
if(brokers[i].mapping && brokers[i].mapping.length > 0) {
for(let j=0;j<brokers[i].mapping.length;j++) {
if(brokers[i].mapping[j].converter.type == "json") {
if(brokers[i].mapping[j].converter.deviceNameTopicExpression) {
brokers[i].mapping[j].converter.nameExp = "deviceNameTopicExpression";
} else {
brokers[i].mapping[j].converter.nameExp = "deviceNameJsonExpression";
}
if(brokers[i].mapping[j].converter.deviceTypeTopicExpression) {
brokers[i].mapping[j].converter.typeExp = "deviceTypeTopicExpression";
} else {
brokers[i].mapping[j].converter.typeExp = "deviceTypeJsonExpression";
}
brokers[i].mapping[j].converterType = "json";
} else {
brokers[i].mapping[j].converterType = "custom";
}
}
}
if(brokers[i].connectRequests && brokers[i].connectRequests.length > 0) {
for(let j=0;j<brokers[i].connectRequests.length;j++) {
if(brokers[i].connectRequests[j].deviceNameTopicExpression) {
brokers[i].connectRequests[j].nameExp = "deviceNameTopicExpression";
} else {
brokers[i].connectRequests[j].nameExp = "deviceNameJsonExpression";
}
}
}
if(brokers[i].disconnectRequests && brokers[i].disconnectRequests.length > 0) {
for(let j=0;j<brokers[i].disconnectRequests.length;j++) {
if(brokers[i].disconnectRequests[j].deviceNameTopicExpression) {
brokers[i].disconnectRequests[j].nameExp = "deviceNameTopicExpression";
} else {
brokers[i].disconnectRequests[j].nameExp = "deviceNameJsonExpression";
}
}
}
if(brokers[i].attributeRequests && brokers[i].attributeRequests.length > 0) {
for(let j=0;j<brokers[i].attributeRequests.length;j++) {
if(brokers[i].attributeRequests[j].deviceNameTopicExpression) {
brokers[i].attributeRequests[j].nameExp = "deviceNameTopicExpression";
} else {
brokers[i].attributeRequests[j].nameExp = "deviceNameJsonExpression";
}
if(brokers[i].attributeRequests[j].attributeKeyTopicExpression) {
brokers[i].attributeRequests[j].attrKey = "attributeKeyTopicExpression";
} else {
brokers[i].attributeRequests[j].attrKey = "attributeKeyJsonExpression";
}
if(brokers[i].attributeRequests[j].requestIdTopicExpression) {
brokers[i].attributeRequests[j].requestId = "requestIdTopicExpression";
} else {
brokers[i].attributeRequests[j].requestId = "requestIdJsonExpression";
}
}
}
}
}
}
}
/*@ngInject*/
export function ParseToNull() {
var linker = function (scope, elem, attrs, ngModel) {
ngModel.$parsers.push(function(value) {
if(value === "") {
return null;
}
return value;
})
};
return {
restrict: "A",
link: linker,
require: "ngModel"
}
}

81
ui/src/app/extension/extension-dialog.tpl.html

@ -0,0 +1,81 @@
<!--
Copyright © 2016-2017 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.
-->
<md-dialog class="extensionDialog" aria-label="{{ (vm.isAdd ? 'extension.add' : 'extension.edit' ) | translate }}">
<form name="theForm" ng-submit="vm.save()" novalidate>
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>{{ vm.isAdd ? 'extension.add' : 'extension.edit'}}</h2>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.cancel()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!loading"></span>
<md-dialog-content>
<div class="md-dialog-content">
<md-content class="md-padding" layout="column">
<fieldset ng-disabled="loading">
<section flex layout="row">
<md-input-container flex="60" class="md-block" md-is-error="theForm.extensionId.$touched && theForm.extensionId.$invalid">
<label translate>extension.extension-id</label>
<input required name="extensionId" ng-model="vm.extension.id" ng-change="vm.validateId()">
<div ng-messages="theForm.extensionId.$error">
<div translate ng-message="required">extension.field-required</div>
<div translate ng-message="uniqueIdValidation">extension.unique-id-required</div>
</div>
</md-input-container>
<md-input-container flex="40" class="md-block" md-is-error="theForm.extensionType.$touched && theForm.extensionType.$invalid">
<label translate>extension.extension-type</label>
<md-select ng-disabled="!vm.isAdd" required name="extensionType" ng-change="vm.extensionTypeChange()" ng-model="vm.extension.type">
<md-option ng-repeat="(key,value) in vm.types.extensionType" ng-value="value">
{{value}}
</md-option>
</md-select>
<div ng-messages="theForm.extensionType.$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<div tb-extension-form-http config="vm.extension.configuration" is-add="vm.isAdd" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.http"></div>
<div tb-extension-form-mqtt config="vm.extension.configuration" is-add="vm.isAdd" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.mqtt"></div>
<div tb-extension-form-opc configuration="vm.extension.configuration" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.opc"></div>
</fieldset>
</md-content>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<md-button type="submit"
class="md-raised md-primary"
>
{{ (vm.isAdd ? 'action.add' : 'action.save') | translate }}
</md-button>
<md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}
</md-button>
</md-dialog-actions>
</form>
</md-dialog>

404
ui/src/app/extension/extension-table.directive.js

@ -0,0 +1,404 @@
/*
* Copyright © 2016-2017 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 'angular-material-data-table/dist/md-data-table.min.css';
import './extension-table.scss';
/* eslint-disable import/no-unresolved, import/default */
import extensionTableTemplate from './extension-table.tpl.html';
import extensionDialogTemplate from './extension-dialog.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import ExtensionDialogController from './extension-dialog.controller'
import $ from 'jquery';
/*@ngInject*/
export default function ExtensionTableDirective() {
return {
restrict: "E",
scope: true,
bindToController: {
entityId: '=',
entityType: '@',
inWidget: '@?',
ctx: '=?',
entityName: '='
},
controller: ExtensionTableController,
controllerAs: 'vm',
templateUrl: extensionTableTemplate
};
}
/*@ngInject*/
function ExtensionTableController($scope, $filter, $document, $translate, types, $mdDialog, attributeService, telemetryWebsocketService, importExport) {
let vm = this;
vm.extensions = [];
vm.allExtensions = [];
vm.selectedExtensions = [];
vm.extensionsCount = 0;
vm.query = {
order: 'id',
limit: 5,
page: 1,
search: null
};
vm.enterFilterMode = enterFilterMode;
vm.exitFilterMode = exitFilterMode;
vm.onReorder = onReorder;
vm.onPaginate = onPaginate;
vm.addExtension = addExtension;
vm.editExtension = editExtension;
vm.deleteExtension = deleteExtension;
vm.deleteExtensions = deleteExtensions;
vm.reloadExtensions = reloadExtensions;
vm.updateExtensions = updateExtensions;
$scope.$watch("vm.entityId", function(newVal) {
if (newVal) {
if ($scope.subscriber) {
telemetryWebsocketService.unsubscribe($scope.subscriber);
$scope.subscriber = null;
}
vm.subscribed = false;
vm.syncLastTime = $translate.instant('extension.sync.not-available');
subscribeForClientAttributes();
reloadExtensions();
}
});
$scope.$on('$destroy', function() {
if ($scope.subscriber) {
telemetryWebsocketService.unsubscribe($scope.subscriber);
$scope.subscriber = null;
}
});
$scope.$watch("vm.query.search", function(newVal, prevVal) {
if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
updateExtensions();
}
});
$scope.$watch('vm.selectedExtensions.length', function (newLength) {
var selectionMode = newLength ? true : false;
if (vm.ctx) {
if (selectionMode) {
vm.ctx.hideTitlePanel = true;
$scope.$emit("selectedExtensions", true);
} else if (vm.query.search == null) {
vm.ctx.hideTitlePanel = false;
$scope.$emit("selectedExtensions", false);
}
}
});
$scope.$on("showSearch", function($event, source) {
if(source.entityId == vm.entityId) {
enterFilterMode();
$scope.$emit("filterMode", true);
}
});
$scope.$on("refreshExtensions", function($event, source) {
if(source.entityId == vm.entityId) {
reloadExtensions();
}
});
$scope.$on("addExtension", function($event, source) {
if(source.entityId == vm.entityId) {
addExtension();
}
});
$scope.$on("exportExtensions", function($event, source) {
if(source.entityId == vm.entityId) {
vm.exportExtensions(source.entityName);
}
});
$scope.$on("importExtensions", function($event, source) {
if(source.entityId == vm.entityId) {
vm.importExtensions();
}
});
function enterFilterMode() {
vm.query.search = '';
if(vm.inWidget) {
vm.ctx.hideTitlePanel = true;
}
}
function exitFilterMode() {
vm.query.search = null;
updateExtensions();
if(vm.inWidget) {
vm.ctx.hideTitlePanel = false;
$scope.$emit("filterMode", false);
}
}
function onReorder() {
updateExtensions();
}
function onPaginate() {
updateExtensions();
}
function addExtension($event) {
if ($event) {
$event.stopPropagation();
}
openExtensionDialog($event);
}
function editExtension($event, extension) {
if ($event) {
$event.stopPropagation();
}
openExtensionDialog($event, extension);
}
function openExtensionDialog($event, extension) {
if ($event) {
$event.stopPropagation();
}
var isAdd = false;
if(!extension) {
isAdd = true;
}
$mdDialog.show({
controller: ExtensionDialogController,
controllerAs: 'vm',
templateUrl: extensionDialogTemplate,
parent: angular.element($document[0].body),
locals: {
isAdd: isAdd,
allExtensions: vm.allExtensions,
entityId: vm.entityId,
entityType: vm.entityType,
extension: extension
},
bindToController: true,
targetEvent: $event,
fullscreen: true,
skipHide: true
}).then(function() {
reloadExtensions();
}, function () {
});
}
function deleteExtension($event, extension) {
if ($event) {
$event.stopPropagation();
}
if(extension) {
var title = $translate.instant('extension.delete-extension-title', {extensionId: extension.id});
var content = $translate.instant('extension.delete-extension-text');
var confirm = $mdDialog.confirm()
.targetEvent($event)
.title(title)
.htmlContent(content)
.ariaLabel(title)
.cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function() {
var editedExtensions = vm.allExtensions.filter(function(ext) {
return ext.id !== extension.id;
});
var editedValue = angular.toJson(editedExtensions);
attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then(
function success() {
reloadExtensions();
}
);
});
}
}
function deleteExtensions($event) {
if ($event) {
$event.stopPropagation();
}
if (vm.selectedExtensions && vm.selectedExtensions.length > 0) {
var title = $translate.instant('extension.delete-extensions-title', {count: vm.selectedExtensions.length}, 'messageformat');
var content = $translate.instant('extension.delete-extensions-text');
var confirm = $mdDialog.confirm()
.targetEvent($event)
.title(title)
.htmlContent(content)
.ariaLabel(title)
.cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function () {
var editedExtensions = angular.copy(vm.allExtensions);
for (var i = 0; i < vm.selectedExtensions.length; i++) {
editedExtensions = editedExtensions.filter(function (ext) {
return ext.id !== vm.selectedExtensions[i].id;
});
}
var editedValue = angular.toJson(editedExtensions);
attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then(
function success() {
reloadExtensions();
}
);
});
}
}
function reloadExtensions() {
vm.subscribed = false;
vm.allExtensions.length = 0;
vm.extensions.length = 0;
vm.extensionsPromise = attributeService.getEntityAttributesValues(vm.entityType, vm.entityId, types.attributesScope.shared.value, ["configuration"]);
vm.extensionsPromise.then(
function success(data) {
if (data.length) {
vm.allExtensions = angular.fromJson(data[0].value);
} else {
vm.allExtensions = [];
}
vm.selectedExtensions = [];
updateExtensions();
vm.extensionsPromise = null;
},
function fail() {
vm.extensions = [];
vm.selectedExtensions = [];
updateExtensions();
vm.extensionsPromise = null;
}
);
}
function updateExtensions() {
vm.selectedExtensions = [];
var result = $filter('orderBy')(vm.allExtensions, vm.query.order);
if (vm.query.search != null) {
result = $filter('filter')(result, function(extension) {
if(!vm.query.search || (extension.id.indexOf(vm.query.search) != -1) || (extension.type.indexOf(vm.query.search) != -1)) {
return true;
}
return false;
});
}
vm.extensionsCount = result.length;
var startIndex = vm.query.limit * (vm.query.page - 1);
vm.extensions = result.slice(startIndex, startIndex + vm.query.limit);
vm.extensionsJSON = angular.toJson(vm.extensions);
checkForSync();
}
function subscribeForClientAttributes() {
if (!vm.subscribed) {
if (vm.entityId && vm.entityType) {
$scope.subscriber = {
subscriptionCommands: [{
entityType: vm.entityType,
entityId: vm.entityId,
scope: 'CLIENT_SCOPE'
}],
type: 'attribute',
onData: function (data) {
if (data.data) {
onSubscriptionData(data.data);
}
vm.subscribed = true;
}
};
telemetryWebsocketService.subscribe($scope.subscriber);
}
}
}
function onSubscriptionData(data) {
if ($.isEmptyObject(data)) {
vm.appliedConfiguration = undefined;
} else {
if (data.appliedConfiguration && data.appliedConfiguration[0] && data.appliedConfiguration[0][1]) {
vm.appliedConfiguration = data.appliedConfiguration[0][1];
}
}
updateExtensions();
$scope.$digest();
}
function checkForSync() {
if (vm.appliedConfiguration && vm.extensionsJSON && vm.appliedConfiguration === vm.extensionsJSON) {
vm.syncStatus = $translate.instant('extension.sync.sync');
vm.syncLastTime = formatDate();
$scope.isSync = true;
} else {
vm.syncStatus = $translate.instant('extension.sync.not-sync');
$scope.isSync = false;
}
}
function formatDate(date) {
let d;
if (date) {
d = date;
} else {
d = new Date();
}
d = d.getFullYear() +'/'+ addZero(d.getMonth()+1) +'/'+ addZero(d.getDate()) + ' ' + addZero(d.getHours()) + ':' + addZero(d.getMinutes()) +':'+ addZero(d.getSeconds());
return d;
function addZero(num) {
if ((angular.isNumber(num) && num < 10) || (angular.isString(num) && num.length === 1)) {
num = '0' + num;
}
return num;
}
}
vm.importExtensions = function($event) {
importExport.importExtension($event, {"entityType":vm.entityType, "entityId":vm.entityId, "successFunc":reloadExtensions});
};
vm.exportExtensions = function(widgetSourceEntityName) {
if(vm.inWidget) {
importExport.exportToPc(vm.extensionsJSON, widgetSourceEntityName + '_configuration.json');
} else {
importExport.exportToPc(vm.extensionsJSON, vm.entityName + '_configuration.json');
}
};
/*change function for widget implementing, like vm.exportExtensions*/
vm.exportExtension = function($event, extension) {
if ($event) {
$event.stopPropagation();
}
importExport.exportToPc(extension, vm.entityName +'_'+ extension.id +'_configuration.json');
};
}

47
ui/src/app/extension/extension-table.scss

@ -0,0 +1,47 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../scss/constants';
.extension-table {
md-input-container .md-errors-spacer {
min-height: 0;
}
/*&.tb-data-table table.md-table tbody tr td.tb-action-cell,
&.tb-data-table table.md-table.md-row-select tbody tr td.tb-action-cell {
width: 114px;
}*/
.sync-widget {
max-height: 90px;
overflow: hidden;
}
.toolbar-widget {
min-height: 39px;
max-height: 39px;
}
}
.extension__syncStatus--black {
color: #000000!important;
}
.extension__syncStatus--green {
color: #228634!important;
}
.extension__syncStatus--red {
color: #862222!important;
}

159
ui/src/app/extension/extension-table.tpl.html

@ -0,0 +1,159 @@
<!--
Copyright © 2016-2017 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.
-->
<md-content flex class="md-padding tb-absolute-fill tb-data-table extension-table" layout="column">
<div layout="column" class="md-whiteframe-z1" ng-class="{'tb-absolute-fill' : vm.inWidget}">
<md-toolbar ng-if="!vm.inWidget" class="md-table-toolbar md-default" ng-show="!vm.selectedExtensions.length
&& vm.query.search === null">
<div class="md-toolbar-tools">
<span translate>{{ 'extension.extensions' }}</span>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.importExtensions($event)">
<md-icon>file_upload</md-icon>
<md-tooltip md-direction="top">
{{ 'extension.import-extensions-configuration' | translate }}
</md-tooltip>
</md-button>
<md-button class="md-icon-button" ng-click="vm.exportExtensions()">
<md-icon>file_download</md-icon>
<md-tooltip md-direction="top">
{{ 'extension.export-extensions-configuration' | translate }}
</md-tooltip>
</md-button>
<md-button class="md-icon-button" ng-click="vm.addExtension($event)">
<md-icon>add</md-icon>
<md-tooltip md-direction="top">
{{ 'action.add' | translate }}
</md-tooltip>
</md-button>
<md-button class="md-icon-button" ng-click="vm.enterFilterMode()">
<md-icon>search</md-icon>
<md-tooltip md-direction="top">
{{ 'action.search' | translate }}
</md-tooltip>
</md-button>
<md-button class="md-icon-button" ng-click="vm.reloadExtensions()">
<md-icon>refresh</md-icon>
<md-tooltip md-direction="top">
{{ 'action.refresh' | translate }}
</md-tooltip>
</md-button>
</div>
</md-toolbar>
<md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedExtensions.length
&& vm.query.search != null" ng-class="{'toolbar-widget' : vm.inWidget}">
<div class="md-toolbar-tools">
<md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
<md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
<md-tooltip md-direction="top">
{{ 'action.search' | translate }}
</md-tooltip>
</md-button>
<md-input-container flex>
<label>&nbsp;</label>
<input ng-model="vm.query.search" placeholder="{{ 'common.enter-search' | translate }}"/>
</md-input-container>
<md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="vm.exitFilterMode()">
<md-icon aria-label="{{ 'action.close' | translate }}" class="material-icons">close</md-icon>
<md-tooltip md-direction="{{vm.ctx.dashboard.isWidgetExpanded ? 'bottom' : 'top'}}">
{{ 'action.close' | translate }}
</md-tooltip>
</md-button>
</div>
</md-toolbar>
<md-toolbar class="md-table-toolbar alternate" ng-show="vm.selectedExtensions.length" ng-class="{'toolbar-widget' : vm.inWidget}">
<div class="md-toolbar-tools">
<span translate
translate-values="{count: vm.selectedExtensions.length}"
translate-interpolation="messageformat">extension.selected-extensions</span>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.deleteExtensions($event)">
<md-icon>delete</md-icon>
<md-tooltip md-direction="{{vm.ctx.dashboard.isWidgetExpanded ? 'bottom' : 'top'}}">
{{ 'action.delete' | translate }}
</md-tooltip>
</md-button>
</div>
</md-toolbar>
<div class="md-padding" flex layout="row" ng-class="{'sync-widget' : vm.inWidget}">
<md-input-container flex="50" class="md-block">
<label translate>extension.sync.status</label>
<input ng-model="vm.syncStatus"
ng-class="{'extension__syncStatus--green':isSync, 'extension__syncStatus--red':!isSync}"
disabled
>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.sync.last-sync-time</label>
<input ng-model="vm.syncLastTime"
class="extension__syncStatus--black"
disabled
>
</md-input-container>
</div>
<md-table-container flex>
<table md-table md-row-select multiple="" ng-model="vm.selectedExtensions" md-progress="vm.extensionsDeferred.promise">
<thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
<tr md-row>
<th md-column md-order-by="id"><span translate>extension.id</span></th>
<th md-column md-order-by="type"><span translate>extension.type</span></th>
<th md-column><span>&nbsp</span></th>
</tr>
</thead>
<tbody md-body>
<tr md-row md-select="extension" md-select-id="extension" md-auto-select ng-repeat="extension in vm.extensions">
<td md-cell>{{ extension.id }}</td>
<td md-cell>{{ extension.type }}</td>
<td md-cell class="tb-action-cell">
<!--<md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}" ng-click="vm.exportExtension($event, extension)">
<md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">file_download</md-icon>
<md-tooltip md-direction="top">
{{ 'extension.export-extension' | translate }}
</md-tooltip>
</md-button>-->
<md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}" ng-click="vm.editExtension($event, extension)">
<md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">edit</md-icon>
<md-tooltip md-direction="top">
{{ 'extension.edit' | translate }}
</md-tooltip>
</md-button>
<md-button class="md-icon-button" aria-label="{{ 'action.delete' | translate }}" ng-click="vm.deleteExtension($event, extension)">
<md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">delete</md-icon>
<md-tooltip md-direction="top">
{{ 'extension.delete' | translate }}
</md-tooltip>
</md-button>
</td>
</tr>
</tbody>
</table>
<md-divider ng-if="vm.inWidget"></md-divider>
</md-table-container>
<md-table-pagination md-limit="vm.query.limit" md-limit-options="[5, 10, 15]"
md-page="vm.query.page" md-total="{{vm.extensionsCount}}"
md-on-paginate="vm.onPaginate" md-page-select>
</md-table-pagination>
</div>
</md-content>

147
ui/src/app/extension/extensions-forms/extension-form-http.directive.js

@ -0,0 +1,147 @@
/*
* Copyright © 2016-2017 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 'brace/ext/language_tools';
import 'brace/mode/json';
import 'brace/theme/github';
import './extension-form.scss';
/* eslint-disable angular/log */
import extensionFormHttpTemplate from './extension-form-http.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function ExtensionFormHttpDirective($compile, $templateCache, $translate, types) {
var linker = function(scope, element) {
var template = $templateCache.get(extensionFormHttpTemplate);
element.html(template);
scope.types = types;
scope.theForm = scope.$parent.theForm;
scope.extensionCustomTransformerOptions = {
useWrapMode: false,
mode: 'json',
showGutter: true,
showPrintMargin: true,
theme: 'github',
advanced: {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
},
onLoad: function(_ace) {
_ace.$blockScrolling = 1;
}
};
scope.addConverterConfig = function() {
var newConverterConfig = {converterId:"", converters:[]};
scope.converterConfigs.push(newConverterConfig);
scope.converterConfigs[scope.converterConfigs.length - 1].converters = [];
scope.addConverter(scope.converterConfigs[scope.converterConfigs.length - 1].converters);
};
scope.removeConverterConfig = function(config) {
var index = scope.converterConfigs.indexOf(config);
if (index > -1) {
scope.converterConfigs.splice(index, 1);
}
};
scope.addConverter = function(converters) {
var newConverter = {
deviceNameJsonExpression:"",
deviceTypeJsonExpression:"",
attributes:[],
timeseries:[]
};
converters.push(newConverter);
};
scope.removeConverter = function(converter, converters) {
var index = converters.indexOf(converter);
if (index > -1) {
converters.splice(index, 1);
}
};
scope.addAttribute = function(attributes) {
var newAttribute = {type:"", key:"", value:""};
attributes.push(newAttribute);
};
scope.removeAttribute = function(attribute, attributes) {
var index = attributes.indexOf(attribute);
if (index > -1) {
attributes.splice(index, 1);
}
};
if(scope.isAdd) {
scope.converterConfigs = scope.config.converterConfigurations;
scope.addConverterConfig();
} else {
scope.converterConfigs = scope.config.converterConfigurations;
}
scope.transformerTypeChange = function(attribute) {
attribute.transformer = "";
};
scope.validateTransformer = function (model, editorName) {
if(model && model.length) {
try {
angular.fromJson(model);
scope.theForm[editorName].$setValidity('transformerJSON', true);
} catch(e) {
scope.theForm[editorName].$setValidity('transformerJSON', false);
}
}
};
scope.collapseValidation = function(index, id) {
var invalidState = angular.element('#'+id+':has(.ng-invalid)');
if(invalidState.length) {
invalidState.addClass('inner-invalid');
}
};
scope.expandValidation = function (index, id) {
var invalidState = angular.element('#'+id);
invalidState.removeClass('inner-invalid');
};
$compile(element.contents())(scope);
};
return {
restrict: "A",
link: linker,
scope: {
config: "=",
isAdd: "="
}
}
}

311
ui/src/app/extension/extensions-forms/extension-form-http.tpl.html

@ -0,0 +1,311 @@
<!--
Copyright © 2016-2017 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.
-->
<md-card class="extension-form extension-http">
<md-card-title>
<md-card-title-text>
<span translate class="md-headline">extension.configuration</span>
</md-card-title-text>
</md-card-title>
<md-card-content>
<v-accordion id="http-converter-configs-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="http-converters-pane" expanded="true">
<v-pane-header>
{{ 'extension.converter-configurations' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="converterConfigs.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(configIndex, config) in converterConfigs">
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeConverterConfig(config)"
ng-hide="converterConfigs.length < 2"
>
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<md-input-container class="md-block" md-is-error="theForm['httpConverterId_' + configIndex].$touched && theForm['httpConverterId_' + configIndex].$invalid">
<label translate>extension.converter-id</label>
<input required name="httpConverterId_{{configIndex}}" ng-model="config.converterId">
<div ng-messages="theForm['httpConverterId_' + configIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container class="md-block">
<label translate>extension.token</label>
<input name="httpToken" ng-model="config.token" parse-to-null>
</md-input-container>
<v-accordion id="http-converters-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="http-converters-pane_{{configIndex}}" expanded="true">
<v-pane-header>
{{ 'extension.converters' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="config.converters.length > 0">
<ol class="list-group">
<li class="list-group-item"
ng-repeat="(converterIndex,converter) in config.converters"
>
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeConverter(converter, config.converters)"
ng-hide="config.converters.length < 2"
>
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<md-input-container class="md-block" md-is-error="theForm['httpDeviceNameExp_' + configIndex + converterIndex].$touched && theForm['httpDeviceNameExp_' + configIndex + converterIndex].$invalid">
<label translate>extension.device-name-expression</label>
<input required name="httpDeviceNameExp_{{configIndex}}{{converterIndex}}" ng-model="converter.deviceNameJsonExpression">
<div ng-messages="theForm['httpDeviceNameExp_' + configIndex + converterIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container class="md-block" md-is-error="theForm['httpDeviceTypeExp_' + configIndex + converterIndex].$touched && theForm['httpDeviceTypeExp_' + configIndex + converterIndex].$invalid">
<label translate>extension.device-type-expression</label>
<input required name="httpDeviceTypeExp_{{configIndex}}{{converterIndex}}" ng-model="converter.deviceTypeJsonExpression">
<div ng-messages="theForm['httpDeviceTypeExp_' + configIndex + converterIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<v-accordion id="http-attributes-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="http-attributes-pane_{{configIndex}}{{converterIndex}}">
<v-pane-header>
{{ 'extension.attributes' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="converter.attributes.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(attributeIndex, attribute) in converter.attributes">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attribute, converter.attributes)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="60" class="md-block" md-is-error="theForm['httpAttributeKey_' + configIndex + converterIndex + attributeIndex].$touched && theForm['httpAttributeKey_' + configIndex + converterIndex + attributeIndex].$invalid">
<label translate>extension.key</label>
<input required name="httpAttributeKey_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.key">
<div ng-messages="theForm['httpAttributeKey_' + configIndex + converterIndex + attributeIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="40" class="md-block" md-is-error="theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$touched && theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$invalid">
<label translate>extension.type</label>
<md-select required name="httpAttributeType_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.type">
<md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
{{attrTypeValue | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="60" class="md-block" md-is-error="theForm['httpAttributeValue_' + configIndex + converterIndex + attributeIndex].$touched && theForm['httpAttributeValue_' + configIndex + converterIndex + attributeIndex].$invalid">
<label translate>extension.value</label>
<input required name="httpAttributeValue_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.value">
<div ng-messages="theForm['httpAttributeValue_' + configIndex + converterIndex + attributeIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="40" class="md-block">
<label translate>extension.transformer</label>
<md-select name="httpAttributeTransformer" ng-model="attribute.transformerType" ng-change="transformerTypeChange(attribute)">
<md-option ng-repeat="(transformerType, value) in types.extensionTransformerType" ng-value="transformerType">
{{value | translate}}
</md-option>
</md-select>
</md-input-container>
</section>
<div ng-if='attribute.transformerType == "custom"'>
<div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
<div flex class="tb-extension-custom-transformer-panel">
<div flex class="tb-extension-custom-transformer"
ui-ace="extensionCustomTransformerOptions"
ng-model="attribute.transformer"
name="attributeCustomTransformer_{{configIndex}}{{converterIndex}}{{attributeIndex}}"
ng-change='validateTransformer(attribute.transformer,"attributeCustomTransformer_" + configIndex + converterIndex + attributeIndex)'
required>
</div>
</div>
<div class="tb-error-messages" ng-messages="theForm['attributeCustomTransformer_' + configIndex + converterIndex + attributeIndex].$error" role="alert">
<div ng-message="required" class="tb-error-message" translate>extension.json-required</div>
<div ng-message="transformerJSON" class="tb-error-message" translate>extension.json-parse</div>
</div>
</div>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addAttribute(converter.attributes)" aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-attribute</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion id="http-timeseries-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="http-timeseries-pane_{{configIndex}}{{converterIndex}}">
<v-pane-header>
{{ 'extension.timeseries' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="converter.timeseries.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(timeseriesIndex, timeseries) in converter.timeseries">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(timeseries, converter.timeseries)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="60" class="md-block" md-is-error="theForm['httpTimeseriesKey_' + configIndex + converterIndex + timeseriesIndex].$touched && theForm['httpTimeseriesKey_' + configIndex + converterIndex + timeseriesIndex].$invalid">
<label translate>extension.key</label>
<input required name="httpTimeseriesKey_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.key">
<div ng-messages="theForm['httpTimeseriesKey_' + configIndex + converterIndex + timeseriesIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="40" class="md-block" md-is-error="theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$touched && theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$invalid">
<label translate>extension.type</label>
<md-select required name="httpTimeseriesType_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.type">
<md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
{{attrTypeValue | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="60" class="md-block" md-is-error="theForm['httpTimeseriesValue_' + configIndex + converterIndex + timeseriesIndex].$touched && theForm['httpTimeseriesValue_' + configIndex + converterIndex + timeseriesIndex].$invalid">
<label translate>extension.value</label>
<input required name="httpTimeseriesValue_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.value">
<div ng-messages="theForm['httpTimeseriesValue_' + configIndex + converterIndex + timeseriesIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="40" class="md-block">
<label translate>extension.transformer</label>
<md-select name="httpTimeseriesTransformer" ng-model="timeseries.transformerType" ng-change="transformerTypeChange(timeseries)">
<md-option ng-repeat="(transformerType, value) in types.extensionTransformerType" ng-value="transformerType">
{{value | translate}}
</md-option>
</md-select>
</md-input-container>
</section>
<div ng-if='timeseries.transformerType == "custom"'>
<div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
<div flex class="tb-extension-custom-transformer-panel">
<div flex class="tb-extension-custom-transformer"
ui-ace="extensionCustomTransformerOptions"
ng-model="timeseries.transformer"
name="timeseriesCustomTransformer_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}"
ng-change='validateTransformer(timeseries.transformer,"timeseriesCustomTransformer_" + configIndex + converterIndex + timeseriesIndex)'
required>
</div>
</div>
<div class="tb-error-messages" ng-messages="theForm['timeseriesCustomTransformer_' + configIndex + converterIndex + timeseriesIndex].$error" role="alert">
<div ng-message="required" class="tb-error-message" translate>extension.json-required</div>
<div ng-message="transformerJSON" class="tb-error-message" translate>extension.json-parse</div>
</div>
</div>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addAttribute(converter.timeseries)" aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-timeseries</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addConverter(config.converters)" aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-converter</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addConverterConfig()" aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-config</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<!--{{config}}-->
</md-card-content>
</md-card>

347
ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js

@ -0,0 +1,347 @@
/*
* Copyright © 2016-2017 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 './extension-form.scss';
/* eslint-disable angular/log */
import extensionFormMqttTemplate from './extension-form-mqtt.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function ExtensionFormHttpDirective($compile, $templateCache, $translate, types) {
var linker = function(scope, element) {
var template = $templateCache.get(extensionFormMqttTemplate);
element.html(template);
scope.types = types;
scope.theForm = scope.$parent.theForm;
scope.deviceNameExpressions = {
deviceNameJsonExpression: "extension.converter-json",
deviceNameTopicExpression: "extension.topic"
};
scope.deviceTypeExpressions = {
deviceTypeJsonExpression: "extension.converter-json",
deviceTypeTopicExpression: "extension.topic"
};
scope.attributeKeyExpressions = {
attributeKeyJsonExpression: "extension.converter-json",
attributeKeyTopicExpression: "extension.topic"
};
scope.requestIdExpressions = {
requestIdJsonExpression: "extension.converter-json",
requestIdTopicExpression: "extension.topic"
}
scope.extensionCustomConverterOptions = {
useWrapMode: false,
mode: 'json',
showGutter: true,
showPrintMargin: true,
theme: 'github',
advanced: {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
},
onLoad: function(_ace) {
_ace.$blockScrolling = 1;
}
};
scope.updateValidity = function () {
if(scope.brokers.length) {
for(let i=0;i<scope.brokers.length;i++) {
if(scope.brokers[i].credentials.type == scope.types.mqttCredentialTypes.pem.value) {
if(!(scope.brokers[i].credentials.caCert && scope.brokers[i].credentials.privateKey && scope.brokers[i].credentials.cert)) {
scope.theForm.$setValidity('cert.PEM', false);
break;
} else {
scope.theForm.$setValidity('cert.PEM', true);
}
}
}
}
};
scope.$watch('brokers', function() {
scope.updateValidity();
}, true);
scope.addBroker = function() {
var newBroker = {
host: "localhost",
port: 1882,
ssl: false,
retryInterval: 3000,
credentials: {type:"anonymous"},
mapping: [],
connectRequests: [],
disconnectRequests: [],
attributeRequests: [],
attributeUpdates: [],
serverSideRpc: []
};
scope.brokers.push(newBroker);
};
scope.removeBroker = function(broker) {
var index = scope.brokers.indexOf(broker);
if (index > -1) {
scope.brokers.splice(index, 1);
}
};
if(scope.isAdd) {
scope.brokers = [];
scope.config.brokers = scope.brokers;
scope.addBroker();
} else {
scope.brokers = scope.config.brokers;
}
scope.addMap = function(mapping) {
var newMap = {topicFilter:"sensors", converter:{attributes:[],timeseries:[]}};
mapping.push(newMap);
};
scope.removeMap = function(map, mapping) {
var index = mapping.indexOf(map);
if (index > -1) {
mapping.splice(index, 1);
}
};
scope.addAttribute = function(attributes) {
var newAttribute = {type:"", key:"", value:""};
attributes.push(newAttribute);
};
scope.removeAttribute = function(attribute, attributes) {
var index = attributes.indexOf(attribute);
if (index > -1) {
attributes.splice(index, 1);
}
};
scope.addConnectRequest = function(requests, type) {
var newRequest = {};
if(type == "connect") {
newRequest.topicFilter = "sensors/connect";
} else {
newRequest.topicFilter = "sensors/disconnect";
}
requests.push(newRequest);
};
scope.addAttributeRequest = function(requests) {
var newRequest = {
topicFilter: "sensors/attributes",
clientScope: false,
responseTopicExpression: "sensors/${deviceName}/attributes/${responseId}",
valueExpression: "${attributeValue}"
};
requests.push(newRequest);
};
scope.addAttributeUpdate = function(updates) {
var newUpdate = {
deviceNameFilter: ".*",
attributeFilter: ".*",
topicExpression: "sensor/${deviceName}/${attributeKey}",
valueExpression: "{\"${attributeKey}\":\"${attributeValue}\"}"
}
updates.push(newUpdate);
};
scope.addServerSideRpc = function(rpcRequests) {
var newRpc = {
deviceNameFilter: ".*",
methodFilter: "echo",
requestTopicExpression: "sensor/${deviceName}/request/${methodName}/${requestId}",
responseTopicExpression: "sensor/${deviceName}/response/${methodName}/${requestId}",
responseTimeout: 10000,
valueExpression: "${params}"
};
rpcRequests.push(newRpc);
};
scope.changeCredentials = function(broker) {
var type = broker.credentials.type;
broker.credentials = {};
broker.credentials.type = type;
};
scope.changeConverterType = function(map) {
if(map.converterType == "custom"){
map.converter = "";
}
if(map.converterType == "json") {
map.converter = {attributes:[],timeseries:[]};
}
};
scope.changeNameExpression = function(element, type) {
if(element.nameExp == "deviceNameJsonExpression") {
if(element.deviceNameTopicExpression) {
delete element.deviceNameTopicExpression;
}
if(type) {
element.deviceNameJsonExpression = "${$.serialNumber}";
}
}
if(element.nameExp == "deviceNameTopicExpression") {
if(element.deviceNameJsonExpression) {
delete element.deviceNameJsonExpression;
}
if(type && type == "connect") {
element.deviceNameTopicExpression = "(?<=sensor\\/)(.*?)(?=\\/connect)";
}
if(type && type == "disconnect") {
element.deviceNameTopicExpression = "(?<=sensor\\/)(.*?)(?=\\/disconnect)";
}
if(type && type == "attribute") {
element.deviceNameTopicExpression = "(?<=sensors\\/)(.*?)(?=\\/attributes)";
}
}
};
scope.changeTypeExpression = function(converter) {
if(converter.typeExp == "deviceTypeJsonExpression") {
if(converter.deviceTypeTopicExpression) {
delete converter.deviceTypeTopicExpression;
}
}
if(converter.typeExp == "deviceTypeTopicExpression") {
if(converter.deviceTypeJsonExpression) {
delete converter.deviceTypeJsonExpression;
}
}
};
scope.changeAttrKeyExpression = function(request) {
if(request.attrKey == "attributeKeyJsonExpression") {
if(request.attributeKeyTopicExpression) {
delete request.attributeKeyTopicExpression;
}
request.attributeKeyJsonExpression = "${$.key}";
}
if(request.attrKey == "attributeKeyTopicExpression") {
if(request.attributeKeyJsonExpression) {
delete request.attributeKeyJsonExpression;
}
request.attributeKeyTopicExpression = "(?<=attributes\\/)(.*?)(?=\\/request)";
}
};
scope.changeRequestIdExpression = function(request) {
if(request.requestId == "requestIdJsonExpression") {
if(request.requestIdTopicExpression) {
delete request.requestIdTopicExpression;
}
request.requestIdJsonExpression = "${$.requestId}";
}
if(request.requestId == "requestIdTopicExpression") {
if(request.requestIdJsonExpression) {
delete request.requestIdJsonExpression;
}
request.requestIdTopicExpression = "(?<=request\\/)(.*?)($)";
}
};
scope.validateCustomConverter = function(model, editorName) {
if(model && model.length) {
try {
angular.fromJson(model);
scope.theForm[editorName].$setValidity('converterJSON', true);
} catch(e) {
scope.theForm[editorName].$setValidity('converterJSON', false);
}
}
};
scope.fileAdded = function($file, broker, fileType) {
var reader = new FileReader();
reader.onload = function(event) {
scope.$apply(function() {
if(event.target.result) {
scope.theForm.$setDirty();
var addedFile = event.target.result;
if (addedFile && addedFile.length > 0) {
if(fileType == "caCert") {
broker.credentials.caCertFileName = $file.name;
broker.credentials.caCert = addedFile.replace(/^data.*base64,/, "");
}
if(fileType == "privateKey") {
broker.credentials.privateKeyFileName = $file.name;
broker.credentials.privateKey = addedFile.replace(/^data.*base64,/, "");
}
if(fileType == "Cert") {
broker.credentials.certFileName = $file.name;
broker.credentials.cert = addedFile.replace(/^data.*base64,/, "");
}
}
}
});
};
reader.readAsDataURL($file.file);
};
scope.clearFile = function(broker, fileType) {
scope.theForm.$setDirty();
if(fileType == "caCert") {
broker.credentials.caCertFileName = null;
broker.credentials.caCert = null;
}
if(fileType == "privateKey") {
broker.credentials.privateKeyFileName = null;
broker.credentials.privateKey = null;
}
if(fileType == "Cert") {
broker.credentials.certFileName = null;
broker.credentials.cert = null;
}
};
scope.collapseValidation = function(index, id) {
var invalidState = angular.element('#'+id+':has(.ng-invalid)');
if(invalidState.length) {
invalidState.addClass('inner-invalid');
}
};
scope.expandValidation = function (index, id) {
var invalidState = angular.element('#'+id);
invalidState.removeClass('inner-invalid');
};
$compile(element.contents())(scope);
};
return {
restrict: "A",
link: linker,
scope: {
config: "=",
isAdd: "="
}
}
}

860
ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html

@ -0,0 +1,860 @@
<!--
Copyright © 2016-2017 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.
-->
<md-card class="extension-form extension-mqtt">
<md-card-title name="testValid">
<md-card-title-text>
<span translate class="md-headline">extension.configuration</span>
</md-card-title-text>
</md-card-title>
<md-card-content>
<v-accordion id="mqtt-brokers-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="mqtt-brokers-pane" expanded="true">
<v-pane-header>
{{ 'extension.brokers' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="brokers.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(brokerIndex,broker) in brokers">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeBroker(broker)" ng-hide="brokers.length < 2">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="40" class="md-block">
<label translate>extension.port</label>
<input required type="number" min="1" max="65535" name="mqttPort_{{brokerIndex}}" ng-model="broker.port">
<div ng-messages="theForm['mqttPort_' + brokerIndex].$error">
<div translate ng-message="required">extension.field-required</div>
<div translate ng-message="min">extension.port-range</div>
<div translate ng-message="max">extension.port-range</div>
</div>
</md-input-container>
<md-input-container flex="60" class="md-block">
<label translate>extension.host</label>
<input required name="mqttHost_{{brokerIndex}}" ng-model="broker.host">
<div ng-messages="theForm['mqttHost_' + brokerIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="40" class="md-block">
<label translate>extension.retry-interval</label>
<input required type="number" name="mqttRetryInterval_{{brokerIndex}}" ng-model="broker.retryInterval">
<div ng-messages="theForm['mqttRetryInterval_' + brokerIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.credentials</label>
<md-select required name="mqttCredentials_{{brokerIndex}}" ng-model="broker.credentials.type" ng-change="changeCredentials(broker)">
<md-option ng-repeat="(credentialsType, credentialsValue) in types.mqttCredentialTypes" ng-value="credentialsValue.value">
{{credentialsValue.name | translate}}
</md-option>
</md-select>
</md-input-container>
<md-input-container flex="10" class="md-block">
<md-checkbox flex aria-label="{{ 'extension.ssl' | translate }}"
ng-model="broker.ssl">{{ 'extension.ssl' | translate }}
</md-checkbox>
</md-input-container>
</section>
<section flex layout="row" ng-if='broker.credentials.type == "basic"'>
<md-input-container flex="40" class="md-block" md-is-error="theForm['mqttUsername_' + brokerIndex].$touched && theForm['mqttUsername_' + brokerIndex].$invalid">
<label translate>extension.username</label>
<input required name="mqttUsername_{{brokerIndex}}" ng-model="broker.credentials.username">
<div ng-messages="theForm['mqttUsername_' + brokerIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="60" class="md-block" md-is-error="theForm['mqttPassword_' + brokerIndex].$touched && theForm['mqttPassword_' + brokerIndex].$invalid">
<label translate>extension.password</label>
<input required name="mqttPassword_{{brokerIndex}}" ng-model="broker.credentials.password">
<div ng-messages="theForm['mqttPassword_' + brokerIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<section flex layout="column" ng-if='broker.credentials.type == "cert.PEM"' class="dropdown-section">
<div class="tb-container" ng-class="broker.credentials.caCertFileName ? 'ng-valid' : 'ng-invalid'">
<label class="tb-label" translate>extension.ca-cert</label>
<div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, broker, "caCert")' class="tb-file-select-container">
<div class="tb-file-clear-container">
<md-button ng-click='clearFile(broker, "caCert")' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
</md-button>
</div>
<div class="alert tb-flow-drop" flow-drop>
<label for="caCertSelect_{{brokerIndex}}" translate>extension.drop-file</label>
<input class="file-input" flow-btn flow-attrs="{accept:'.pem'}" id="caCertSelect_{{brokerIndex}}">
</div>
</div>
</div>
<div class="dropdown-messages">
<div ng-if="!broker.credentials.caCertFileName" class="tb-error-message" translate>extension.no-file</div>
<div ng-if="broker.credentials.caCertFileName">{{broker.credentials.caCertFileName}}</div>
</div>
<div class="tb-container" ng-class="broker.credentials.privateKeyFileName ? 'ng-valid' : 'ng-invalid'">
<label class="tb-label" translate>extension.private-key</label>
<div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, broker, "privateKey")' class="tb-file-select-container">
<div class="tb-file-clear-container">
<md-button ng-click='clearFile(broker, "privateKey")' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
</md-button>
</div>
<div class="alert tb-flow-drop" flow-drop>
<label for="privateKeySelect_{{brokerIndex}}" translate>extension.drop-file</label>
<input class="file-input" flow-btn flow-attrs="{accept:'.pem'}" id="privateKeySelect_{{brokerIndex}}">
</div>
</div>
</div>
<div class="dropdown-messages">
<div ng-if="!broker.credentials.privateKeyFileName" class="tb-error-message" translate>extension.no-file</div>
<div ng-if="broker.credentials.privateKeyFileName">{{broker.credentials.privateKeyFileName}}</div>
</div>
<div class="tb-container" ng-class="broker.credentials.certFileName ? 'ng-valid' : 'ng-invalid'">
<label class="tb-label" translate>extension.cert</label>
<div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, broker, "Cert")' class="tb-file-select-container">
<div class="tb-file-clear-container">
<md-button ng-click='clearFile(broker, "Cert")' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
</md-button>
</div>
<div class="alert tb-flow-drop" flow-drop>
<label for="CertSelect_{{brokerIndex}}" translate>extension.drop-file</label>
<input class="file-input" flow-btn flow-attrs="{accept:'.pem'}" id="CertSelect_{{brokerIndex}}">
</div>
</div>
</div>
<div class="dropdown-messages">
<div ng-if="!broker.credentials.certFileName" class="tb-error-message" translate>extension.no-file</div>
<div ng-if="broker.credentials.certFileName">{{broker.credentials.certFileName}}</div>
</div>
</section>
<v-accordion id="mqtt-mapping-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="mqtt-mapping-pane_{{brokerIndex}}">
<v-pane-header>
{{ 'extension.mapping' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="broker.mapping.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(mapIndex,map) in broker.mapping">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeMap(map, broker.mapping)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="40" class="md-block" md-is-error="theForm['mqttConverterType_' + brokerIndex + mapIndex].$touched && theForm['mqttConverterType_' + brokerIndex + mapIndex].$invalid">
<label translate>extension.converter-type</label>
<md-select required name="mqttConverterType_{{brokerIndex}}{{mapIndex}}" ng-model="map.converterType" ng-change="changeConverterType(map)">
<md-option ng-repeat="(converterType, value) in types.mqttConverterTypes" ng-value="converterType">
{{value | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['mqttConverterType_' + brokerIndex + mapIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="60" class="md-block">
<label translate>extension.topic-filter</label>
<input required name="mqttTopicFilter_{{brokerIndex}}{{mapIndex}}" ng-model="map.topicFilter">
<div ng-messages="theForm['mqttTopicFilter_' + brokerIndex + mapIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<div ng-if='map.converterType =="json"' ng-init="map.converter.type = 'json'">
<section flex layout="row">
<md-input-container flex="40" class="md-block" md-is-error="theForm['mqttDeviceNameExpression_' + brokerIndex + mapIndex].$touched && theForm['mqttDeviceNameExpression_' + brokerIndex + mapIndex].$invalid">
<label translate>extension.device-name-expression</label>
<md-select required name="mqttDeviceNameExpression_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.nameExp" ng-change="changeNameExpression(map.converter)">
<md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
{{value | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['mqttDeviceNameExpression_' + brokerIndex + mapIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="map.converter.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block" md-is-error="theForm['mqttJsonNameExp_' + brokerIndex + mapIndex].$touched && theForm['mqttJsonNameExp_' + brokerIndex + mapIndex].$invalid">
<label translate>extension.json-name-expression</label>
<input required name="mqttJsonNameExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceNameJsonExpression">
<div ng-messages="theForm['mqttJsonNameExp_' + brokerIndex + mapIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="map.converter.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block" md-is-error="theForm['mqttTopicNameExp_' + brokerIndex + mapIndex].$touched && theForm['mqttTopicNameExp_' + brokerIndex + mapIndex].$invalid">
<label translate>extension.topic-name-expression</label>
<input required name="mqttTopicNameExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceNameTopicExpression">
<div ng-messages="theForm['mqttTopicNameExp_' + brokerIndex + mapIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="40" class="md-block" md-is-error="theForm['mqttDeviceTypeExpression_' + brokerIndex + mapIndex].$touched && theForm['mqttDeviceTypeExpression_' + brokerIndex + mapIndex].$invalid">
<label translate>extension.device-type-expression</label>
<md-select required name="mqttDeviceTypeExpression_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.typeExp" ng-change="changeTypeExpression(map.converter)">
<md-option ng-repeat="(key, value) in deviceTypeExpressions" ng-value='key'>
{{value | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['mqttDeviceTypeExpression_' + brokerIndex + mapIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="map.converter.typeExp == 'deviceTypeJsonExpression'" flex="60" class="md-block" md-is-error="theForm['mqttJsonTypeExp_' + brokerIndex + mapIndex].$touched && theForm['mqttJsonTypeExp_' + brokerIndex + mapIndex].$invalid">
<label translate>extension.json-type-expression</label>
<input required name="mqttJsonTypeExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceTypeJsonExpression">
<div ng-messages="theForm['mqttJsonTypeExp_' + brokerIndex + mapIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="map.converter.typeExp == 'deviceTypeTopicExpression'" flex="60" class="md-block" md-is-error="theForm['mqttTopicTypeExp_' + brokerIndex + mapIndex].$touched && theForm['mqttTopicTypeExp_' + brokerIndex + mapIndex].$invalid">
<label translate>extension.topic-type-expression</label>
<input required name="mqttTopicTypeExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceTypeTopicExpression">
<div ng-messages="theForm['mqttTopicTypeExp_' + brokerIndex + mapIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="40" class="md-block">
<label translate>extension.timeout</label>
<input type="number" name="mqttTimeout_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.timeout" parse-to-null>
</md-input-container>
<md-input-container flex="60" class="md-block" md-is-error="theForm['mqttFilterExpression' + brokerIndex + mapIndex].$touched && theForm['mqttFilterExpression' + brokerIndex + mapIndex].$invalid">
<label translate>extension.filter-expression</label>
<input required name="mqttFilterExpression{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.filterExpression">
<div ng-messages="theForm['mqttFilterExpression' + brokerIndex + mapIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
</div>
<div ng-if='map.converterType == "custom"'>
<div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
<div flex class="tb-extension-custom-transformer-panel">
<div flex class="tb-extension-custom-transformer"
ui-ace="extensionCustomConverterOptions"
ng-model="map.converter"
name="mqttCustomConverter_{{brokerIndex}}{{mapIndex}}"
ng-change='validateCustomConverter(map.converter, "mqttCustomConverter_" + brokerIndex + mapIndex)'
required>
</div>
</div>
<div class="tb-error-messages" ng-messages="theForm['mqttCustomConverter_' + brokerIndex + mapIndex].$error" role="alert">
<div ng-message="required" class="tb-error-message" translate>extension.converter-json-required</div>
<div ng-message="converterJSON" class="tb-error-message" translate>extension.converter-json-parse</div>
</div>
</div>
<v-accordion ng-if='map.converterType =="json"' id="mqtt-attributes-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="mqtt-attributes-pane_{{brokerIndex}}{{mapIndex}}">
<v-pane-header>
{{ 'extension.attributes' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="map.converter.attributes.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(attributeIndex, attribute) in map.converter.attributes">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attribute, map.converter.attributes)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="60" class="md-block" md-is-error="theForm['mqttAttributeKey_' + brokerIndex + mapIndex + attributeIndex].$touched && theForm['mqttAttributeKey_' + brokerIndex + mapIndex + attributeIndex].$invalid">
<label translate>extension.key</label>
<input required name="mqttAttributeKey_{{brokerIndex}}{{mapIndex}}{{attributeIndex}}" ng-model="attribute.key">
<div ng-messages="theForm['mqttAttributeKey_' + brokerIndex + mapIndex + attributeIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="40" class="md-block" md-is-error="theForm['mqttAttributeType_' + brokerIndex + mapIndex + attributeIndex].$touched && theForm['mqttAttributeType_' + brokerIndex + mapIndex + attributeIndex].$invalid">
<label translate>extension.type</label>
<md-select required name="mqttAttributeType_{{brokerIndex}}{{mapIndex}}{{attributeIndex}}" ng-model="attribute.type">
<md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
{{attrTypeValue | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['mqttAttributeType_' + brokerIndex + mapIndex + attributeIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<md-input-container class="md-block" md-is-error="theForm['mqttAttributeValue_' + brokerIndex + mapIndex + attributeIndex].$touched && theForm['mqttAttributeValue_' + brokerIndex + mapIndex + attributeIndex].$invalid">
<label translate>extension.value</label>
<input required name="mqttAttributeValue_{{brokerIndex}}{{mapIndex}}{{attributeIndex}}" ng-model="attribute.value">
<div ng-messages="theForm['mqttAttributeValue_' + brokerIndex + mapIndex + attributeIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addAttribute(map.converter.attributes)" aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-attribute</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion ng-if='map.converterType =="json"' id="mqtt-timeseries-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="mqtt-timeseries-pane_{{brokerIndex}}{{mapIndex}}">
<v-pane-header>
{{ 'extension.timeseries' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="map.converter.timeseries.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(timeseriesIndex, timeseries) in map.converter.timeseries">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(timeseries, map.converter.timeseries)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="60" class="md-block" md-is-error="theForm['mqttTimeseriesKey_' + brokerIndex + mapIndex + timeseriesIndex].$touched && theForm['mqttTimeseriesKey_' + brokerIndex + mapIndex + timeseriesIndex].$invalid">
<label translate>extension.key</label>
<input required name="mqttTimeseriesKey_{{brokerIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.key">
<div ng-messages="theForm['mqttTimeseriesKey_' + brokerIndex + mapIndex + timeseriesIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="40" class="md-block" md-is-error="theForm['mqttTimeseriesType_' + brokerIndex + mapIndex + timeseriesIndex].$touched && theForm['mqttTimeseriesType_' + brokerIndex + mapIndex + timeseriesIndex].$invalid">
<label translate>extension.type</label>
<md-select required name="mqttTimeseriesType_{{brokerIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.type">
<md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
{{attrTypeValue | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['mqttTimeseriesType_' + brokerIndex + mapIndex + timeseriesIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<md-input-container class="md-block" md-is-error="theForm['mqttTimeseriesValue_' + brokerIndex + mapIndex + timeseriesIndex].$touched && theForm['mqttTimeseriesValue_' + brokerIndex + mapIndex + timeseriesIndex].$invalid">
<label translate>extension.value</label>
<input required name="mqttTimeseriesValue_{{brokerIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.value">
<div ng-messages="theForm['mqttTimeseriesValue_' + brokerIndex + mapIndex + timeseriesIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addAttribute(map.converter.timeseries)" aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-timeseries</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addMap(broker.mapping)" aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-map</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion id="mqtt-connect-requests-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="mqtt-connect-requests-pane_{{brokerIndex}}">
<v-pane-header>
{{ 'extension.connect-requests' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="broker.connectRequests.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(connectRequestIndex, connectRequest) in broker.connectRequests">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(connectRequest, broker.connectRequests)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<md-input-container class="md-block">
<label translate>extension.topic-filter</label>
<input required name="conRequestTopicFilter_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.topicFilter">
<div ng-messages="theForm['conRequestTopicFilter_' + brokerIndex + connectRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<section flex layout="row">
<md-input-container flex="40" class="md-block" md-is-error="theForm['connectDeviceNameExpression_' + brokerIndex + connectRequestIndex].$touched && theForm['connectDeviceNameExpression_' + brokerIndex + connectRequestIndex].$invalid">
<label translate>extension.device-name-expression</label>
<md-select required name="connectDeviceNameExpression_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.nameExp" ng-change="changeNameExpression(connectRequest, 'connect')">
<md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
{{value | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['connectDeviceNameExpression_' + brokerIndex + connectRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="connectRequest.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block">
<label translate>extension.json-name-expression</label>
<input required name="connectJsonNameExp_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.deviceNameJsonExpression">
<div ng-messages="theForm['connectJsonNameExp_' + brokerIndex + connectRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="connectRequest.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block">
<label translate>extension.topic-name-expression</label>
<input required name="connectTopicNameExp_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.deviceNameTopicExpression">
<div ng-messages="theForm['connectTopicNameExp_' + brokerIndex + connectRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click='addConnectRequest(broker.connectRequests, "connect")' aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-connect-request</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion id="mqtt-disconnect-requests-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="mqtt-disconnect-requests-pane_{{brokerIndex}}">
<v-pane-header>
{{ 'extension.disconnect-requests' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="broker.disconnectRequests.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(disconnectRequestIndex, disconnectRequest) in broker.disconnectRequests">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(disconnectRequest, broker.disconnectRequests)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<md-input-container class="md-block">
<label translate>extension.topic-filter</label>
<input required name="disconRequestTopicFilter_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.topicFilter">
<div ng-messages="theForm['disconRequestTopicFilter_' + brokerIndex + disconnectRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<section flex layout="row">
<md-input-container flex="40" class="md-block" md-is-error="theForm['disconnectDeviceNameExpression_' + brokerIndex + disconnectRequestIndex].$touched && theForm['disconnectDeviceNameExpression_' + brokerIndex + disconnectRequestIndex].$invalid">
<label translate>extension.device-name-expression</label>
<md-select required name="disconnectDeviceNameExpression_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.nameExp" ng-change="changeNameExpression(disconnectRequest, 'disconnect')">
<md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
{{value | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['disconnectDeviceNameExpression_' + brokerIndex + disconnectRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="disconnectRequest.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block">
<label translate>extension.json-name-expression</label>
<input required name="disconnectJsonNameExp_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.deviceNameJsonExpression">
<div ng-messages="theForm['disconnectJsonNameExp_' + brokerIndex + disconnectRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="disconnectRequest.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block">
<label translate>extension.topic-name-expression</label>
<input required name="disconnectTopicNameExp_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.deviceNameTopicExpression">
<div ng-messages="theForm['disconnectTopicNameExp_' + brokerIndex + disconnectRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click='addConnectRequest(broker.disconnectRequests, "disconnect")' aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-disconnect-request</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion id="mqtt-attribute-requests-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="mqtt-attribute-requests-pane_{{brokerIndex}}">
<v-pane-header>
{{ 'extension.attribute-requests' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="broker.attributeRequests.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(attributeRequestIndex, attributeRequest) in broker.attributeRequests">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attributeRequest, broker.attributeRequests)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="80" class="md-block">
<label translate>extension.topic-filter</label>
<input required name="attributeRequestTopicFilter_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.topicFilter">
<div ng-messages="theForm['attributeRequestTopicFilter_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="20" class="md-block">
<md-checkbox flex aria-label="{{ 'extension.client-scope' | translate }}"
ng-model="attributeRequest.clientScope">{{ 'extension.client-scope' | translate }}
</md-checkbox>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="40" class="md-block" md-is-error="theForm['attrRequestDeviceNameExpression_' + brokerIndex + attributeRequestIndex].$touched && theForm['attrRequestDeviceNameExpression_' + brokerIndex + attributeRequestIndex].$invalid">
<label translate>extension.device-name-expression</label>
<md-select required name="attrRequestDeviceNameExpression_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.nameExp" ng-change="changeNameExpression(attributeRequest, 'attribute')">
<md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
{{value | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['attrRequestDeviceNameExpression_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="attributeRequest.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block">
<label translate>extension.json-name-expression</label>
<input required name="attrRequestJsonNameExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.deviceNameJsonExpression">
<div ng-messages="theForm['attrRequestJsonNameExp_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="attributeRequest.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block">
<label translate>extension.topic-name-expression</label>
<input required name="attrRequestTopicNameExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.deviceNameTopicExpression">
<div ng-messages="theForm['attrRequestTopicNameExp_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="40" class="md-block" md-is-error="theForm['attrRequestAttributeKeyExpression_' + brokerIndex + attributeRequestIndex].$touched && theForm['attrRequestAttributeKeyExpression_' + brokerIndex + attributeRequestIndex].$invalid">
<label translate>extension.attribute-key-expression</label>
<md-select required name="attrRequestAttributeKeyExpression_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.attrKey" ng-change="changeAttrKeyExpression(attributeRequest)">
<md-option ng-repeat="(key, value) in attributeKeyExpressions" ng-value='key'>
{{value | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['attrRequestAttributeKeyExpression_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="attributeRequest.attrKey == 'attributeKeyJsonExpression'" flex="60" class="md-block">
<label translate>extension.attr-json-key-expression</label>
<input required name="attrRequestJsonKeyExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.attributeKeyJsonExpression">
<div ng-messages="theForm['attrRequestJsonKeyExp_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="attributeRequest.attrKey == 'attributeKeyTopicExpression'" flex="60" class="md-block">
<label translate>extension.attr-topic-key-expression</label>
<input required name="attrRequestTopicKeyExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.attributeKeyTopicExpression">
<div ng-messages="theForm['attrRequestTopicKeyExp_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="40" class="md-block" md-is-error="theForm['attrRequestIdExpression_' + brokerIndex + attributeRequestIndex].$touched && theForm['attrRequestIdExpression_' + brokerIndex + attributeRequestIndex].$invalid">
<label translate>extension.request-id-expression</label>
<md-select required name="attrRequestIdExpression_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.requestId" ng-change="changeRequestIdExpression(attributeRequest)">
<md-option ng-repeat="(key, value) in requestIdExpressions" ng-value='key'>
{{value | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['attrRequestIdExpression_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="attributeRequest.requestId == 'requestIdJsonExpression'" flex="60" class="md-block">
<label translate>extension.request-id-json-expression</label>
<input required name="attrRequestJsonIdExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.requestIdJsonExpression">
<div ng-messages="theForm['attrRequestJsonIdExp_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="attributeRequest.requestId == 'requestIdTopicExpression'" flex="60" class="md-block">
<label translate>extension.request-id-topic-expression</label>
<input required name="attrRequestTopicIdExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.requestIdTopicExpression">
<div ng-messages="theForm['attrRequestTopicIdExp_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<md-input-container class="md-block">
<label translate>extension.response-topic-expression</label>
<input required name="attributeRequestResponseTopicExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.responseTopicExpression">
<div ng-messages="theForm['attributeRequestResponseTopicExp_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container class="md-block">
<label translate>extension.value-expression</label>
<input required name="attributeRequestValueExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.valueExpression">
<div ng-messages="theForm['attributeRequestValueExp_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addAttributeRequest(broker.attributeRequests)" aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-attribute-request</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion id="mqtt-attribute-updates-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="mqtt-attribute-updates-pane_{{brokerIndex}}">
<v-pane-header>
{{ 'extension.attribute-updates' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="broker.attributeUpdates.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(attributeUpdateIndex, attributeUpdate) in broker.attributeUpdates">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attributeUpdate, broker.attributeUpdates)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.device-name-filter</label>
<input required name="attributeUpdateDeviceNameFilter_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.deviceNameFilter">
<div ng-messages="theForm['attributeUpdateDeviceNameFilter_' + brokerIndex + attributeUpdateIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.attribute-filter</label>
<input required name="attributeUpdateAttributeFilter_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.attributeFilter">
<div ng-messages="theForm['attributeUpdateAttributeFilter_' + brokerIndex + attributeUpdateIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<md-input-container class="md-block">
<label translate>extension.topic-expression</label>
<input required name="attributeUpdateTopicExp_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.topicExpression">
<div ng-messages="theForm['attributeUpdateTopicExp_' + brokerIndex + attributeUpdateIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container class="md-block">
<label translate>extension.value-expression</label>
<input required name="attributeUpdateValueExp_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.valueExpression">
<div ng-messages="theForm['attributeUpdateValueExp_' + brokerIndex + attributeUpdateIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click='addAttributeUpdate(broker.attributeUpdates)' aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-attribute-update</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion id="mqtt-server-side-rpc-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="mqtt-server-side-rpc-pane_{{brokerIndex}}">
<v-pane-header>
{{ 'extension.server-side-rpc' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="broker.serverSideRpc.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(rpcIndex, rpc) in broker.serverSideRpc">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(rpc, broker.serverSideRpc)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.device-name-filter</label>
<input required name="serverSideRpcDeviceNameFilter_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.deviceNameFilter">
<div ng-messages="theForm['serverSideRpcDeviceNameFilter_' + brokerIndex + rpcIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.method-filter</label>
<input required name="serverSideRpcMethodFilter_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.methodFilter">
<div ng-messages="theForm['serverSideRpcMethodFilter_' + brokerIndex + rpcIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<md-input-container class="md-block">
<label translate>extension.request-topic-expression</label>
<input required name="serverSideRpcRequestTopicExp_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.requestTopicExpression">
<div ng-messages="theForm['serverSideRpcRequestTopicExp_' + brokerIndex + rpcIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container class="md-block">
<label translate>extension.response-topic-expression</label>
<input name="serverSideRpcResponseTopicExp_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.responseTopicExpression" parse-to-null>
</md-input-container>
<section flex layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.response-timeout</label>
<input type="number" name="serverSideRpcResponseTimeout_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.responseTimeout" parse-to-null>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.value-expression</label>
<input required name="serverSideRpcValueExp_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.valueExpression">
<div ng-messages="theForm['serverSideRpcValueExp_' + brokerIndex + rpcIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click='addServerSideRpc(broker.serverSideRpc)' aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-server-side-rpc-request</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addBroker()" aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-broker</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<!--<pre>
{{config | json}}
</pre>-->
</md-card-content>
</md-card>

173
ui/src/app/extension/extensions-forms/extension-form-opc.directive.js

@ -0,0 +1,173 @@
/*
* Copyright © 2016-2017 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 'brace/ext/language_tools';
import 'brace/mode/json';
import 'brace/theme/github';
import './extension-form.scss';
/* eslint-disable angular/log */
import extensionFormOpcTemplate from './extension-form-opc.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function ExtensionFormOpcDirective($compile, $templateCache, $translate, types) {
var linker = function(scope, element) {
function Server() {
this.applicationName = "Thingsboard OPC-UA client";
this.applicationUri = "";
this.host = "localhost";
this.port = 49320;
this.scanPeriodInSeconds = 10;
this.timeoutInMillis = 5000;
this.security = "Basic128Rsa15";
this.identity = {
"type": "anonymous"
};
this.keystore = {
"type": "PKCS12",
"location": "example.pfx",
"password": "secret",
"alias": "gateway",
"keyPassword": "secret"
};
this.mapping = []
}
function Map() {
this.deviceNodePattern = "Channel1\\.Device\\d+$";
this.deviceNamePattern = "Device ${_System._DeviceId}";
this.attributes = [];
this.timeseries = [];
}
function Attribute() {
this.key = "Tag1";
this.type = "string";
this.value = "${Tag1}";
}
function Timeseries() {
this.key = "Tag2";
this.type = "long";
this.value = "${Tag2}";
}
var template = $templateCache.get(extensionFormOpcTemplate);
element.html(template);
scope.types = types;
scope.theForm = scope.$parent.theForm;
if (!scope.configuration.servers.length) {
scope.configuration.servers.push(new Server());
}
scope.addServer = function(serversList) {
serversList.push(new Server());
// scope.addMap(serversList[serversList.length-1].mapping);
scope.theForm.$setDirty();
};
scope.addMap = function(mappingList) {
mappingList.push(new Map());
scope.theForm.$setDirty();
};
scope.addNewAttribute = function(attributesList) {
attributesList.push(new Attribute());
scope.theForm.$setDirty();
};
scope.addNewTimeseries = function(timeseriesList) {
timeseriesList.push(new Timeseries());
scope.theForm.$setDirty();
};
scope.removeItem = (item, itemList) => {
var index = itemList.indexOf(item);
if (index > -1) {
itemList.splice(index, 1);
}
scope.theForm.$setDirty();
};
$compile(element.contents())(scope);
scope.fileAdded = function($file, model, options) {
let reader = new FileReader();
reader.onload = function(event) {
scope.$apply(function() {
if(event.target.result) {
scope.theForm.$setDirty();
let addedFile = event.target.result;
if (addedFile && addedFile.length > 0) {
model[options.fileName] = $file.name;
model[options.file] = addedFile.replace(/^data.*base64,/, "");
}
}
});
};
reader.readAsDataURL($file.file);
};
scope.clearFile = function(model, options) {
scope.theForm.$setDirty();
model[options.fileName] = null;
model[options.file] = null;
};
scope.collapseValidation = function(index, id) {
var invalidState = angular.element('#'+id+':has(.ng-invalid)');
if(invalidState.length) {
invalidState.addClass('inner-invalid');
}
};
scope.expandValidation = function (index, id) {
var invalidState = angular.element('#'+id);
invalidState.removeClass('inner-invalid');
};
};
return {
restrict: "A",
link: linker,
scope: {
configuration: "=",
isAdd: "="
}
}
}

557
ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html

@ -0,0 +1,557 @@
<!--
Copyright © 2016-2017 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.
-->
<md-card class="extension-form extension-opc">
<md-card-title>
<md-card-title-text>
<span translate class="md-headline">extension.configuration</span>
</md-card-title-text>
</md-card-title>
<md-card-content>
<v-accordion id="opc-server-configs-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="opc-servers-pane" expanded="true">
<v-pane-header>
{{ 'extension.opc-server' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="configuration.servers.length === 0">
<span translate layout-align="center center" class="tb-prompt">extension.opc-add-server-prompt</span>
</div>
<div ng-if="configuration.servers.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(serverIndex, server) in configuration.servers">
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeItem(server, configuration.servers)"
ng-hide="configuration.servers.length < 2"
>
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<div layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.opc-application-name</label>
<input required name="applicationName_{{serverIndex}}" ng-model="server.applicationName">
<div ng-messages="theForm['applicationName_' + serverIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block" md-is-error="theForm['applicationUri_' + serverIndex].$touched && theForm['applicationUri_' + serverIndex].$invalid">
<label translate>extension.opc-application-uri</label>
<input required name="applicationUri_{{serverIndex}}" ng-model="server.applicationUri">
<div ng-messages="theForm['applicationUri_' + serverIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</div>
<div layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.host</label>
<input required name="host_{{serverIndex}}" ng-model="server.host">
<div ng-messages="theForm['host_' + serverIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.port</label>
<input type="number"
required
name="port_{{serverIndex}}"
ng-model="server.port"
min="1"
max="65535"
>
<div ng-messages="theForm['port_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.port-range</div>
<div translate
ng-message="max"
>extension.port-range</div>
</div>
</md-input-container>
</div>
<div layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.opc-scan-period-in-seconds</label>
<input type="number"
required
name="scanPeriodInSeconds_{{serverIndex}}"
ng-model="server.scanPeriodInSeconds">
<div ng-messages="theForm['scanPeriodInSeconds_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.timeout</label>
<input type="number"
required name="timeoutInMillis_{{serverIndex}}"
ng-model="server.timeoutInMillis"
>
<div ng-messages="theForm['timeoutInMillis_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</div>
<div layout="row">
<md-input-container flex="50" class="md-block tb-container-for-select">
<label translate>extension.opc-security</label>
<md-select required
name="securityType_{{serverIndex}}"
ng-model="server.security">
<md-option ng-value="securityType"
ng-repeat="(securityType, securityValue) in types.extensionOpcSecurityTypes"
><span ng-bind="::securityValue"></span></md-option>
</md-select>
<div ng-messages="theForm['securityType_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block tb-container-for-select">
<label translate>extension.opc-identity</label>
<md-select required
name="identityType_{{serverIndex}}"
ng-model="server.identity.type"
>
<md-option ng-value="identityType"
ng-repeat="(identityType, identityValue) in types.extensionIdentityType"
><span ng-bind="identityValue | translate"></span></md-option>
</md-select>
<div ng-messages="theForm['identityType_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</div>
<div ng-if="server.identity.type != 'username'">
<span class=""
ng-init="server.identity = {'type':'anonymous'}"></span>
</div>
<div layout="row" ng-if="server.identity.type == 'username'">
<md-input-container flex="50" class="md-block" md-is-error="theForm['identityUsername_' + serverIndex].$touched && theForm['identityUsername_' + serverIndex].$invalid">
<label translate>extension.username</label>
<input required
name="identityUsername_{{serverIndex}}"
ng-model="server.identity.username"
>
<div ng-messages="theForm['identityUsername_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block" md-is-error="theForm['identityPassword_' + serverIndex].$touched && theForm['identityPassword_' + serverIndex].$invalid">
<label translate>extension.password</label>
<input required
name="identityPassword_{{serverIndex}}" ng-model="server.identity.password">
<div ng-messages="theForm['identityPassword_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</div>
<v-accordion id="opc-keystore-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="opc-keystore-pane__{{serverIndex}}" expanded="true">
<v-pane-header>
{{ 'extension.opc-keystore' | translate }}
</v-pane-header>
<v-pane-content>
<md-input-container class="md-block tb-container-for-select">
<label translate>extension.opc-keystore-type</label>
<md-select required name="keystoreType_{{serverIndex}}" ng-model="server.keystore.type">
<md-option ng-value="keystoreType" ng-repeat="(keystoreType, keystoreValue) in types.extensionKeystoreType"><span ng-bind="::keystoreValue"></span></md-option>
</md-select>
<div ng-messages="theForm['keystoreType_'+serverIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<section class="dropdown-section">
<div class="tb-container" ng-class="{'ng-invalid':!server.keystore.file}">
<span ng-init='fieldsToFill = {"fileName":"fileName", "file":"file"}'></span>
<label class="tb-label" translate>extension.opc-keystore-location</label>
<div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, server.keystore, fieldsToFill)' class="tb-file-select-container">
<div class="tb-file-clear-container">
<md-button ng-click='clearFile(server.keystore, fieldsToFill)' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
</md-button>
</div>
<div class="alert tb-flow-drop" flow-drop>
<label for="dropFileKeystore_{{serverIndex}}" translate>extension.drop-file</label>
<input flow-attrs="{accept:'.pfx,.p12'}"
type="file"
class="file-input"
flow-btn id="dropFileKeystore_{{serverIndex}}"
name="keystoreFile"
ng-model="server.keystore.file"
>
</div>
</div>
</div>
<div class="dropdown-messages">
<div ng-if="!server.keystore[fieldsToFill.fileName]" class="tb-error-message" translate>extension.no-file</div>
<div ng-if="server.keystore[fieldsToFill.fileName]">{{server.keystore[fieldsToFill.fileName]}}</div>
</div>
</section>
<div flex layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.opc-keystore-password</label>
<input required name="keystorePassword_{{serverIndex}}" ng-model="server.keystore.password">
<div ng-messages="theForm['keystorePassword_' + serverIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.opc-keystore-alias</label>
<input required name="keystoreAlias_{{serverIndex}}" ng-model="server.keystore.alias">
<div ng-messages="theForm['keystoreAlias_' + serverIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</div>
<md-input-container class="md-block">
<label translate>extension.opc-keystore-key-password</label>
<input required name="keystoreKeyPassword_{{serverIndex}}" ng-model="server.keystore.keyPassword">
<div ng-messages="theForm['keystoreKeyPassword_' + serverIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion id="opc-mapping-accordion"
class="vAccordion--default"
onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="opc-mapping-pane_{{serverIndex}}">
<v-pane-header>
{{ 'extension.mapping' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="server.mapping.length > 0">
<ol class="list-group">
<li class="list-group-item"
ng-repeat="(mapIndex, map) in server.mapping"
>
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeItem(map, server.mapping)"
>
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<div flex layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.opc-device-node-pattern</label>
<input required
name="deviceNodePattern_{{serverIndex}}{{mapIndex}}"
ng-model="map.deviceNodePattern"
>
<div ng-messages="theForm['deviceNodePattern_' + serverIndex + mapIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.opc-device-name-pattern</label>
<input required
name="deviceNamePattern_{{serverIndex}}{{mapIndex}}"
ng-model="map.deviceNamePattern"
>
<div ng-messages="theForm['deviceNamePattern_' + serverIndex + mapIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</div>
<v-accordion id="opc-attributes-accordion"
class="vAccordion--default"
onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="opc-attributes-pane_{{serverIndex}}{{mapIndex}}">
<v-pane-header>
{{ 'extension.attributes' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-show="map.attributes.length > 0">
<ol class="list-group">
<li class="list-group-item"
ng-repeat="(attributeIndex, attribute) in map.attributes"
>
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeItem(attribute, map.attributes)">
<ng-md-icon icon="close"
aria-label="{{ 'action.remove' | translate }}"
></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex
layout="row"
>
<md-input-container flex="60" class="md-block">
<label translate>extension.key</label>
<input required
name="opcAttributeKey_{{serverIndex}}{{mapIndex}}{{attributeIndex}}"
ng-model="attribute.key"
>
<div ng-messages="theForm['opcAttributeKey_' + serverIndex + mapIndex + attributeIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="40" class="md-block tb-container-for-select">
<label translate>extension.type</label>
<md-select required name="opcAttributeType_{{serverIndex}}{{mapIndex}}{{attributeIndex}}"
ng-model="attribute.type"
>
<md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType"
ng-value="attrType"
>
{{attrTypeValue | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['opcAttributeType_' + serverIndex + mapIndex + attributeIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="100" class="md-block">
<label translate>extension.value</label>
<input required name="opcAttributeValue_{{serverIndex}}{{mapIndex}}{{attributeIndex}}"
ng-model="attribute.value"
>
<div ng-messages="theForm['opcAttributeValue_' + serverIndex + mapIndex + attributeIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</section>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addNewAttribute(map.attributes)"
aria-label="{{ 'action.add' | translate }}"
>
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-attribute</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion id="opc-timeseries-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="opc-timeseries-pane_{{serverIndex}}{{mapIndex}}">
<v-pane-header>
{{ 'extension.timeseries' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-show="map.timeseries.length > 0">
<ol class="list-group">
<li class="list-group-item"
ng-repeat="(timeseriesIndex, timeseries) in map.timeseries"
>
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeItem(timeseries, map.timeseries)"
>
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="60" class="md-block">
<label translate>extension.key</label>
<input required
name="opcTimeseriesKey_{{serverIndex}}{{mapIndex}}{{timeseriesIndex}}"
ng-model="timeseries.key"
>
<div ng-messages="theForm['opcTimeseriesKey_' + serverIndex + mapIndex + timeseriesIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="40"
class="md-block tb-container-for-select"
>
<label translate>extension.type</label>
<md-select required
name="opcTimeseriesType_{{serverIndex}}{{mapIndex}}{{timeseriesIndex}}"
ng-model="timeseries.type"
>
<md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType"
ng-value="attrType"
>
{{attrTypeValue | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['opcTimeseriesType_' + serverIndex + mapIndex + timeseriesIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="100" class="md-block">
<label translate>extension.value</label>
<input required name="opcTimeseriesValue_{{serverIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.value">
<div ng-messages="theForm['opcTimeseriesValue_' + serverIndex + mapIndex + timeseriesIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addNewAttribute(map.timeseries)"
aria-label="{{ 'action.add' | translate }}"
>
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-timeseries</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex
layout="row"
layout-align="start center"
>
<md-button class="md-primary md-raised"
ng-click="addMap(server.mapping)"
aria-label="{{ 'action.add' | translate }}"
>
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-map</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
</md-card-content>
</md-card>
</li>
</ol>
<div flex
layout="row"
layout-align="start center"
>
<md-button class="md-primary md-raised"
ng-click="addServer(configuration.servers)"
aria-label="{{ 'action.add' | translate }}"
>
<md-icon class="material-icons">add</md-icon>
<span translate>extension.opc-add-server</span>
</md-button>
</div>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<!--{{config}}-->
</md-card-content>
</md-card>

76
ui/src/app/extension/extensions-forms/extension-form.scss

@ -0,0 +1,76 @@
/**
* Copyright © 2016-2017 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.
*/
.extension-form {
li > .md-button {
color: rgba(0, 0, 0, 0.7);
margin: 0;
}
.vAccordion--default {
margin-top: 0;
padding-left: 3px;
}
.tb-container {
width:100%;
}
.dropdown-messages {
.tb-error-message {
padding: 5px 0 0 0;
}
}
.dropdown-section {
margin-bottom: 30px;
}
v-pane.inner-invalid > v-pane-header {
border-bottom: 2px solid rgb(221,44,0);
}
}
.extension-form.extension-mqtt {
md-checkbox{
margin-left: 10px;
}
}
.tb-extension-custom-transformer-panel {
margin-left: 15px;
border: 1px solid #C0C0C0;
height: 100%;
.tb-extension-custom-transformer {
min-width: 600px;
min-height: 200px;
width: 100%;
height: 100%;
}
.ace_text-input {
position:absolute!important
}
}
.extensionDialog {
min-width: 1000px;
}
.tb-container-for-select {
height: 58px;
}
.tb-drop-file-input-hide {
height: 200%;
display: block;
position: absolute;
bottom: 0;
width: 100%;
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save