Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard

pull/496/head
Andrew Shvayka 9 years ago
parent
commit
57c0fff3a9
  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
      common/data/pom.xml
  6. 2
      common/message/pom.xml
  7. 2
      common/pom.xml
  8. 2
      common/transport/pom.xml
  9. 2
      dao/pom.xml
  10. 2
      extensions-api/pom.xml
  11. 2
      extensions-core/pom.xml
  12. 2
      extensions/extension-kafka/pom.xml
  13. 2
      extensions/extension-mqtt/pom.xml
  14. 2
      extensions/extension-rabbitmq/pom.xml
  15. 2
      extensions/extension-rest-api-call/pom.xml
  16. 2
      extensions/extension-sns/pom.xml
  17. 2
      extensions/extension-sqs/pom.xml
  18. 0
      extensions/extension-sqs/src/assembly/extension.xml
  19. 0
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionMsg.java
  20. 0
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionPayload.java
  21. 0
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginAction.java
  22. 0
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginActionConfiguration.java
  23. 0
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionMsg.java
  24. 0
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionPayload.java
  25. 0
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginAction.java
  26. 0
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginActionConfiguration.java
  27. 0
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsMessageHandler.java
  28. 0
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPlugin.java
  29. 0
      extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPluginConfiguration.java
  30. 0
      extensions/extension-sqs/src/main/resources/SqsFifoQueueActionDescriptor.json
  31. 0
      extensions/extension-sqs/src/main/resources/SqsPluginDescriptor.json
  32. 0
      extensions/extension-sqs/src/main/resources/SqsStandardQueueActionDescriptor.json
  33. 0
      extensions/extension-sqs/src/test/java/org/thingsboard/server/extensions/sqs/SqsDemoClient.java
  34. 0
      extensions/extension-sqs/src/test/resources/logback.xml
  35. 4
      extensions/pom.xml
  36. 2
      tools/pom.xml
  37. 2
      transport/coap/pom.xml
  38. 2
      transport/http/pom.xml
  39. 2
      transport/mqtt/pom.xml
  40. 2
      transport/pom.xml
  41. 1
      ui/package.json
  42. 2
      ui/pom.xml
  43. 3
      ui/src/app/api/widget.service.js
  44. 3
      ui/src/app/app.js
  45. 2
      ui/src/app/asset/asset-card.tpl.html
  46. 4
      ui/src/app/asset/assign-to-customer.controller.js
  47. 47
      ui/src/app/common/types.constant.js
  48. 137
      ui/src/app/components/dashboard.directive.js
  49. 4
      ui/src/app/device/assign-to-customer.controller.js
  50. 2
      ui/src/app/device/device-card.tpl.html
  51. 7
      ui/src/app/device/devices.tpl.html
  52. 4
      ui/src/app/entity/attribute/attribute-table.tpl.html
  53. 298
      ui/src/app/extension/extension-dialog.controller.js
  54. 81
      ui/src/app/extension/extension-dialog.tpl.html
  55. 404
      ui/src/app/extension/extension-table.directive.js
  56. 47
      ui/src/app/extension/extension-table.scss
  57. 159
      ui/src/app/extension/extension-table.tpl.html
  58. 147
      ui/src/app/extension/extensions-forms/extension-form-http.directive.js
  59. 311
      ui/src/app/extension/extensions-forms/extension-form-http.tpl.html
  60. 347
      ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js
  61. 860
      ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html
  62. 173
      ui/src/app/extension/extensions-forms/extension-form-opc.directive.js
  63. 557
      ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
  64. 76
      ui/src/app/extension/extensions-forms/extension-form.scss
  65. 29
      ui/src/app/extension/index.js
  66. 4
      ui/src/app/home/home-links.controller.js
  67. 90
      ui/src/app/import-export/import-export.service.js
  68. 2
      ui/src/app/layout/index.js
  69. 131
      ui/src/app/locale/locale.constant.js
  70. 18
      ui/src/app/widget/lib/CanvasDigitalGauge.js
  71. 4
      ui/src/app/widget/lib/analogue-compass.js
  72. 4
      ui/src/app/widget/lib/analogue-linear-gauge.js
  73. 4
      ui/src/app/widget/lib/analogue-radial-gauge.js
  74. 9
      ui/src/app/widget/lib/canvas-digital-gauge.js
  75. 135
      ui/src/app/widget/lib/extensions-table-widget.js
  76. 29
      ui/src/app/widget/lib/extensions-table-widget.scss
  77. 27
      ui/src/app/widget/lib/extensions-table-widget.tpl.html
  78. 2
      ui/src/index.html
  79. 2
      ui/src/scss/animations.scss

10
application/pom.xml

@ -496,6 +496,16 @@
<artifactId>extension-mqtt</artifactId>
<classifier>extension</classifier>
</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>
</configuration>
</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",
"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}",
"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",
"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}",
"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
common/data/pom.xml

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

2
common/message/pom.xml

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

2
common/pom.xml

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

2
common/transport/pom.xml

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

2
dao/pom.xml

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

2
extensions-api/pom.xml

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

2
extensions-core/pom.xml

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

2
extensions/extension-kafka/pom.xml

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

2
extensions/extension-mqtt/pom.xml

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

2
extensions/extension-rabbitmq/pom.xml

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

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

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

2
extensions/extension-sns/pom.xml

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

2
extensions/extention-sqs/pom.xml → extensions/extension-sqs/pom.xml

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

0
extensions/extention-sqs/src/assembly/extension.xml → extensions/extension-sqs/src/assembly/extension.xml

0
extensions/extention-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionMsg.java → extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionMsg.java

0
extensions/extention-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionPayload.java → extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionPayload.java

0
extensions/extention-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginAction.java → extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginAction.java

0
extensions/extention-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginActionConfiguration.java → extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginActionConfiguration.java

0
extensions/extention-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionMsg.java → extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionMsg.java

0
extensions/extention-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionPayload.java → extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionPayload.java

0
extensions/extention-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginAction.java → extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginAction.java

0
extensions/extention-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginActionConfiguration.java → extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginActionConfiguration.java

0
extensions/extention-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsMessageHandler.java → extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsMessageHandler.java

0
extensions/extention-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPlugin.java → extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPlugin.java

0
extensions/extention-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPluginConfiguration.java → extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPluginConfiguration.java

0
extensions/extention-sqs/src/main/resources/SqsFifoQueueActionDescriptor.json → extensions/extension-sqs/src/main/resources/SqsFifoQueueActionDescriptor.json

0
extensions/extention-sqs/src/main/resources/SqsPluginDescriptor.json → extensions/extension-sqs/src/main/resources/SqsPluginDescriptor.json

0
extensions/extention-sqs/src/main/resources/SqsStandardQueueActionDescriptor.json → extensions/extension-sqs/src/main/resources/SqsStandardQueueActionDescriptor.json

0
extensions/extention-sqs/src/test/java/org/thingsboard/server/extensions/sqs/SqsDemoClient.java → extensions/extension-sqs/src/test/java/org/thingsboard/server/extensions/sqs/SqsDemoClient.java

0
extensions/extention-sqs/src/test/resources/logback.xml → extensions/extension-sqs/src/test/resources/logback.xml

4
extensions/pom.xml

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

2
tools/pom.xml

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

2
transport/coap/pom.xml

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

2
transport/http/pom.xml

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

2
transport/mqtt/pom.xml

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

2
transport/pom.xml

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

1
ui/package.json

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

2
ui/pom.xml

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

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 thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget';
import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget';
import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget';
import thingsboardRpcWidgets from '../widget/lib/rpc';
@ -42,7 +43,7 @@ import thingsboardTypes from '../common/types.constant';
import thingsboardUtils from '../common/utils.service';
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)
.name;

3
ui/src/app/app.js

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

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

@ -16,7 +16,7 @@
-->
<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.isPublic()">{{'asset.public' | translate}}</div>
</div>

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

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

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

@ -317,6 +317,53 @@ export default angular.module('thingsboard.types', [])
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: {
value: "LATEST_TELEMETRY",
name: "attribute.scope-latest-telemetry",

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

@ -140,6 +140,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
vm.widgetLayoutInfo = {
};
vm.widgetIds = [];
vm.widgetItemMap = {
sizeX: 'vm.widgetLayoutInfo[widget.id].sizeX',
sizeY: 'vm.widgetLayoutInfo[widget.id].sizeY',
@ -233,73 +235,67 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
removeResizeListener(gridsterParent[0], onGridsterParentResize); // eslint-disable-line no-undef
});
watchWidgets();
function onGridsterParentResize() {
if (gridsterParent.height() && autofillHeight()) {
updateMobileOpts();
$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);
}
}
function watchWidgets() {
$scope.widgetsCollectionWatch = $scope.$watchCollection('vm.widgets', function () {
if (vm.skipInitialWidgetsWatch) {
$timeout(function() { vm.skipInitialWidgetsWatch = false; });
return;
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;
}
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];
}
for (var widgetId in vm.widgetLayoutInfo) {
if (ids.indexOf(widgetId) === -1) {
delete vm.widgetLayoutInfo[widgetId];
}
}
sortWidgets();
$mdUtil.nextTick(function () {
if (autofillHeight()) {
updateMobileOpts();
}
$mdUtil.nextTick(function () {
sortWidgets();
if (autofillHeight()) {
updateMobileOpts();
}
});
});
}
});
function stopWatchWidgets() {
if ($scope.widgetsCollectionWatch) {
$scope.widgetsCollectionWatch();
$scope.widgetsCollectionWatch = null;
function onGridsterParentResize() {
if (gridsterParent.height() && autofillHeight()) {
updateMobileOpts();
}
}
//TODO: widgets visibility
/*gridsterParent.scroll(function () {
updateVisibleRect();
@ -346,10 +342,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
$scope.$watch(function() { return $mdMedia('gt-sm'); }, function() {
updateMobileOpts();
sortWidgets();
});
$scope.$watch('vm.isMobile', function () {
updateMobileOpts();
sortWidgets();
});
$scope.$watch('vm.autofillHeight', function () {
@ -366,6 +364,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
$scope.$watch('vm.isMobileDisabled', function () {
updateMobileOpts();
sortWidgets();
});
$scope.$watch('vm.widgetLayouts', function () {
updateMobileOpts();
sortWidgets();
});
$scope.$watch('vm.columns', function () {
@ -410,6 +414,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
$scope.$on('gridster-resized', function (event, sizes, theGridster) {
if (checkIsLocalGridsterElement(theGridster)) {
vm.gridster = theGridster;
setupGridster(vm.gridster);
vm.isResizing = false;
//TODO: widgets visibility
//updateVisibleRect(false, true);
@ -419,6 +424,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
$scope.$on('gridster-mobile-changed', function (event, theGridster) {
if (checkIsLocalGridsterElement(theGridster)) {
vm.gridster = theGridster;
setupGridster(vm.gridster);
detectRowSize(vm.gridster.isMobile).then(
function(rowHeight) {
if (vm.gridsterOpts.rowHeight != rowHeight) {
@ -517,18 +523,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
loadDashboard();
function sortWidgets() {
stopWatchWidgets();
vm.widgets.sort(function (widget1, widget2) {
var row1 = widgetOrder(widget1);
var row2 = widgetOrder(widget2);
var res = row1 - row2;
if (res === 0) {
res = widget1.col - widget2.col;
res = widgetCol(widget1) - widgetCol(widget2);
}
return res;
});
vm.skipInitialWidgetsWatch = true;
watchWidgets();
}
function reload() {
@ -1037,6 +1040,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
$scope.gridsterScopeWatcher = null;
var gridsterScope = gridsterElement.scope();
vm.gridster = gridsterScope.gridster;
setupGridster(vm.gridster);
if (vm.onInit) {
vm.onInit({dashboard: vm});
}
@ -1046,6 +1050,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
});
}
function setupGridster(gridster) {
if (gridster) {
if (!gridster.origMoveOverlappingItems) {
gridster.origMoveOverlappingItems = gridster.moveOverlappingItems;
gridster.moveOverlappingItems = () => {};
}
}
}
function loading() {
return !vm.ignoreLoading && $rootScope.loading;
}

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

@ -77,8 +77,8 @@ export default function AssignDeviceToCustomerController(customerService, device
function assign() {
var tasks = [];
for (var deviceId in deviceIds) {
tasks.push(deviceService.assignDeviceToCustomer(vm.customers.selection.id.id, deviceIds[deviceId]));
for (var i=0;i<deviceIds.length;i++) {
tasks.push(deviceService.assignDeviceToCustomer(vm.customers.selection.id.id, deviceIds[i]));
}
$q.all(tasks).then(function () {
$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 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.isPublic()">{{'device.public' | translate}}</div>
</div>

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

@ -67,4 +67,11 @@
entity-type="{{vm.types.entityType.device}}">
</tb-relation-table>
</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>

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">
<section layout="row" ng-show="!disableAttributeScopeSelection">
<section ng-show="!disableAttributeScopeSelection">
<md-input-container class="md-block" style="width: 200px;">
<label translate>attribute.attributes-scope</label>
<md-select ng-model="attributeScope" ng-disabled="loading() || attributeScopeSelectionReadonly">
@ -26,7 +26,7 @@
</md-select>
</md-input-container>
</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'
&& !selectedAttributes.length
&& query.search === null">

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%;
}

29
ui/src/app/extension/index.js

@ -0,0 +1,29 @@
/*
* 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 ExtensionTableDirective from './extension-table.directive';
import ExtensionFormHttpDirective from './extensions-forms/extension-form-http.directive';
import ExtensionFormMqttDirective from './extensions-forms/extension-form-mqtt.directive'
import ExtensionFormOpcDirective from './extensions-forms/extension-form-opc.directive';
import {ParseToNull} from './extension-dialog.controller';
export default angular.module('thingsboard.extension', [])
.directive('tbExtensionTable', ExtensionTableDirective)
.directive('tbExtensionFormHttp', ExtensionFormHttpDirective)
.directive('tbExtensionFormMqtt', ExtensionFormMqttDirective)
.directive('tbExtensionFormOpc', ExtensionFormOpcDirective)
.directive('parseToNull', ParseToNull)
.name;

4
ui/src/app/home/home-links.controller.js

@ -33,9 +33,7 @@ export default function HomeLinksController($scope, $mdMedia, menu) {
updateColumnCount();
menu.getHomeSections().then((homeSections) => {
vm.model = homeSections;
});
vm.model = menu.getHomeSections();
function updateColumnCount() {
vm.cols = 2;

90
ui/src/app/import-export/import-export.service.js

@ -24,8 +24,9 @@ import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
/* eslint-disable no-undef, angular/window-service, angular/document-service */
/*@ngInject*/
export default function ImportExport($log, $translate, $q, $mdDialog, $document, itembuffer, utils, types, dashboardUtils,
entityService, dashboardService, pluginService, ruleService, widgetService, toast) {
export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types,
dashboardUtils, entityService, dashboardService, pluginService, ruleService,
widgetService, toast, attributeService) {
var service = {
@ -40,8 +41,11 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
exportWidgetType: exportWidgetType,
importWidgetType: importWidgetType,
exportWidgetsBundle: exportWidgetsBundle,
importWidgetsBundle: importWidgetsBundle
}
importWidgetsBundle: importWidgetsBundle,
exportExtension: exportExtension,
importExtension: importExtension,
exportToPc: exportToPc
};
return service;
@ -614,6 +618,84 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
return true;
}
function exportExtension(extensionId) {
getExtension(extensionId)
.then(
function success(extension) {
var name = extension.title;
name = name.toLowerCase().replace(/\W/g,"_");
exportToPc(prepareExport(extension), name + '.json');
},
function fail(rejection) {
var message = rejection;
if (!message) {
message = $translate.instant('error.unknown-error');
}
toast.showError($translate.instant('extension.export-failed-error', {error: message}));
}
);
function getExtension(extensionId) {
var deferred = $q.defer();
var url = '/api/plugins/telemetry/DEVICE/' + extensionId;
$http.get(url, null)
.then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
}
function importExtension($event, options) {
var deferred = $q.defer();
openImportDialog($event, 'extension.import-extensions', 'extension.file')
.then(
function success(extension) {
if (!validateImportedExtension(extension)) {
toast.showError($translate.instant('extension.invalid-file-error'));
deferred.reject();
} else {
attributeService
.saveEntityAttributes(
options.entityType,
options.entityId,
types.attributesScope.shared.value,
[{
key: "configuration",
value: angular.toJson(extension)
}]
)
.then(function success() {
options.successFunc();
});
}
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
function validateImportedExtension(configuration) {
if (configuration.length) {
for (let i = 0; i < configuration.length; i++) {
if (angular.isUndefined(configuration[i].configuration) || angular.isUndefined(configuration[i].id )|| angular.isUndefined(configuration[i].type)) {
return false;
}
}
} else {
return false;
}
return true;
}
function processEntityAliases(entityAliases, aliasIds) {
var deferred = $q.defer();
var missingEntityAliases = {};

2
ui/src/app/layout/index.js

@ -35,6 +35,7 @@ import thingsboardUserMenu from './user-menu.directive';
import thingsboardEntity from '../entity';
import thingsboardEvent from '../event';
import thingsboardAlarm from '../alarm';
import thingsboardExtension from '../extension';
import thingsboardTenant from '../tenant';
import thingsboardCustomer from '../customer';
import thingsboardUser from '../user';
@ -66,6 +67,7 @@ export default angular.module('thingsboard.home', [
thingsboardEntity,
thingsboardEvent,
thingsboardAlarm,
thingsboardExtension,
thingsboardTenant,
thingsboardCustomer,
thingsboardUser,

131
ui/src/app/locale/locale.constant.js

@ -729,6 +729,133 @@ export default angular.module('thingsboard.locale', [])
"messages-processed": "Messages processed",
"errors-occurred": "Errors occurred"
},
"extension": {
"extensions": "Extensions",
"selected-extensions": "{ count, select, 1 {1 extension} other {# extensions} } selected",
"type": "Type",
"key": "Key",
"value": "Value",
"id": "Id",
"extension-id": "Extension id",
"extension-type": "Extension type",
"transformer-json": "JSON *",
"unique-id-required": "Current extension id already exists.",
"delete": "Delete extension",
"add": "Add extension",
"edit": "Edit extension",
"delete-extension-title": "Are you sure you want to delete the extension '{{extensionId}}'?",
"delete-extension-text": "Be careful, after the confirmation the extension and all related data will become unrecoverable.",
"delete-extensions-title": "Are you sure you want to delete { count, select, 1 {1 extension} other {# extensions} }?",
"delete-extensions-text": "Be careful, after the confirmation all selected extensions will be removed.",
"converters": "Converters",
"converter-id": "Converter id",
"configuration": "Configuration",
"converter-configurations": "Converter configurations",
"token": "Security token",
"add-converter": "Add converter",
"add-config": "Add converter configuration",
"device-name-expression": "Device name expression",
"device-type-expression": "Device type expression",
"custom": "Custom",
"to-double": "To Double",
"transformer": "Transformer",
"json-required": "Transformer json is required.",
"json-parse": "Unable to parse transformer json.",
"attributes": "Attributes",
"add-attribute": "Add attribute",
"add-map": "Add mapping element",
"timeseries": "Timeseries",
"add-timeseries": "Add timeseries",
"field-required": "Field is required",
"brokers": "Brokers",
"add-broker": "Add broker",
"host": "Host",
"port": "Port",
"port-range": "Port should be in a range from 1 to 65535.",
"ssl": "Ssl",
"credentials": "Credentials",
"username": "Username",
"password": "Password",
"retry-interval": "Retry interval in milliseconds",
"anonymous": "Anonymous",
"basic": "Basic",
"pem": "PEM",
"ca-cert": "CA certificate file *",
"private-key": "Private key file *",
"cert": "Certificate file *",
"no-file": "No file selected.",
"drop-file": "Drop a file or click to select a file to upload.",
"mapping": "Mapping",
"topic-filter": "Topic filter",
"converter-type": "Converter type",
"converter-json": "Json",
"json-name-expression": "Device name json expression",
"topic-name-expression": "Device name topic expression",
"json-type-expression": "Device type json expression",
"topic-type-expression": "Device type topic expression",
"attribute-key-expression": "Attribute key expression",
"attr-json-key-expression": "Attribute key json expression",
"attr-topic-key-expression": "Attribute key topic expression",
"request-id-expression": "Request id expression",
"request-id-json-expression": "Request id json expression",
"request-id-topic-expression": "Request id topic expression",
"response-topic-expression": "Response topic expression",
"value-expression": "Value expression",
"topic": "Topic",
"timeout": "Timeout in milliseconds",
"converter-json-required": "Converter json is required.",
"converter-json-parse": "Unable to parse converter json.",
"filter-expression": "Filter expression",
"connect-requests": "Connect requests",
"add-connect-request": "Add connect request",
"disconnect-requests": "Disconnect requests",
"add-disconnect-request": "Add disconnect request",
"attribute-requests": "Attribute requests",
"add-attribute-request": "Add attribute request",
"attribute-updates": "Attribute updates",
"add-attribute-update": "Add attribute update",
"server-side-rpc": "Server side RPC",
"add-server-side-rpc-request": "Add server-side RPC request",
"device-name-filter": "Device name filter",
"attribute-filter": "Attribute filter",
"method-filter": "Method filter",
"request-topic-expression": "Request topic expression",
"response-timeout": "Response timeout in milliseconds",
"topic-expression": "Topic expression",
"client-scope": "Client scope",
"opc-server": "Servers",
"opc-add-server": "Add server",
"opc-application-name": "Application name",
"opc-application-uri": "Application uri",
"opc-scan-period-in-seconds": "Scan period in seconds",
"opc-security": "Security",
"opc-identity": "Identity",
"opc-keystore": "Keystore",
"opc-type": "Type",
"opc-keystore-type":"Type",
"opc-keystore-location":"Location *",
"opc-keystore-password":"Password",
"opc-keystore-alias":"Alias",
"opc-keystore-key-password":"Key password",
"opc-device-node-pattern":"Device node pattern",
"opc-device-name-pattern":"Device name pattern",
"sync": {
"status": "Status",
"sync": "Sync",
"not-sync": "Not sync",
"last-sync-time": "Last sync time",
"not-available": "Not available"
},
"export-extensions-configuration":"Export extensions configuration",
"import-extensions-configuration":"Import extensions configuration",
"import-extensions": "Import extensions",
"import-extension": "Import extension",
"export-extension": "Export extension",
"file": "Extensions file",
"invalid-file-error": "Invalid extension file"
},
"fullscreen": {
"expand": "Expand to fullscreen",
"exit": "Exit fullscreen",
@ -851,7 +978,6 @@ export default angular.module('thingsboard.locale', [])
"invalid-plugin-file-error": "Unable to import plugin: Invalid plugin data structure.",
"copyId": "Copy plugin Id",
"idCopiedMessage": "Plugin Id has been copied to clipboard"
},
"position": {
"top": "Top",
@ -1071,7 +1197,8 @@ export default angular.module('thingsboard.locale', [])
"boolean": "Boolean",
"boolean-value": "Boolean value",
"false": "False",
"true": "True"
"true": "True",
"long": "Long"
},
"widget": {
"widget-library": "Widgets Library",

18
ui/src/app/widget/lib/CanvasDigitalGauge.js

@ -155,6 +155,15 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
return result;
}
set timestamp(timestamp) {
this.options.timestamp = timestamp;
this.draw();
}
get timestamp() {
return this.options.timestamp;
}
draw() {
try {
@ -195,7 +204,9 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
canvas.elementClone.initialized = true;
}
if (!this.elementValueClone.initialized || this.elementValueClone.renderedValue !== this.value) {
var valueChanged = false;
if (!this.elementValueClone.initialized || this.elementValueClone.renderedValue !== this.value || (options.showTimestamp && this.elementValueClone.renderedTimestamp !== this.timestamp)) {
let context = this.contextValueClone;
// clear the cache
context.clearRect(x, y, w, h);
@ -208,10 +219,13 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
if (options.showTimestamp) {
drawDigitalLabel(context, options);
this.elementValueClone.renderedTimestamp = this.timestamp;
}
this.elementValueClone.initialized = true;
this.elementValueClone.renderedValue = this.value;
valueChanged = true;
}
var progress = (canvasGauges.drawings.normalizedValue(options).normal - options.minValue) /
@ -219,7 +233,7 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
var fixedProgress = progress.toFixed(3);
if (!this.elementProgressClone.initialized || this.elementProgressClone.renderedProgress !== fixedProgress) {
if (!this.elementProgressClone.initialized || this.elementProgressClone.renderedProgress !== fixedProgress || valueChanged) {
let context = this.contextProgressClone;
// clear the cache
context.clearRect(x, y, w, h);

4
ui/src/app/widget/lib/analogue-compass.js

@ -104,7 +104,9 @@ export default class TbAnalogueCompass {
var tvPair = cellData.data[cellData.data.length -
1];
var value = tvPair[1];
this.gauge.value = value;
if(value !== this.gauge.value) {
this.gauge.value = value;
}
}
}
}

4
ui/src/app/widget/lib/analogue-linear-gauge.js

@ -212,7 +212,9 @@ export default class TbAnalogueLinearGauge {
var tvPair = cellData.data[cellData.data.length -
1];
var value = tvPair[1];
this.gauge.value = value;
if(value !== this.gauge.value) {
this.gauge.value = value;
}
}
}
}

4
ui/src/app/widget/lib/analogue-radial-gauge.js

@ -221,7 +221,9 @@ export default class TbAnalogueRadialGauge {
var tvPair = cellData.data[cellData.data.length -
1];
var value = tvPair[1];
this.gauge.value = value;
if(value !== this.gauge.value) {
this.gauge.value = value;
}
}
}

9
ui/src/app/widget/lib/canvas-digital-gauge.js

@ -197,14 +197,19 @@ export default class TbCanvasDigitalGauge {
if (cellData.data.length > 0) {
var tvPair = cellData.data[cellData.data.length -
1];
var timestamp;
if (this.localSettings.showTimestamp) {
var timestamp = tvPair[0];
timestamp = tvPair[0];
var filter= this.ctx.$scope.$injector.get('$filter');
var timestampDisplayValue = filter('date')(timestamp, this.localSettings.timestampFormat);
this.gauge.options.label = timestampDisplayValue;
}
var value = tvPair[1];
this.gauge.value = value;
if(value !== this.gauge.value) {
this.gauge.value = value;
} else if (this.localSettings.showTimestamp && this.gauge.timestamp != timestamp) {
this.gauge.timestamp = timestamp;
}
}
}
}

135
ui/src/app/widget/lib/extensions-table-widget.js

@ -0,0 +1,135 @@
/*
* 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 './extensions-table-widget.scss';
/* eslint-disable import/no-unresolved, import/default */
import extensionsTableWidgetTemplate from './extensions-table-widget.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
export default angular.module('thingsboard.widgets.extensionsTableWidget', [])
.directive('tbExtensionsTableWidget', ExtensionsTableWidget)
.name;
/*@ngInject*/
function ExtensionsTableWidget() {
return {
restrict: "E",
scope: true,
bindToController: {
ctx: '='
},
controller: ExtensionsTableWidgetController,
controllerAs: 'vm',
templateUrl: extensionsTableWidgetTemplate
};
}
/*@ngInject*/
function ExtensionsTableWidgetController($scope, $translate, utils) {
var vm = this;
vm.datasources = null;
vm.tabsHidden = false;
$scope.$watch('vm.ctx', function() {
if (vm.ctx && vm.ctx.defaultSubscription) {
vm.settings = vm.ctx.settings;
vm.subscription = vm.ctx.defaultSubscription;
vm.datasources = vm.subscription.datasources;
initializeConfig();
updateDatasources();
}
});
function initializeConfig() {
if (vm.settings.extensionsTitle && vm.settings.extensionsTitle.length) {
vm.extensionsTitle = utils.customTranslation(vm.settings.extensionsTitle, vm.settings.extensionsTitle);
} else {
vm.extensionsTitle = $translate.instant('extension.extensions');
}
vm.ctx.widgetTitle = vm.extensionsTitle;
vm.ctx.widgetActions = [vm.importExtensionsAction, vm.exportExtensionsAction, vm.addAction, vm.searchAction, vm.refreshAction];
}
function updateDatasources() {
var datasource = vm.datasources[0];
vm.selectedSource = vm.datasources[0];
vm.ctx.widgetTitle = utils.createLabelFromDatasource(datasource, vm.extensionsTitle);
}
vm.changeSelectedSource = function(source) {
vm.selectedSource = source;
};
vm.searchAction = {
name: "action.search",
show: true,
onAction: function() {
$scope.$broadcast("showSearch", vm.selectedSource);
},
icon: "search"
};
vm.refreshAction = {
name: "action.refresh",
show: true,
onAction: function() {
$scope.$broadcast("refreshExtensions", vm.selectedSource);
},
icon: "refresh"
};
vm.addAction = {
name: "action.add",
show: true,
onAction: function() {
$scope.$broadcast("addExtension", vm.selectedSource);
},
icon: "add"
};
vm.exportExtensionsAction = {
name: "extension.export-extensions-configuration",
show: true,
onAction: function() {
$scope.$broadcast("exportExtensions", vm.selectedSource);
},
icon: "file_download"
};
vm.importExtensionsAction = {
name: "extension.import-extensions-configuration",
show: true,
onAction: function() {
$scope.$broadcast("importExtensions", vm.selectedSource);
},
icon: "file_upload"
};
$scope.$on("filterMode", function($event, mode) {
vm.tabsHidden = mode;
});
$scope.$on("selectedExtensions", function($event, mode) {
vm.tabsHidden = mode;
});
}

29
ui/src/app/widget/lib/extensions-table-widget.scss

@ -0,0 +1,29 @@
/**
* 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.
*/
tb-extension-table {
md-content {
background-color: #fff;
}
}
md-tabs.hide-tabs-menu {
md-tabs-wrapper {
display: none;
}
md-tabs-content-wrapper {
top: 0 !important;
}
}

27
ui/src/app/widget/lib/extensions-table-widget.tpl.html

@ -0,0 +1,27 @@
<!--
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-tabs id="tabs" md-border-bottom flex class="tb-absolute-fill" ng-class="{'hide-tabs-menu': vm.datasources.length == 1 || vm.tabsHidden}">
<md-tab ng-repeat="source in vm.datasources" label="{{ source.name }}" md-on-select="vm.changeSelectedSource(source)">
<tb-extension-table flex
entity-id="source.entityId"
entity-type="{{source.entityType}}"
in-widget="true"
ctx="vm.ctx">
</tb-extension-table>
</md-tab>
</md-tabs>

2
ui/src/index.html

@ -16,7 +16,7 @@
-->
<!DOCTYPE html>
<html ng-app="thingsboard" ng-strict-di>
<html ng-app="thingsboard" ng-strict-di style="width: 100%;">
<head>
<title ng-bind="pageTitle"></title>
<base href="/" />

2
ui/src/scss/animations.scss

@ -39,6 +39,6 @@
@include keyframes(tbMoveToBottomFade) {
to {
opacity: 0;
@include transform(translate(0, 100%));
@include transform(translate(0, 150%));
}
}
Loading…
Cancel
Save