Commit 787f9f942ed92787b6d6b94bcdcd461bd2527bf0

Authored by Volodymyr Babak
2 parents fe2becb7 c2bc2871

Merge remote-tracking branch 'upstream/master'

Showing 118 changed files with 5762 additions and 232 deletions
... ... @@ -496,6 +496,16 @@
496 496 <artifactId>extension-mqtt</artifactId>
497 497 <classifier>extension</classifier>
498 498 </artifactItem>
  499 + <artifactItem>
  500 + <groupId>org.thingsboard.extensions</groupId>
  501 + <artifactId>extension-sqs</artifactId>
  502 + <classifier>extension</classifier>
  503 + </artifactItem>
  504 + <artifactItem>
  505 + <groupId>org.thingsboard.extensions</groupId>
  506 + <artifactId>extension-sns</artifactId>
  507 + <classifier>extension</classifier>
  508 + </artifactItem>
499 509 </artifactItems>
500 510 </configuration>
501 511 </execution>
... ...
... ... @@ -18,7 +18,7 @@
18 18 "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",
19 19 "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}",
20 20 "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}",
21   - "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}"
  21 + "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}"
22 22 }
23 23 }
24 24 ]
... ...
... ... @@ -34,7 +34,7 @@
34 34 "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",
35 35 "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}",
36 36 "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}",
37   - "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;\"}]}]}"
  37 + "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;\"}]}]}"
38 38 }
39 39 },
40 40 {
... ...
  1 +{
  2 + "widgetsBundle": {
  3 + "alias": "gateway_widgets",
  4 + "title": "Gateway widgets",
  5 + "image": null
  6 + },
  7 + "widgetTypes": [
  8 + {
  9 + "alias": "extension_configuration_widget",
  10 + "name": "Extensions table",
  11 + "descriptor": {
  12 + "type": "latest",
  13 + "sizeX": 9,
  14 + "sizeY": 6.5,
  15 + "resources": [],
  16 + "templateHtml": "<tb-extensions-table-widget \n ctx=\"ctx\">\n</tb-extensions-table-widget>",
  17 + "templateCss": "#container {\n overflow: auto;\n}",
  18 + "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",
  19 + "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}",
  20 + "dataKeySettingsSchema": "{}\n",
  21 + "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\":{}}"
  22 + }
  23 + }
  24 + ]
  25 +}
\ No newline at end of file
... ...
... ... @@ -170,7 +170,7 @@ class RuleActorMessageProcessor extends ComponentMsgProcessor<RuleId> {
170 170 Optional<RuleToPluginMsg<?>> ruleToPluginMsgOptional = action.convert(ruleCtx, inMsg, inMsgMd);
171 171 if (ruleToPluginMsgOptional.isPresent()) {
172 172 RuleToPluginMsg<?> ruleToPluginMsg = ruleToPluginMsgOptional.get();
173   - logger.debug("[{}] Device msg is converter to: {}", entityId, ruleToPluginMsg);
  173 + logger.debug("[{}] Device msg is converted to: {}", entityId, ruleToPluginMsg);
174 174 context.parent().tell(new RuleToPluginMsgWrapper(pluginTenantId, pluginId, tenantId, entityId, ruleToPluginMsg), context.self());
175 175 if (action.isOneWayAction()) {
176 176 pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_TWO_WAY_ACTIONS);
... ...
... ... @@ -28,6 +28,7 @@ import org.springframework.security.authentication.AuthenticationManager;
28 28 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
29 29 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
30 30 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  31 +import org.springframework.security.config.annotation.web.builders.WebSecurity;
31 32 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
32 33 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
33 34 import org.springframework.security.config.http.SessionCreationPolicy;
... ... @@ -148,6 +149,11 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
148 149 }
149 150
150 151 @Override
  152 + public void configure(WebSecurity web) throws Exception {
  153 + web.ignoring().antMatchers("/static/**");
  154 + }
  155 +
  156 + @Override
151 157 protected void configure(HttpSecurity http) throws Exception {
152 158 http.headers().cacheControl().and().frameOptions().disable()
153 159 .and()
... ...
... ... @@ -19,8 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode;
19 19 import com.fasterxml.jackson.databind.ObjectMapper;
20 20 import com.fasterxml.jackson.databind.node.ObjectNode;
21 21 import lombok.extern.slf4j.Slf4j;
22   -import org.slf4j.Logger;
23   -import org.slf4j.LoggerFactory;
24 22 import org.springframework.beans.factory.annotation.Autowired;
25 23 import org.springframework.http.HttpHeaders;
26 24 import org.springframework.http.HttpStatus;
... ... @@ -30,7 +28,6 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
30 28 import org.springframework.web.bind.annotation.*;
31 29 import org.thingsboard.server.common.data.User;
32 30 import org.thingsboard.server.common.data.security.UserCredentials;
33   -import org.thingsboard.server.dao.user.UserService;
34 31 import org.thingsboard.server.exception.ThingsboardErrorCode;
35 32 import org.thingsboard.server.exception.ThingsboardException;
36 33 import org.thingsboard.server.service.mail.MailService;
... ... @@ -78,9 +75,10 @@ public class AuthController extends BaseController {
78 75 @RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST)
79 76 @ResponseStatus(value = HttpStatus.OK)
80 77 public void changePassword (
81   - @RequestParam(value = "currentPassword") String currentPassword,
82   - @RequestParam(value = "newPassword") String newPassword) throws ThingsboardException {
  78 + @RequestBody JsonNode changePasswordRequest) throws ThingsboardException {
83 79 try {
  80 + String currentPassword = changePasswordRequest.get("currentPassword").asText();
  81 + String newPassword = changePasswordRequest.get("newPassword").asText();
84 82 SecurityUser securityUser = getCurrentUser();
85 83 UserCredentials userCredentials = userService.findUserCredentialsByUserId(securityUser.getId());
86 84 if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) {
... ... @@ -118,9 +116,10 @@ public class AuthController extends BaseController {
118 116 @RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST)
119 117 @ResponseStatus(value = HttpStatus.OK)
120 118 public void requestResetPasswordByEmail (
121   - @RequestParam(value = "email") String email,
  119 + @RequestBody JsonNode resetPasswordByEmailRequest,
122 120 HttpServletRequest request) throws ThingsboardException {
123 121 try {
  122 + String email = resetPasswordByEmailRequest.get("email").asText();
124 123 UserCredentials userCredentials = userService.requestPasswordReset(email);
125 124 String baseUrl = constructBaseUrl(request);
126 125 String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl,
... ... @@ -158,10 +157,11 @@ public class AuthController extends BaseController {
158 157 @ResponseStatus(value = HttpStatus.OK)
159 158 @ResponseBody
160 159 public JsonNode activateUser(
161   - @RequestParam(value = "activateToken") String activateToken,
162   - @RequestParam(value = "password") String password,
  160 + @RequestBody JsonNode activateRequest,
163 161 HttpServletRequest request) throws ThingsboardException {
164 162 try {
  163 + String activateToken = activateRequest.get("activateToken").asText();
  164 + String password = activateRequest.get("password").asText();
165 165 String encodedPassword = passwordEncoder.encode(password);
166 166 UserCredentials credentials = userService.activateUserCredentials(activateToken, encodedPassword);
167 167 User user = userService.findUserById(credentials.getUserId());
... ... @@ -194,10 +194,11 @@ public class AuthController extends BaseController {
194 194 @ResponseStatus(value = HttpStatus.OK)
195 195 @ResponseBody
196 196 public JsonNode resetPassword(
197   - @RequestParam(value = "resetToken") String resetToken,
198   - @RequestParam(value = "password") String password,
  197 + @RequestBody JsonNode resetPasswordRequest,
199 198 HttpServletRequest request) throws ThingsboardException {
200 199 try {
  200 + String resetToken = resetPasswordRequest.get("resetToken").asText();
  201 + String password = resetPasswordRequest.get("password").asText();
201 202 UserCredentials userCredentials = userService.findUserCredentialsByResetToken(resetToken);
202 203 if (userCredentials != null) {
203 204 String encodedPassword = passwordEncoder.encode(password);
... ...
... ... @@ -221,7 +221,10 @@ public abstract class AbstractControllerTest {
221 221 doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken)
222 222 .andExpect(status().isSeeOther())
223 223 .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
224   - JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", "activateToken", TestMailService.currentActivateToken, "password", password).andExpect(status().isOk()), JsonNode.class);
  224 + JsonNode activateRequest = new ObjectMapper().createObjectNode()
  225 + .put("activateToken", TestMailService.currentActivateToken)
  226 + .put("password", password);
  227 + JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class);
225 228 validateAndSetJwtToken(tokenInfo, user.getEmail());
226 229 return savedUser;
227 230 }
... ...
... ... @@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
17 17
18 18 import com.fasterxml.jackson.core.type.TypeReference;
19 19 import com.fasterxml.jackson.databind.JsonNode;
  20 +import com.fasterxml.jackson.databind.ObjectMapper;
20 21 import org.apache.commons.lang3.RandomStringUtils;
21 22 import org.junit.Assert;
22 23 import org.junit.Test;
... ... @@ -73,7 +74,11 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
73 74 .andExpect(status().isSeeOther())
74 75 .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
75 76
76   - JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", "activateToken", TestMailService.currentActivateToken, "password", "testPassword").andExpect(status().isOk()), JsonNode.class);
  77 + JsonNode activateRequest = new ObjectMapper().createObjectNode()
  78 + .put("activateToken", TestMailService.currentActivateToken)
  79 + .put("password", "testPassword");
  80 +
  81 + JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class);
77 82 validateAndSetJwtToken(tokenInfo, email);
78 83
79 84 doGet("/api/auth/user")
... ... @@ -117,13 +122,21 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
117 122
118 123 User savedUser = createUserAndLogin(user, "testPassword1");
119 124 logout();
120   - doPost("/api/noauth/resetPasswordByEmail", "email", email)
  125 +
  126 + JsonNode resetPasswordByEmailRequest = new ObjectMapper().createObjectNode()
  127 + .put("email", email);
  128 +
  129 + doPost("/api/noauth/resetPasswordByEmail", resetPasswordByEmailRequest)
121 130 .andExpect(status().isOk());
122 131 doGet("/api/noauth/resetPassword?resetToken={resetToken}", TestMailService.currentResetPasswordToken)
123 132 .andExpect(status().isSeeOther())
124 133 .andExpect(header().string(HttpHeaders.LOCATION, "/login/resetPassword?resetToken=" + TestMailService.currentResetPasswordToken));
125   -
126   - JsonNode tokenInfo = readResponse(doPost("/api/noauth/resetPassword", "resetToken", TestMailService.currentResetPasswordToken, "password", "testPassword2").andExpect(status().isOk()), JsonNode.class);
  134 +
  135 + JsonNode resetPasswordRequest = new ObjectMapper().createObjectNode()
  136 + .put("resetToken", TestMailService.currentResetPasswordToken)
  137 + .put("password", "testPassword2");
  138 +
  139 + JsonNode tokenInfo = readResponse(doPost("/api/noauth/resetPassword", resetPasswordRequest).andExpect(status().isOk()), JsonNode.class);
127 140 validateAndSetJwtToken(tokenInfo, email);
128 141
129 142 doGet("/api/auth/user")
... ...
... ... @@ -28,7 +28,7 @@
28 28 <packaging>jar</packaging>
29 29
30 30 <name>Thingsboard Server Common Data</name>
31   - <url>http://thingsboard.org</url>
  31 + <url>https://thingsboard.io</url>
32 32
33 33 <properties>
34 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
... ...
... ... @@ -28,7 +28,7 @@
28 28 <packaging>jar</packaging>
29 29
30 30 <name>Thingsboard Server Common Messages</name>
31   - <url>http://thingsboard.org</url>
  31 + <url>https://thingsboard.io</url>
32 32
33 33 <properties>
34 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
... ...
... ... @@ -28,7 +28,7 @@
28 28 <packaging>pom</packaging>
29 29
30 30 <name>Thingsboard Server Commons</name>
31   - <url>http://thingsboard.org</url>
  31 + <url>https://thingsboard.io</url>
32 32
33 33 <properties>
34 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
... ...
... ... @@ -28,7 +28,7 @@
28 28 <packaging>jar</packaging>
29 29
30 30 <name>Thingsboard Server Common Transport components</name>
31   - <url>http://thingsboard.org</url>
  31 + <url>https://thingsboard.io</url>
32 32
33 33 <properties>
34 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
... ...
... ... @@ -28,7 +28,7 @@
28 28 <packaging>jar</packaging>
29 29
30 30 <name>Thingsboard Server DAO Layer</name>
31   - <url>http://thingsboard.org</url>
  31 + <url>https://thingsboard.io</url>
32 32
33 33 <properties>
34 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
... ...
... ... @@ -384,7 +384,7 @@ public class BaseRelationService implements RelationService {
384 384 Set<EntityRelation> children = new HashSet<>(findRelations(rootId, direction).get());
385 385 Set<EntityId> childrenIds = new HashSet<>();
386 386 for (EntityRelation childRelation : children) {
387   - log.info("Found Relation: {}", childRelation);
  387 + log.trace("Found Relation: {}", childRelation);
388 388 EntityId childId;
389 389 if (direction == EntitySearchDirection.FROM) {
390 390 childId = childRelation.getTo();
... ... @@ -392,9 +392,9 @@ public class BaseRelationService implements RelationService {
392 392 childId = childRelation.getFrom();
393 393 }
394 394 if (uniqueMap.putIfAbsent(childId, Boolean.TRUE) == null) {
395   - log.info("Adding Relation: {}", childId);
  395 + log.trace("Adding Relation: {}", childId);
396 396 if (childrenIds.add(childId)) {
397   - log.info("Added Relation: {}", childId);
  397 + log.trace("Added Relation: {}", childId);
398 398 }
399 399 }
400 400 }
... ...
... ... @@ -22,6 +22,7 @@ import javax.annotation.PreDestroy;
22 22 import java.util.concurrent.Executors;
23 23
24 24 public abstract class JpaAbstractDaoListeningExecutorService {
  25 +
25 26 protected ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
26 27
27 28 @PreDestroy
... ...
... ... @@ -17,9 +17,7 @@ package org.thingsboard.server.dao.sql.timeseries;
17 17
18 18 import com.google.common.base.Function;
19 19 import com.google.common.collect.Lists;
20   -import com.google.common.util.concurrent.Futures;
21   -import com.google.common.util.concurrent.ListenableFuture;
22   -import com.google.common.util.concurrent.SettableFuture;
  20 +import com.google.common.util.concurrent.*;
23 21 import lombok.extern.slf4j.Slf4j;
24 22 import org.springframework.beans.factory.annotation.Autowired;
25 23 import org.springframework.data.domain.PageRequest;
... ... @@ -36,10 +34,12 @@ import org.thingsboard.server.dao.timeseries.TimeseriesDao;
36 34 import org.thingsboard.server.dao.util.SqlDao;
37 35
38 36 import javax.annotation.Nullable;
  37 +import javax.annotation.PreDestroy;
39 38 import java.util.ArrayList;
40 39 import java.util.List;
41 40 import java.util.Optional;
42 41 import java.util.concurrent.CompletableFuture;
  42 +import java.util.concurrent.Executors;
43 43 import java.util.stream.Collectors;
44 44
45 45 import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
... ... @@ -50,6 +50,8 @@ import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
50 50 @SqlDao
51 51 public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService implements TimeseriesDao {
52 52
  53 + private ListeningExecutorService insertService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
  54 +
53 55 @Autowired
54 56 private TsKvRepository tsKvRepository;
55 57
... ... @@ -232,7 +234,8 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
232 234 entity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null));
233 235 entity.setLongValue(tsKvEntry.getLongValue().orElse(null));
234 236 entity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null));
235   - return service.submit(() -> {
  237 + log.trace("Saving entity: " + entity);
  238 + return insertService.submit(() -> {
236 239 tsKvRepository.save(entity);
237 240 return null;
238 241 });
... ... @@ -240,7 +243,7 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
240 243
241 244 @Override
242 245 public ListenableFuture<Void> savePartition(EntityId entityId, long tsKvEntryTs, String key, long ttl) {
243   - return service.submit(() -> null);
  246 + return insertService.submit(() -> null);
244 247 }
245 248
246 249 @Override
... ... @@ -254,10 +257,15 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
254 257 latestEntity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null));
255 258 latestEntity.setLongValue(tsKvEntry.getLongValue().orElse(null));
256 259 latestEntity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null));
257   - return service.submit(() -> {
  260 + return insertService.submit(() -> {
258 261 tsKvLatestRepository.save(latestEntity);
259 262 return null;
260 263 });
261 264 }
262 265
  266 + @PreDestroy
  267 + void onDestroy() {
  268 + insertService.shutdown();
  269 + }
  270 +
263 271 }
... ...
... ... @@ -28,7 +28,7 @@
28 28 <packaging>jar</packaging>
29 29
30 30 <name>Thingsboard Server Extensions API</name>
31   - <url>http://thingsboard.org</url>
  31 + <url>https://thingsboard.io</url>
32 32
33 33 <properties>
34 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
... ...
... ... @@ -28,7 +28,7 @@
28 28 <packaging>jar</packaging>
29 29
30 30 <name>Thingsboard Server Core Extensions</name>
31   - <url>http://thingsboard.org</url>
  31 + <url>https://thingsboard.io</url>
32 32
33 33 <properties>
34 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
... ...
... ... @@ -128,12 +128,16 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
128 128 Optional<Long> interval = request.getLongParamValue("interval");
129 129 Optional<Integer> limit = request.getIntParamValue("limit");
130 130
  131 + // If some of these params are specified, they all must be
131 132 if (startTs.isPresent() || endTs.isPresent() || interval.isPresent() || limit.isPresent()) {
132   - if (!startTs.isPresent() || !endTs.isPresent() || !interval.isPresent()) {
  133 + if (!startTs.isPresent() || !endTs.isPresent() || !interval.isPresent() || interval.get() < 0) {
133 134 msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
134 135 return;
135 136 }
136   - Aggregation agg = Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name()));
  137 +
  138 + // If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted
  139 + Aggregation agg = (interval.isPresent() && interval.get() == 0) ? Aggregation.valueOf(Aggregation.NONE.name()) :
  140 + Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name()));
137 141
138 142 List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), interval.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg))
139 143 .collect(Collectors.toList());
... ...
... ... @@ -30,7 +30,7 @@
30 30 <packaging>jar</packaging>
31 31
32 32 <name>Thingsboard Server Kafka Extension</name>
33   - <url>http://thingsboard.org</url>
  33 + <url>https://thingsboard.io</url>
34 34
35 35 <properties>
36 36 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
... ...
... ... @@ -30,7 +30,7 @@
30 30 <packaging>jar</packaging>
31 31
32 32 <name>Thingsboard Server MQTT Extension</name>
33   - <url>http://thingsboard.org</url>
  33 + <url>https://thingsboard.io</url>
34 34
35 35 <properties>
36 36 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
... ...
... ... @@ -28,7 +28,7 @@
28 28 <packaging>jar</packaging>
29 29
30 30 <name>Thingsboard Server RabbitMQ Extension</name>
31   - <url>http://thingsboard.org</url>
  31 + <url>https://thingsboard.io</url>
32 32
33 33 <properties>
34 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
... ...
... ... @@ -30,7 +30,7 @@
30 30 <packaging>jar</packaging>
31 31
32 32 <name>Thingsboard Server REST API Call Extension</name>
33   - <url>http://thingsboard.org</url>
  33 + <url>https://thingsboard.io</url>
34 34
35 35 <properties>
36 36 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
... ...
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!--
  3 +
  4 + Copyright © 2016-2017 The Thingsboard Authors
  5 +
  6 + Licensed under the Apache License, Version 2.0 (the "License");
  7 + you may not use this file except in compliance with the License.
  8 + You may obtain a copy of the License at
  9 +
  10 + http://www.apache.org/licenses/LICENSE-2.0
  11 +
  12 + Unless required by applicable law or agreed to in writing, software
  13 + distributed under the License is distributed on an "AS IS" BASIS,
  14 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15 + See the License for the specific language governing permissions and
  16 + limitations under the License.
  17 +
  18 +-->
  19 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  20 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  21 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  22 + <parent>
  23 + <artifactId>extensions</artifactId>
  24 + <groupId>org.thingsboard</groupId>
  25 + <version>1.4.0-SNAPSHOT</version>
  26 + </parent>
  27 + <modelVersion>4.0.0</modelVersion>
  28 + <groupId>org.thingsboard.extensions</groupId>
  29 + <artifactId>extension-sns</artifactId>
  30 + <packaging>jar</packaging>
  31 +
  32 + <name>Thingsboard Server SNS Extension</name>
  33 + <url>https://thingsboard.io</url>
  34 +
  35 + <properties>
  36 + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  37 + <main.dir>${basedir}/../..</main.dir>
  38 + <aws.sdk.version>1.11.229</aws.sdk.version>
  39 + </properties>
  40 +
  41 + <dependencies>
  42 + <dependency>
  43 + <groupId>org.thingsboard</groupId>
  44 + <artifactId>extensions-api</artifactId>
  45 + <scope>provided</scope>
  46 + </dependency>
  47 + <dependency>
  48 + <groupId>org.thingsboard</groupId>
  49 + <artifactId>extensions-core</artifactId>
  50 + <scope>provided</scope>
  51 + </dependency>
  52 + <dependency>
  53 + <groupId>com.amazonaws</groupId>
  54 + <artifactId>aws-java-sdk-sns</artifactId>
  55 + <version>${aws.sdk.version}</version>
  56 + </dependency>
  57 + </dependencies>
  58 +
  59 + <build>
  60 + <plugins>
  61 + <plugin>
  62 + <artifactId>maven-assembly-plugin</artifactId>
  63 + <configuration>
  64 + <descriptors>
  65 + <descriptor>src/assembly/extension.xml</descriptor>
  66 + </descriptors>
  67 + </configuration>
  68 + <executions>
  69 + <execution>
  70 + <id>make-assembly</id>
  71 + <phase>package</phase>
  72 + <goals>
  73 + <goal>single</goal>
  74 + </goals>
  75 + </execution>
  76 + </executions>
  77 + </plugin>
  78 + </plugins>
  79 + </build>
  80 +
  81 +</project>
\ No newline at end of file
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
  19 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  20 + xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
  21 + <id>extension</id>
  22 + <formats>
  23 + <format>jar</format>
  24 + </formats>
  25 + <includeBaseDirectory>false</includeBaseDirectory>
  26 + <dependencySets>
  27 + <dependencySet>
  28 + <outputDirectory>/</outputDirectory>
  29 + <useProjectArtifact>true</useProjectArtifact>
  30 + <unpack>true</unpack>
  31 + <scope>runtime</scope>
  32 + <excludes>
  33 +
  34 + </excludes>
  35 + </dependencySet>
  36 + </dependencySets>
  37 +</assembly>
\ No newline at end of file
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.sns.action;
  17 +
  18 +import org.thingsboard.server.common.data.id.CustomerId;
  19 +import org.thingsboard.server.common.data.id.DeviceId;
  20 +import org.thingsboard.server.common.data.id.TenantId;
  21 +import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
  22 +
  23 +/**
  24 + * Created by Valerii Sosliuk on 11/15/2017.
  25 + */
  26 +public class SnsTopicActionMsg extends AbstractRuleToPluginMsg<SnsTopicActionPayload> {
  27 +
  28 + public SnsTopicActionMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, SnsTopicActionPayload payload) {
  29 + super(tenantId, customerId, deviceId, payload);
  30 + }
  31 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.sns.action;
  17 +
  18 +import lombok.Builder;
  19 +import lombok.Data;
  20 +import org.thingsboard.server.common.msg.session.MsgType;
  21 +
  22 +import java.io.Serializable;
  23 +
  24 +/**
  25 + * Created by Valerii Sosliuk on 11/15/2017.
  26 + */
  27 +@Data
  28 +@Builder
  29 +public class SnsTopicActionPayload implements Serializable {
  30 +
  31 + private final String topicArn;
  32 + private final String msgBody;
  33 +
  34 + private final Integer requestId;
  35 + private final MsgType msgType;
  36 + private final boolean sync;
  37 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.sns.action;
  17 +
  18 +import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
  19 +import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
  20 +import org.thingsboard.server.extensions.api.component.Action;
  21 +import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
  22 +import org.thingsboard.server.extensions.api.rules.RuleContext;
  23 +import org.thingsboard.server.extensions.core.action.template.AbstractTemplatePluginAction;
  24 +
  25 +import java.util.Optional;
  26 +
  27 +/**
  28 + * Created by Valerii Sosliuk on 11/15/2017.
  29 + */
  30 +@Action(name = "SNS Topic Action", descriptor = "SnsTopicActionDescriptor.json", configuration = SnsTopicPluginActionConfiguration.class)
  31 +public class SnsTopicPluginAction extends AbstractTemplatePluginAction<SnsTopicPluginActionConfiguration> {
  32 +
  33 + @Override
  34 + protected Optional<RuleToPluginMsg> buildRuleToPluginMsg(RuleContext ctx, ToDeviceActorMsg msg, FromDeviceRequestMsg payload) {
  35 + SnsTopicActionPayload.SnsTopicActionPayloadBuilder builder = SnsTopicActionPayload.builder();
  36 + builder.msgType(payload.getMsgType());
  37 + builder.requestId(payload.getRequestId());
  38 + builder.topicArn(configuration.getTopicArn());
  39 + builder.msgBody(getMsgBody(ctx, msg));
  40 + return Optional.of(new SnsTopicActionMsg(msg.getTenantId(),
  41 + msg.getCustomerId(),
  42 + msg.getDeviceId(),
  43 + builder.build()));
  44 + }
  45 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.sns.action;
  17 +
  18 +import lombok.Data;
  19 +import org.thingsboard.server.extensions.core.action.template.TemplateActionConfiguration;
  20 +
  21 +/**
  22 + * Created by Valerii Sosliuk on 11/15/2017.
  23 + */
  24 +@Data
  25 +public class SnsTopicPluginActionConfiguration implements TemplateActionConfiguration {
  26 +
  27 + private String topicArn;
  28 + private String template;
  29 + private boolean sync;
  30 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.sns.plugin;
  17 +
  18 +import com.amazonaws.services.sns.AmazonSNS;
  19 +import com.amazonaws.services.sns.model.PublishRequest;
  20 +import com.amazonaws.services.sns.model.PublishResult;
  21 +import com.amazonaws.services.sqs.AmazonSQS;
  22 +import com.amazonaws.services.sqs.model.SendMessageRequest;
  23 +import lombok.RequiredArgsConstructor;
  24 +import lombok.extern.slf4j.Slf4j;
  25 +import org.thingsboard.server.common.data.id.RuleId;
  26 +import org.thingsboard.server.common.data.id.TenantId;
  27 +import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
  28 +import org.thingsboard.server.extensions.api.plugins.PluginContext;
  29 +import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
  30 +import org.thingsboard.server.extensions.api.plugins.msg.ResponsePluginToRuleMsg;
  31 +import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
  32 +import org.thingsboard.server.extensions.api.rules.RuleException;
  33 +import org.thingsboard.server.extensions.sns.action.SnsTopicActionMsg;
  34 +import org.thingsboard.server.extensions.sns.action.SnsTopicActionPayload;
  35 +
  36 +/**
  37 + * Created by Valerii Sosliuk on 11/6/2017.
  38 + */
  39 +@RequiredArgsConstructor
  40 +@Slf4j
  41 +public class SnsMessageHandler implements RuleMsgHandler {
  42 +
  43 + private final AmazonSNS sns;
  44 +
  45 + @Override
  46 + public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
  47 + if (msg instanceof SnsTopicActionMsg) {
  48 + SnsTopicActionPayload payload = ((SnsTopicActionMsg) msg).getPayload();
  49 + PublishRequest publishRequest = new PublishRequest()
  50 + .withTopicArn(payload.getTopicArn())
  51 + .withMessage(payload.getMsgBody());
  52 + sns.publish(publishRequest);
  53 + if (payload.isSync()) {
  54 + ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId,
  55 + BasicStatusCodeResponse.onSuccess(payload.getMsgType(), payload.getRequestId())));
  56 + }
  57 + return;
  58 + }
  59 + throw new RuleException("Unsupported message type " + msg.getClass().getName() + "!");
  60 +
  61 + }
  62 +
  63 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.sns.plugin;
  17 +
  18 +import com.amazonaws.auth.AWSCredentials;
  19 +import com.amazonaws.auth.AWSStaticCredentialsProvider;
  20 +import com.amazonaws.auth.BasicAWSCredentials;
  21 +import com.amazonaws.services.sns.AmazonSNS;
  22 +import com.amazonaws.services.sns.AmazonSNSClient;
  23 +import org.thingsboard.server.extensions.api.component.Plugin;
  24 +import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
  25 +import org.thingsboard.server.extensions.api.plugins.PluginContext;
  26 +import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
  27 +import org.thingsboard.server.extensions.sns.action.SnsTopicPluginAction;
  28 +
  29 +/**
  30 + * Created by Valerii Sosliuk on 11/15/2017.
  31 + */
  32 +@Plugin(name = "SNS Plugin", actions = {SnsTopicPluginAction.class},
  33 + descriptor = "SnsPluginDescriptor.json", configuration = SnsPluginConfiguration.class)
  34 +public class SnsPlugin extends AbstractPlugin<SnsPluginConfiguration> {
  35 +
  36 + private SnsMessageHandler snsMessageHandler;
  37 + private SnsPluginConfiguration configuration;
  38 +
  39 + @Override
  40 + public void init(SnsPluginConfiguration configuration) {
  41 + this.configuration = configuration;
  42 + init();
  43 + }
  44 +
  45 + private void init() {
  46 + AWSCredentials awsCredentials = new BasicAWSCredentials(configuration.getAccessKeyId(), configuration.getSecretAccessKey());
  47 + AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials);
  48 + AmazonSNS sns = AmazonSNSClient.builder()
  49 + .withCredentials(credProvider)
  50 + .withRegion(configuration.getRegion())
  51 + .build();
  52 + this.snsMessageHandler = new SnsMessageHandler(sns);
  53 +
  54 + }
  55 +
  56 + private void destroy() {
  57 + this.snsMessageHandler = null;
  58 + }
  59 +
  60 + @Override
  61 + protected RuleMsgHandler getRuleMsgHandler() {
  62 + return snsMessageHandler;
  63 + }
  64 +
  65 + @Override
  66 + public void resume(PluginContext ctx) {
  67 + init();
  68 + }
  69 +
  70 + @Override
  71 + public void suspend(PluginContext ctx) {
  72 + destroy();
  73 + }
  74 +
  75 + @Override
  76 + public void stop(PluginContext ctx) {
  77 + destroy();
  78 + }
  79 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.sns.plugin;
  17 +
  18 +import lombok.Data;
  19 +
  20 +/**
  21 + * Created by Valerii Sosliuk on 11/5/2017.
  22 + */
  23 +@Data
  24 +public class SnsPluginConfiguration {
  25 +
  26 + private String accessKeyId;
  27 + private String secretAccessKey;
  28 + private String region;
  29 +
  30 +}
... ...
  1 +{
  2 + "schema": {
  3 + "title": "SNS Plugin Configuration",
  4 + "type": "object",
  5 + "properties": {
  6 + "accessKeyId": {
  7 + "title": "Access Key ID",
  8 + "type": "string"
  9 + },
  10 + "secretAccessKey": {
  11 + "title": "Secret Access Key",
  12 + "type": "string"
  13 + },
  14 + "region": {
  15 + "title": "Region",
  16 + "type": "string"
  17 + }
  18 + },
  19 + "required": [
  20 + "accessKeyId",
  21 + "secretAccessKey",
  22 + "region"
  23 + ]
  24 + },
  25 + "form": [
  26 + "accessKeyId",
  27 + "secretAccessKey",
  28 + "region"
  29 + ]
  30 +}
\ No newline at end of file
... ...
  1 +{
  2 + "schema": {
  3 + "title": "SNS Topic Action Configuration",
  4 + "type": "object",
  5 + "properties": {
  6 + "sync": {
  7 + "title": "Requires delivery confirmation",
  8 + "type": "boolean"
  9 + },
  10 + "topicArn": {
  11 + "title": "Topic ARN",
  12 + "type": "string"
  13 + },
  14 + "template": {
  15 + "title": "Body Template",
  16 + "type": "string"
  17 + }
  18 + },
  19 + "required": [
  20 + "sync",
  21 + "topicArn",
  22 + "template"
  23 + ]
  24 + },
  25 + "form": [
  26 + "sync",
  27 + "topicArn",
  28 + {
  29 + "key": "template",
  30 + "type": "textarea",
  31 + "rows": 5
  32 + }
  33 + ]
  34 +}
\ No newline at end of file
... ...
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!--
  3 +
  4 + Copyright © 2016-2017 The Thingsboard Authors
  5 +
  6 + Licensed under the Apache License, Version 2.0 (the "License");
  7 + you may not use this file except in compliance with the License.
  8 + You may obtain a copy of the License at
  9 +
  10 + http://www.apache.org/licenses/LICENSE-2.0
  11 +
  12 + Unless required by applicable law or agreed to in writing, software
  13 + distributed under the License is distributed on an "AS IS" BASIS,
  14 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15 + See the License for the specific language governing permissions and
  16 + limitations under the License.
  17 +
  18 +-->
  19 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  20 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  21 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  22 + <parent>
  23 + <artifactId>extensions</artifactId>
  24 + <groupId>org.thingsboard</groupId>
  25 + <version>1.4.0-SNAPSHOT</version>
  26 + </parent>
  27 + <modelVersion>4.0.0</modelVersion>
  28 + <groupId>org.thingsboard.extensions</groupId>
  29 + <artifactId>extension-sqs</artifactId>
  30 + <packaging>jar</packaging>
  31 +
  32 + <name>Thingsboard Server SQS Extension</name>
  33 + <url>https://thingsboard.io</url>
  34 +
  35 + <properties>
  36 + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  37 + <main.dir>${basedir}/../..</main.dir>
  38 + <aws.sdk.version>1.11.229</aws.sdk.version>
  39 + </properties>
  40 +
  41 + <dependencies>
  42 + <dependency>
  43 + <groupId>org.thingsboard</groupId>
  44 + <artifactId>extensions-api</artifactId>
  45 + <scope>provided</scope>
  46 + </dependency>
  47 + <dependency>
  48 + <groupId>org.thingsboard</groupId>
  49 + <artifactId>extensions-core</artifactId>
  50 + <scope>provided</scope>
  51 + </dependency>
  52 + <dependency>
  53 + <groupId>com.amazonaws</groupId>
  54 + <artifactId>aws-java-sdk-sqs</artifactId>
  55 + <version>${aws.sdk.version}</version>
  56 + </dependency>
  57 + </dependencies>
  58 +
  59 + <build>
  60 + <plugins>
  61 + <plugin>
  62 + <artifactId>maven-assembly-plugin</artifactId>
  63 + <configuration>
  64 + <descriptors>
  65 + <descriptor>src/assembly/extension.xml</descriptor>
  66 + </descriptors>
  67 + </configuration>
  68 + <executions>
  69 + <execution>
  70 + <id>make-assembly</id>
  71 + <phase>package</phase>
  72 + <goals>
  73 + <goal>single</goal>
  74 + </goals>
  75 + </execution>
  76 + </executions>
  77 + </plugin>
  78 + </plugins>
  79 + </build>
  80 +
  81 +</project>
\ No newline at end of file
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
  19 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  20 + xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
  21 + <id>extension</id>
  22 + <formats>
  23 + <format>jar</format>
  24 + </formats>
  25 + <includeBaseDirectory>false</includeBaseDirectory>
  26 + <dependencySets>
  27 + <dependencySet>
  28 + <outputDirectory>/</outputDirectory>
  29 + <useProjectArtifact>true</useProjectArtifact>
  30 + <unpack>true</unpack>
  31 + <scope>runtime</scope>
  32 + <excludes>
  33 +
  34 + </excludes>
  35 + </dependencySet>
  36 + </dependencySets>
  37 +</assembly>
\ No newline at end of file
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.sqs.action.fifo;
  17 +
  18 +import org.thingsboard.server.common.data.id.CustomerId;
  19 +import org.thingsboard.server.common.data.id.DeviceId;
  20 +import org.thingsboard.server.common.data.id.TenantId;
  21 +import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
  22 +
  23 +/**
  24 + * Created by Valerii Sosliuk on 11/10/2017.
  25 + */
  26 +public class SqsFifoQueueActionMsg extends AbstractRuleToPluginMsg<SqsFifoQueueActionPayload> {
  27 +
  28 + public SqsFifoQueueActionMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, SqsFifoQueueActionPayload payload) {
  29 + super(tenantId, customerId, deviceId, payload);
  30 + }
  31 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.sqs.action.fifo;
  17 +
  18 +import lombok.Builder;
  19 +import lombok.Data;
  20 +import org.thingsboard.server.common.msg.session.MsgType;
  21 +
  22 +import java.io.Serializable;
  23 +
  24 +/**
  25 + * Created by Valerii Sosliuk on 11/10/2017.
  26 + */
  27 +@Data
  28 +@Builder
  29 +public class SqsFifoQueueActionPayload implements Serializable {
  30 +
  31 + private final String queue;
  32 + private final String msgBody;
  33 + private final String deviceId;
  34 +
  35 + private final Integer requestId;
  36 + private final MsgType msgType;
  37 + private final boolean sync;
  38 +
  39 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.sqs.action.fifo;
  17 +
  18 +import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
  19 +import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
  20 +import org.thingsboard.server.extensions.api.component.Action;
  21 +import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
  22 +import org.thingsboard.server.extensions.api.rules.RuleContext;
  23 +import org.thingsboard.server.extensions.core.action.template.AbstractTemplatePluginAction;
  24 +import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionMsg;
  25 +import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionPayload;
  26 +import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueuePluginActionConfiguration;
  27 +
  28 +import java.util.Optional;
  29 +
  30 +/**
  31 + * Created by Valerii Sosliuk on 11/5/2017.
  32 + */
  33 +@Action(name = "SQS Fifo Queue Action", descriptor = "SqsFifoQueueActionDescriptor.json", configuration = SqsFifoQueuePluginActionConfiguration.class)
  34 +public class SqsFifoQueuePluginAction extends AbstractTemplatePluginAction<SqsFifoQueuePluginActionConfiguration> {
  35 +
  36 + @Override
  37 + protected Optional<RuleToPluginMsg> buildRuleToPluginMsg(RuleContext ctx, ToDeviceActorMsg msg, FromDeviceRequestMsg payload) {
  38 + SqsFifoQueueActionPayload.SqsFifoQueueActionPayloadBuilder builder = SqsFifoQueueActionPayload.builder();
  39 + builder.msgType(payload.getMsgType());
  40 + builder.requestId(payload.getRequestId());
  41 + builder.queue(configuration.getQueue());
  42 + builder.deviceId(msg.getDeviceId().toString());
  43 + builder.msgBody(getMsgBody(ctx, msg));
  44 + return Optional.of(new SqsFifoQueueActionMsg(msg.getTenantId(),
  45 + msg.getCustomerId(),
  46 + msg.getDeviceId(),
  47 + builder.build()));
  48 + }
  49 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.sqs.action.fifo;
  17 +
  18 +import lombok.Data;
  19 +import org.thingsboard.server.extensions.core.action.template.TemplateActionConfiguration;
  20 +
  21 +/**
  22 + * Created by Valerii Sosliuk on 11/10/2017.
  23 + */
  24 +@Data
  25 +public class SqsFifoQueuePluginActionConfiguration implements TemplateActionConfiguration {
  26 +
  27 + private String queue;
  28 + private String template;
  29 + private boolean sync;
  30 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.sqs.action.standard;
  17 +
  18 +import org.thingsboard.server.common.data.id.CustomerId;
  19 +import org.thingsboard.server.common.data.id.DeviceId;
  20 +import org.thingsboard.server.common.data.id.TenantId;
  21 +import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
  22 +
  23 +/**
  24 + * Created by Valerii Sosliuk on 11/6/2017.
  25 + */
  26 +public class SqsStandardQueueActionMsg extends AbstractRuleToPluginMsg<SqsStandardQueueActionPayload> {
  27 +
  28 + public SqsStandardQueueActionMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, SqsStandardQueueActionPayload payload) {
  29 + super(tenantId, customerId, deviceId, payload);
  30 + }
  31 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.sqs.action.standard;
  17 +
  18 +import lombok.Builder;
  19 +import lombok.Data;
  20 +import org.thingsboard.server.common.msg.session.MsgType;
  21 +
  22 +import java.io.Serializable;
  23 +
  24 +/**
  25 + * Created by Valerii Sosliuk on 11/6/2017.
  26 + */
  27 +@Data
  28 +@Builder
  29 +public class SqsStandardQueueActionPayload implements Serializable {
  30 +
  31 + private final String queue;
  32 + private final String msgBody;
  33 + private final int delaySeconds;
  34 +
  35 + private final Integer requestId;
  36 + private final MsgType msgType;
  37 + private final boolean sync;
  38 +
  39 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.sqs.action.standard;
  17 +
  18 +import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
  19 +import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
  20 +import org.thingsboard.server.extensions.api.component.Action;
  21 +import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
  22 +import org.thingsboard.server.extensions.api.rules.RuleContext;
  23 +import org.thingsboard.server.extensions.core.action.template.AbstractTemplatePluginAction;
  24 +
  25 +import java.util.Optional;
  26 +
  27 +/**
  28 + * Created by Valerii Sosliuk on 11/5/2017.
  29 + */
  30 +@Action(name = "SQS Standard Queue Action", descriptor = "SqsStandardQueueActionDescriptor.json", configuration = SqsStandardQueuePluginActionConfiguration.class)
  31 +public class SqsStandardQueuePluginAction extends AbstractTemplatePluginAction<SqsStandardQueuePluginActionConfiguration> {
  32 +
  33 + @Override
  34 + protected Optional<RuleToPluginMsg> buildRuleToPluginMsg(RuleContext ctx, ToDeviceActorMsg msg, FromDeviceRequestMsg payload) {
  35 + SqsStandardQueueActionPayload.SqsStandardQueueActionPayloadBuilder builder = SqsStandardQueueActionPayload.builder();
  36 + builder.msgType(payload.getMsgType());
  37 + builder.requestId(payload.getRequestId());
  38 + builder.queue(configuration.getQueue());
  39 + builder.delaySeconds(configuration.getDelaySeconds());
  40 + builder.msgBody(getMsgBody(ctx, msg));
  41 + return Optional.of(new SqsStandardQueueActionMsg(msg.getTenantId(),
  42 + msg.getCustomerId(),
  43 + msg.getDeviceId(),
  44 + builder.build()));
  45 + }
  46 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.sqs.action.standard;
  17 +
  18 +import lombok.Data;
  19 +import org.thingsboard.server.extensions.core.action.template.TemplateActionConfiguration;
  20 +
  21 +/**
  22 + * Created by Valerii Sosliuk on 11/6/2017.
  23 + */
  24 +@Data
  25 +public class SqsStandardQueuePluginActionConfiguration implements TemplateActionConfiguration {
  26 +
  27 + private String queue;
  28 + private int delaySeconds;
  29 + private boolean sync;
  30 + private String template;
  31 +
  32 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.sqs.plugin;
  17 +
  18 +import com.amazonaws.services.sqs.AmazonSQS;
  19 +import com.amazonaws.services.sqs.model.SendMessageRequest;
  20 +import com.amazonaws.services.sqs.model.SendMessageResult;
  21 +import lombok.RequiredArgsConstructor;
  22 +import lombok.extern.slf4j.Slf4j;
  23 +import org.thingsboard.server.common.data.id.RuleId;
  24 +import org.thingsboard.server.common.data.id.TenantId;
  25 +import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
  26 +import org.thingsboard.server.extensions.api.plugins.PluginContext;
  27 +import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
  28 +import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
  29 +import org.thingsboard.server.extensions.api.plugins.msg.ResponsePluginToRuleMsg;
  30 +import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
  31 +import org.thingsboard.server.extensions.api.rules.RuleException;
  32 +import org.thingsboard.server.extensions.sqs.action.fifo.SqsFifoQueueActionMsg;
  33 +import org.thingsboard.server.extensions.sqs.action.fifo.SqsFifoQueueActionPayload;
  34 +import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionMsg;
  35 +import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionPayload;
  36 +
  37 +/**
  38 + * Created by Valerii Sosliuk on 11/15/2017.
  39 + */
  40 +@RequiredArgsConstructor
  41 +@Slf4j
  42 +public class SqsMessageHandler implements RuleMsgHandler {
  43 +
  44 + private final AmazonSQS sqs;
  45 +
  46 + @Override
  47 + public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
  48 + if (msg instanceof SqsStandardQueueActionMsg) {
  49 + sendMessageToStandardQueue(ctx, tenantId, ruleId, msg);
  50 + return;
  51 + }
  52 + if (msg instanceof SqsFifoQueueActionMsg) {
  53 + sendMessageToFifoQueue(ctx, tenantId, ruleId, msg);
  54 + return;
  55 + }
  56 + throw new RuleException("Unsupported message type " + msg.getClass().getName() + "!");
  57 + }
  58 +
  59 + private void sendMessageToStandardQueue(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) {
  60 + SqsStandardQueueActionPayload payload = ((SqsStandardQueueActionMsg) msg).getPayload();
  61 + SendMessageRequest sendMsgRequest = new SendMessageRequest()
  62 + .withDelaySeconds(payload.getDelaySeconds())
  63 + .withQueueUrl(payload.getQueue())
  64 + .withMessageBody(payload.getMsgBody());
  65 + sqs.sendMessage(sendMsgRequest);
  66 + if (payload.isSync()) {
  67 + ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId,
  68 + BasicStatusCodeResponse.onSuccess(payload.getMsgType(), payload.getRequestId())));
  69 + }
  70 + }
  71 +
  72 + private void sendMessageToFifoQueue(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) {
  73 + SqsFifoQueueActionPayload payload = ((SqsFifoQueueActionMsg) msg).getPayload();
  74 + SendMessageRequest sendMsgRequest = new SendMessageRequest()
  75 + .withQueueUrl(payload.getQueue())
  76 + .withMessageBody(payload.getMsgBody())
  77 + .withMessageGroupId(payload.getDeviceId());
  78 + sqs.sendMessage(sendMsgRequest);
  79 + if (payload.isSync()) {
  80 + ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId,
  81 + BasicStatusCodeResponse.onSuccess(payload.getMsgType(), payload.getRequestId())));
  82 + }
  83 + }
  84 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.sqs.plugin;
  17 +
  18 +import com.amazonaws.auth.AWSCredentials;
  19 +import com.amazonaws.auth.AWSStaticCredentialsProvider;
  20 +import com.amazonaws.auth.BasicAWSCredentials;
  21 +import com.amazonaws.regions.Regions;
  22 +import com.amazonaws.services.sqs.AmazonSQS;
  23 +import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
  24 +import org.thingsboard.server.extensions.api.component.Plugin;
  25 +import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
  26 +import org.thingsboard.server.extensions.api.plugins.PluginContext;
  27 +import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
  28 +import org.thingsboard.server.extensions.sqs.action.fifo.SqsFifoQueuePluginAction;
  29 +import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueuePluginAction;
  30 +
  31 +/**
  32 + * Created by Valerii Sosliuk on 11/6/2017.
  33 + */
  34 +@Plugin(name = "SQS Plugin", actions = {SqsStandardQueuePluginAction.class, SqsFifoQueuePluginAction.class},
  35 + descriptor = "SqsPluginDescriptor.json", configuration = SqsPluginConfiguration.class)
  36 +public class SqsPlugin extends AbstractPlugin<SqsPluginConfiguration> {
  37 +
  38 + private SqsMessageHandler sqsMessageHandler;
  39 + private SqsPluginConfiguration configuration;
  40 +
  41 + @Override
  42 + public void init(SqsPluginConfiguration configuration) {
  43 + this.configuration = configuration;
  44 + init();
  45 + }
  46 +
  47 + private void init() {
  48 + AWSCredentials awsCredentials = new BasicAWSCredentials(configuration.getAccessKeyId(), configuration.getSecretAccessKey());
  49 + AmazonSQS sqs = AmazonSQSClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
  50 + .withRegion(Regions.fromName(configuration.getRegion())).build();
  51 + this.sqsMessageHandler = new SqsMessageHandler(sqs);
  52 +
  53 + }
  54 +
  55 + private void destroy() {
  56 + this.sqsMessageHandler = null;
  57 + }
  58 +
  59 + @Override
  60 + protected RuleMsgHandler getRuleMsgHandler() {
  61 + return sqsMessageHandler;
  62 + }
  63 +
  64 + @Override
  65 + public void resume(PluginContext ctx) {
  66 + init();
  67 + }
  68 +
  69 + @Override
  70 + public void suspend(PluginContext ctx) {
  71 + destroy();
  72 + }
  73 +
  74 + @Override
  75 + public void stop(PluginContext ctx) {
  76 + destroy();
  77 + }
  78 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.sqs.plugin;
  17 +
  18 +import lombok.Data;
  19 +
  20 +/**
  21 + * Created by Valerii Sosliuk on 11/5/2017.
  22 + */
  23 +@Data
  24 +public class SqsPluginConfiguration {
  25 +
  26 + private String accessKeyId;
  27 + private String secretAccessKey;
  28 + private String region;
  29 +
  30 +}
... ...
  1 +{
  2 + "schema": {
  3 + "title": "SQS FIFO Queue Action Configuration",
  4 + "type": "object",
  5 + "properties": {
  6 + "sync": {
  7 + "title": "Requires delivery confirmation",
  8 + "type": "boolean"
  9 + },
  10 + "queue": {
  11 + "title": "Queue URL",
  12 + "type": "string"
  13 + },
  14 + "template": {
  15 + "title": "Body Template",
  16 + "type": "string"
  17 + }
  18 + },
  19 + "required": [
  20 + "sync",
  21 + "queue",
  22 + "template"
  23 + ]
  24 + },
  25 + "form": [
  26 + "sync",
  27 + "queue",
  28 + {
  29 + "key": "template",
  30 + "type": "textarea",
  31 + "rows": 5
  32 + }
  33 + ]
  34 +}
\ No newline at end of file
... ...
  1 +{
  2 + "schema": {
  3 + "title": "SQS Plugin Configuration",
  4 + "type": "object",
  5 + "properties": {
  6 + "accessKeyId": {
  7 + "title": "Access Key ID",
  8 + "type": "string"
  9 + },
  10 + "secretAccessKey": {
  11 + "title": "Secret Access Key",
  12 + "type": "string"
  13 + },
  14 + "region": {
  15 + "title": "Region",
  16 + "type": "string"
  17 + }
  18 + },
  19 + "required": [
  20 + "accessKeyId",
  21 + "secretAccessKey",
  22 + "region"
  23 + ]
  24 + },
  25 + "form": [
  26 + "accessKeyId",
  27 + "secretAccessKey",
  28 + "region"
  29 + ]
  30 +}
\ No newline at end of file
... ...
  1 +{
  2 + "schema": {
  3 + "title": "SQS Standard Queue Action Configuration",
  4 + "type": "object",
  5 + "properties": {
  6 + "sync": {
  7 + "title": "Requires delivery confirmation",
  8 + "type": "boolean"
  9 + },
  10 + "queue": {
  11 + "title": "Queue URL",
  12 + "type": "string"
  13 + },
  14 + "delaySeconds": {
  15 + "title": "Delay Seconds",
  16 + "type": "integer",
  17 + "default": 0
  18 + },
  19 + "template": {
  20 + "title": "Body Template",
  21 + "type": "string"
  22 + }
  23 + },
  24 + "required": [
  25 + "sync",
  26 + "queue",
  27 + "delaySeconds",
  28 + "template"
  29 + ]
  30 + },
  31 + "form": [
  32 + "sync",
  33 + "queue",
  34 + "delaySeconds",
  35 + {
  36 + "key": "template",
  37 + "type": "textarea",
  38 + "rows": 5
  39 + }
  40 + ]
  41 +}
\ No newline at end of file
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.sqs;
  17 +
  18 +import com.amazonaws.auth.AWSCredentials;
  19 +import com.amazonaws.auth.AWSStaticCredentialsProvider;
  20 +import com.amazonaws.auth.BasicAWSCredentials;
  21 +import com.amazonaws.regions.Regions;
  22 +import com.amazonaws.services.sqs.AmazonSQS;
  23 +import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
  24 +import com.amazonaws.services.sqs.model.DeleteMessageRequest;
  25 +import com.amazonaws.services.sqs.model.Message;
  26 +import lombok.extern.slf4j.Slf4j;
  27 +
  28 +import java.util.List;
  29 +
  30 +/**
  31 + * Created by Valerii Sosliuk on 11/10/2017.
  32 + */
  33 +@Slf4j
  34 +public class SqsDemoClient {
  35 +
  36 + private static final String ACCESS_KEY_ID = "$ACCES_KEY_ID";
  37 + private static final String SECRET_ACCESS_KEY = "$SECRET_ACCESS_KEY";
  38 +
  39 + private static final String QUEUE_URL = "$QUEUE_URL";
  40 + private static final String REGION = "us-east-1";
  41 +
  42 + public static void main(String[] args) {
  43 + log.info("Starting SQS Demo Clinent...");
  44 + AWSCredentials awsCredentials = new BasicAWSCredentials(ACCESS_KEY_ID, SECRET_ACCESS_KEY);
  45 + AmazonSQS sqs = AmazonSQSClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
  46 + .withRegion(Regions.fromName(REGION)).build();
  47 + SqsDemoClient client = new SqsDemoClient();
  48 + client.pollMessages(sqs);
  49 + }
  50 +
  51 + private void pollMessages(AmazonSQS sqs) {
  52 + log.info("Polling messages");
  53 + while (true) {
  54 + List<Message> messages = sqs.receiveMessage(QUEUE_URL).getMessages();
  55 + messages.forEach(m -> {
  56 + log.info("Message Received: " + m.getBody());
  57 + System.out.println(m.getBody());
  58 + DeleteMessageRequest deleteMessageRequest = new DeleteMessageRequest(QUEUE_URL, m.getReceiptHandle());
  59 + sqs.deleteMessage(deleteMessageRequest);
  60 + });
  61 + try {
  62 + Thread.sleep(1000);
  63 + } catch (InterruptedException e) {
  64 + Thread.currentThread().interrupt();
  65 + e.printStackTrace();
  66 + }
  67 + }
  68 + }
  69 +}
... ...
  1 +<configuration>
  2 + <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
  3 + <encoder>
  4 + <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
  5 + </encoder>
  6 + </appender>
  7 + <root level="INFO">
  8 + <appender-ref ref="STDOUT"/>
  9 + </root>
  10 +</configuration>
\ No newline at end of file
... ...
... ... @@ -28,7 +28,7 @@
28 28 <packaging>pom</packaging>
29 29
30 30 <name>Thingsboard Extensions</name>
31   - <url>http://thingsboard.org</url>
  31 + <url>https://thingsboard.io</url>
32 32
33 33 <properties>
34 34 <main.dir>${basedir}/..</main.dir>
... ... @@ -39,6 +39,8 @@
39 39 <module>extension-rest-api-call</module>
40 40 <module>extension-kafka</module>
41 41 <module>extension-mqtt</module>
  42 + <module>extension-sqs</module>
  43 + <module>extension-sns</module>
42 44 </modules>
43 45
44 46 </project>
... ...
... ... @@ -351,6 +351,18 @@
351 351 <version>${project.version}</version>
352 352 </dependency>
353 353 <dependency>
  354 + <groupId>org.thingsboard.extensions</groupId>
  355 + <artifactId>extension-sqs</artifactId>
  356 + <classifier>extension</classifier>
  357 + <version>${project.version}</version>
  358 + </dependency>
  359 + <dependency>
  360 + <groupId>org.thingsboard.extensions</groupId>
  361 + <artifactId>extension-sns</artifactId>
  362 + <classifier>extension</classifier>
  363 + <version>${project.version}</version>
  364 + </dependency>
  365 + <dependency>
354 366 <groupId>org.thingsboard.common</groupId>
355 367 <artifactId>data</artifactId>
356 368 <version>${project.version}</version>
... ...
  1 +@REM
  2 +@REM Copyright © 2016-2017 The Thingsboard Authors
  3 +@REM
  4 +@REM Licensed under the Apache License, Version 2.0 (the "License");
  5 +@REM you may not use this file except in compliance with the License.
  6 +@REM You may obtain a copy of the License at
  7 +@REM
  8 +@REM http://www.apache.org/licenses/LICENSE-2.0
  9 +@REM
  10 +@REM Unless required by applicable law or agreed to in writing, software
  11 +@REM distributed under the License is distributed on an "AS IS" BASIS,
  12 +@REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +@REM See the License for the specific language governing permissions and
  14 +@REM limitations under the License.
  15 +@REM
  16 +
  17 +mvn clean install -rf :application
  18 +
... ...
... ... @@ -28,7 +28,7 @@
28 28 <packaging>jar</packaging>
29 29
30 30 <name>Thingsboard Server Tools</name>
31   - <url>http://thingsboard.org</url>
  31 + <url>https://thingsboard.io</url>
32 32
33 33 <properties>
34 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
... ...
... ... @@ -26,9 +26,16 @@ import org.springframework.http.client.ClientHttpResponse;
26 26 import org.springframework.http.client.support.HttpRequestWrapper;
27 27 import org.springframework.web.client.HttpClientErrorException;
28 28 import org.springframework.web.client.RestTemplate;
  29 +import org.thingsboard.server.common.data.Customer;
29 30 import org.thingsboard.server.common.data.Device;
  31 +import org.thingsboard.server.common.data.alarm.Alarm;
  32 +import org.thingsboard.server.common.data.alarm.AlarmSeverity;
  33 +import org.thingsboard.server.common.data.alarm.AlarmStatus;
  34 +import org.thingsboard.server.common.data.asset.Asset;
  35 +import org.thingsboard.server.common.data.id.AssetId;
30 36 import org.thingsboard.server.common.data.id.CustomerId;
31 37 import org.thingsboard.server.common.data.id.DeviceId;
  38 +import org.thingsboard.server.common.data.id.EntityId;
32 39 import org.thingsboard.server.common.data.security.DeviceCredentials;
33 40
34 41 import java.io.IOException;
... ... @@ -71,18 +78,40 @@ public class RestClient implements ClientHttpRequestInterceptor {
71 78 }
72 79 }
73 80
74   - public Device createDevice(String name) {
  81 + public Customer createCustomer(String title) {
  82 + Customer customer = new Customer();
  83 + customer.setTitle(title);
  84 + return restTemplate.postForEntity(baseURL + "/api/customer", customer, Customer.class).getBody();
  85 + }
  86 +
  87 + public Device createDevice(String name, String type) {
75 88 Device device = new Device();
76 89 device.setName(name);
  90 + device.setType(type);
77 91 return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody();
78 92 }
79 93
  94 + public Asset createAsset(String name, String type) {
  95 + Asset asset = new Asset();
  96 + asset.setName(name);
  97 + asset.setType(type);
  98 + return restTemplate.postForEntity(baseURL + "/api/asset", asset, Asset.class).getBody();
  99 + }
  100 +
  101 + public Alarm createAlarm(Alarm alarm) {
  102 + return restTemplate.postForEntity(baseURL + "/api/alarm", alarm, Alarm.class).getBody();
  103 + }
80 104
81 105 public Device assignDevice(CustomerId customerId, DeviceId deviceId) {
82 106 return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/device/{deviceId}", null, Device.class,
83 107 customerId.toString(), deviceId.toString()).getBody();
84 108 }
85 109
  110 + public Asset assignAsset(CustomerId customerId, AssetId assetId) {
  111 + return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/asset/{assetId}", null, Asset.class,
  112 + customerId.toString(), assetId.toString()).getBody();
  113 + }
  114 +
86 115 public DeviceCredentials getCredentials(DeviceId id) {
87 116 return restTemplate.getForEntity(baseURL + "/api/device/" + id.getId().toString() + "/credentials", DeviceCredentials.class).getBody();
88 117 }
... ... @@ -91,11 +120,14 @@ public class RestClient implements ClientHttpRequestInterceptor {
91 120 return restTemplate;
92 121 }
93 122
  123 + public String getToken() {
  124 + return token;
  125 + }
  126 +
94 127 @Override
95 128 public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException {
96 129 HttpRequest wrapper = new HttpRequestWrapper(request);
97 130 wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token);
98 131 return execution.execute(wrapper, bytes);
99 132 }
100   -
101   -}
  133 +}
\ No newline at end of file
... ...
  1 +# -*- coding: utf-8 -*-
  2 +#
  3 +# Copyright © 2016-2017 The Thingsboard Authors
  4 +#
  5 +# Licensed under the Apache License, Version 2.0 (the "License");
  6 +# you may not use this file except in compliance with the License.
  7 +# You may obtain a copy of the License at
  8 +#
  9 +# http://www.apache.org/licenses/LICENSE-2.0
  10 +#
  11 +# Unless required by applicable law or agreed to in writing, software
  12 +# distributed under the License is distributed on an "AS IS" BASIS,
  13 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 +# See the License for the specific language governing permissions and
  15 +# limitations under the License.
  16 +#
  17 +
  18 +import paho.mqtt.client as mqtt
  19 +from time import sleep
  20 +import random
  21 +
  22 +broker="test.mosquitto.org"
  23 +topic_pub='v1/devices/me/telemetry'
  24 +
  25 +
  26 +client = mqtt.Client()
  27 +
  28 +client.username_pw_set("TEST_TOKEN")
  29 +client.connect('127.0.0.1', 1883, 1)
  30 +
  31 +for i in range(5):
  32 + x = random.randrange(20, 100)
  33 + print x
  34 + msg = '{"windSpeed":"'+ str(x) + '"}'
  35 + client.publish(topic_pub, msg)
  36 + sleep(0.1)
... ...
... ... @@ -28,7 +28,7 @@
28 28 <packaging>jar</packaging>
29 29
30 30 <name>Thingsboard COAP Transport</name>
31   - <url>http://thingsboard.org</url>
  31 + <url>https://thingsboard.io</url>
32 32
33 33 <properties>
34 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
... ...
... ... @@ -28,7 +28,7 @@
28 28 <packaging>jar</packaging>
29 29
30 30 <name>Thingsboard HTTP Transport</name>
31   - <url>http://thingsboard.org</url>
  31 + <url>https://thingsboard.io</url>
32 32
33 33 <properties>
34 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
... ...
... ... @@ -28,7 +28,7 @@
28 28 <packaging>jar</packaging>
29 29
30 30 <name>Thingsboard MQTT Transport</name>
31   - <url>http://thingsboard.org</url>
  31 + <url>https://thingsboard.io</url>
32 32
33 33 <properties>
34 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
... ...
... ... @@ -229,6 +229,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
229 229 } else if (topicName.equals(DEVICE_ATTRIBUTES_RESPONSES_TOPIC)) {
230 230 deviceSessionCtx.setAllowAttributeResponses();
231 231 grantedQoSList.add(getMinSupportedQos(reqQoS));
  232 + } else if (topicName.equals(GATEWAY_ATTRIBUTES_TOPIC)) {
  233 + grantedQoSList.add(getMinSupportedQos(reqQoS));
232 234 } else {
233 235 log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topicName, reqQoS);
234 236 grantedQoSList.add(FAILURE.value());
... ...
... ... @@ -16,6 +16,7 @@
16 16 package org.thingsboard.server.transport.mqtt.session;
17 17
18 18 import com.google.gson.Gson;
  19 +import com.google.gson.JsonArray;
19 20 import com.google.gson.JsonElement;
20 21 import com.google.gson.JsonObject;
21 22 import io.netty.buffer.ByteBuf;
... ... @@ -24,6 +25,7 @@ import io.netty.buffer.UnpooledByteBufAllocator;
24 25 import io.netty.handler.codec.mqtt.*;
25 26 import org.thingsboard.server.common.data.Device;
26 27 import org.thingsboard.server.common.data.id.SessionId;
  28 +import org.thingsboard.server.common.data.kv.AttributeKvEntry;
27 29 import org.thingsboard.server.common.data.kv.KvEntry;
28 30 import org.thingsboard.server.common.msg.core.*;
29 31 import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
... ... @@ -35,6 +37,7 @@ import org.thingsboard.server.transport.mqtt.MqttTopics;
35 37 import org.thingsboard.server.transport.mqtt.MqttTransportHandler;
36 38
37 39 import java.nio.charset.Charset;
  40 +import java.util.List;
38 41 import java.util.Optional;
39 42 import java.util.concurrent.atomic.AtomicInteger;
40 43
... ... @@ -83,7 +86,7 @@ public class GatewayDeviceSessionCtx extends DeviceAwareSessionContext {
83 86 if (responseMsg.isSuccess()) {
84 87 MsgType requestMsgType = responseMsg.getRequestMsgType();
85 88 Integer requestId = responseMsg.getRequestId();
86   - if (requestMsgType == MsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == MsgType.POST_TELEMETRY_REQUEST) {
  89 + if (requestId >= 0 && requestMsgType == MsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == MsgType.POST_TELEMETRY_REQUEST) {
87 90 return Optional.of(MqttTransportHandler.createMqttPubAckMsg(requestId));
88 91 }
89 92 }
... ... @@ -135,40 +138,43 @@ public class GatewayDeviceSessionCtx extends DeviceAwareSessionContext {
135 138 if (responseData.isPresent()) {
136 139 AttributesKVMsg msg = responseData.get();
137 140 if (msg.getClientAttributes() != null) {
138   - msg.getClientAttributes().forEach(v -> addValueToJson(result, "value", v));
  141 + addValues(result, msg.getClientAttributes());
139 142 }
140 143 if (msg.getSharedAttributes() != null) {
141   - msg.getSharedAttributes().forEach(v -> addValueToJson(result, "value", v));
  144 + addValues(result, msg.getSharedAttributes());
142 145 }
143 146 }
144 147 return createMqttPublishMsg(topic, result);
145 148 }
146 149
  150 + private void addValues(JsonObject result, List<AttributeKvEntry> kvList) {
  151 + if (kvList.size() == 1) {
  152 + addValueToJson(result, "value", kvList.get(0));
  153 + } else {
  154 + JsonObject values;
  155 + if (result.has("values")) {
  156 + values = result.get("values").getAsJsonObject();
  157 + } else {
  158 + values = new JsonObject();
  159 + result.add("values", values);
  160 + }
  161 + kvList.forEach(value -> addValueToJson(values, value.getKey(), value));
  162 + }
  163 + }
  164 +
147 165 private void addValueToJson(JsonObject json, String name, KvEntry entry) {
148 166 switch (entry.getDataType()) {
149 167 case BOOLEAN:
150   - Optional<Boolean> booleanValue = entry.getBooleanValue();
151   - if (booleanValue.isPresent()) {
152   - json.addProperty(name, booleanValue.get());
153   - }
  168 + entry.getBooleanValue().ifPresent(aBoolean -> json.addProperty(name, aBoolean));
154 169 break;
155 170 case STRING:
156   - Optional<String> stringValue = entry.getStrValue();
157   - if (stringValue.isPresent()) {
158   - json.addProperty(name, stringValue.get());
159   - }
  171 + entry.getStrValue().ifPresent(aString -> json.addProperty(name, aString));
160 172 break;
161 173 case DOUBLE:
162   - Optional<Double> doubleValue = entry.getDoubleValue();
163   - if (doubleValue.isPresent()) {
164   - json.addProperty(name, doubleValue.get());
165   - }
  174 + entry.getDoubleValue().ifPresent(aDouble -> json.addProperty(name, aDouble));
166 175 break;
167 176 case LONG:
168   - Optional<Long> longValue = entry.getLongValue();
169   - if (longValue.isPresent()) {
170   - json.addProperty(name, longValue.get());
171   - }
  177 + entry.getLongValue().ifPresent(aLong -> json.addProperty(name, aLong));
172 178 break;
173 179 }
174 180 }
... ...
... ... @@ -41,10 +41,7 @@ import org.thingsboard.server.dao.relation.RelationService;
41 41 import org.thingsboard.server.transport.mqtt.MqttTransportHandler;
42 42 import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor;
43 43
44   -import java.util.Collections;
45   -import java.util.HashMap;
46   -import java.util.Map;
47   -import java.util.Optional;
  44 +import java.util.*;
48 45 import java.util.stream.Collectors;
49 46
50 47 import static org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor.validateJsonPayload;
... ... @@ -186,24 +183,34 @@ public class GatewaySessionCtx {
186 183 }
187 184 }
188 185
189   - public void onDeviceAttributesRequest(MqttPublishMessage mqttMsg) throws AdaptorException {
190   - JsonElement json = validateJsonPayload(gatewaySessionId, mqttMsg.payload());
  186 + public void onDeviceAttributesRequest(MqttPublishMessage msg) throws AdaptorException {
  187 + JsonElement json = validateJsonPayload(gatewaySessionId, msg.payload());
191 188 if (json.isJsonObject()) {
192 189 JsonObject jsonObj = json.getAsJsonObject();
193 190 int requestId = jsonObj.get("id").getAsInt();
194 191 String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString();
195 192 boolean clientScope = jsonObj.get("client").getAsBoolean();
196   - String key = jsonObj.get("key").getAsString();
  193 + Set<String> keys;
  194 + if (jsonObj.has("key")) {
  195 + keys = Collections.singleton(jsonObj.get("key").getAsString());
  196 + } else {
  197 + JsonArray keysArray = jsonObj.get("keys").getAsJsonArray();
  198 + keys = new HashSet<>();
  199 + for (JsonElement keyObj : keysArray) {
  200 + keys.add(keyObj.getAsString());
  201 + }
  202 + }
197 203
198 204 BasicGetAttributesRequest request;
199 205 if (clientScope) {
200   - request = new BasicGetAttributesRequest(requestId, Collections.singleton(key), null);
  206 + request = new BasicGetAttributesRequest(requestId, keys, null);
201 207 } else {
202   - request = new BasicGetAttributesRequest(requestId, null, Collections.singleton(key));
  208 + request = new BasicGetAttributesRequest(requestId, null, keys);
203 209 }
204 210 GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
205 211 processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
206 212 new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
  213 + ack(msg);
207 214 } else {
208 215 throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
209 216 }
... ... @@ -251,7 +258,7 @@ public class GatewaySessionCtx {
251 258 }
252 259
253 260 private void ack(MqttPublishMessage msg) {
254   - if(msg.variableHeader().messageId() > 0) {
  261 + if (msg.variableHeader().messageId() > 0) {
255 262 writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(msg.variableHeader().messageId()));
256 263 }
257 264 }
... ...
... ... @@ -28,7 +28,7 @@
28 28 <packaging>pom</packaging>
29 29
30 30 <name>Thingsboard Server Transport Modules</name>
31   - <url>http://thingsboard.org</url>
  31 + <url>https://thingsboard.io</url>
32 32
33 33 <properties>
34 34 <main.dir>${basedir}/..</main.dir>
... ...
... ... @@ -30,6 +30,7 @@
30 30 "angular-material": "1.1.1",
31 31 "angular-material-data-table": "^0.10.9",
32 32 "angular-material-icons": "^0.7.1",
  33 + "angular-material-expansion-panel": "^0.7.2",
33 34 "angular-messages": "1.5.8",
34 35 "angular-route": "1.5.8",
35 36 "angular-sanitize": "1.5.8",
... ...
... ... @@ -28,7 +28,7 @@
28 28 <packaging>jar</packaging>
29 29
30 30 <name>Thingsboard Server UI</name>
31   - <url>http://thingsboard.org</url>
  31 + <url>https://thingsboard.io</url>
32 32
33 33 <properties>
34 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
... ...
... ... @@ -65,8 +65,8 @@ function LoginService($http, $q) {
65 65
66 66 function sendResetPasswordLink(email) {
67 67 var deferred = $q.defer();
68   - var url = '/api/noauth/resetPasswordByEmail?email=' + email;
69   - $http.post(url, null).then(function success(response) {
  68 + var url = '/api/noauth/resetPasswordByEmail';
  69 + $http.post(url, {email: email}).then(function success(response) {
70 70 deferred.resolve(response);
71 71 }, function fail() {
72 72 deferred.reject();
... ... @@ -76,8 +76,8 @@ function LoginService($http, $q) {
76 76
77 77 function resetPassword(resetToken, password) {
78 78 var deferred = $q.defer();
79   - var url = '/api/noauth/resetPassword?resetToken=' + resetToken + '&password=' + password;
80   - $http.post(url, null).then(function success(response) {
  79 + var url = '/api/noauth/resetPassword';
  80 + $http.post(url, {resetToken: resetToken, password: password}).then(function success(response) {
81 81 deferred.resolve(response);
82 82 }, function fail() {
83 83 deferred.reject();
... ... @@ -87,8 +87,8 @@ function LoginService($http, $q) {
87 87
88 88 function activate(activateToken, password) {
89 89 var deferred = $q.defer();
90   - var url = '/api/noauth/activate?activateToken=' + activateToken + '&password=' + password;
91   - $http.post(url, null).then(function success(response) {
  90 + var url = '/api/noauth/activate';
  91 + $http.post(url, {activateToken: activateToken, password: password}).then(function success(response) {
92 92 deferred.resolve(response);
93 93 }, function fail() {
94 94 deferred.reject();
... ... @@ -98,8 +98,8 @@ function LoginService($http, $q) {
98 98
99 99 function changePassword(currentPassword, newPassword) {
100 100 var deferred = $q.defer();
101   - var url = '/api/auth/changePassword?currentPassword=' + currentPassword + '&newPassword=' + newPassword;
102   - $http.post(url, null).then(function success(response) {
  101 + var url = '/api/auth/changePassword';
  102 + $http.post(url, {currentPassword: currentPassword, newPassword: newPassword}).then(function success(response) {
103 103 deferred.resolve(response);
104 104 }, function fail() {
105 105 deferred.reject();
... ...
... ... @@ -302,7 +302,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
302 302 $rootScope.forceFullscreen = true;
303 303 fetchAllowedDashboardIds();
304 304 } else if (currentUser.userId) {
305   - getUser(currentUser.userId).then(
  305 + getUser(currentUser.userId, true).then(
306 306 function success(user) {
307 307 currentUserDetails = user;
308 308 updateUserLang();
... ... @@ -319,6 +319,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
319 319 },
320 320 function fail() {
321 321 deferred.reject();
  322 + logout();
322 323 }
323 324 )
324 325 } else {
... ... @@ -414,19 +415,19 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
414 415 }
415 416 $http.post(url, user).then(function success(response) {
416 417 deferred.resolve(response.data);
417   - }, function fail(response) {
418   - deferred.reject(response.data);
  418 + }, function fail() {
  419 + deferred.reject();
419 420 });
420 421 return deferred.promise;
421 422 }
422 423
423   - function getUser(userId) {
  424 + function getUser(userId, ignoreErrors) {
424 425 var deferred = $q.defer();
425 426 var url = '/api/user/' + userId;
426   - $http.get(url).then(function success(response) {
  427 + $http.get(url, { ignoreErrors: ignoreErrors }).then(function success(response) {
427 428 deferred.resolve(response.data);
428   - }, function fail(response) {
429   - deferred.reject(response.data);
  429 + }, function fail() {
  430 + deferred.reject();
430 431 });
431 432 return deferred.promise;
432 433 }
... ... @@ -436,8 +437,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
436 437 var url = '/api/user/' + userId;
437 438 $http.delete(url).then(function success() {
438 439 deferred.resolve();
439   - }, function fail(response) {
440   - deferred.reject(response.data);
  440 + }, function fail() {
  441 + deferred.reject();
441 442 });
442 443 return deferred.promise;
443 444 }
... ...
... ... @@ -21,6 +21,7 @@ import thingsboardLedLight from '../components/led-light.directive';
21 21 import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget';
22 22 import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget';
23 23 import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget';
  24 +import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget';
24 25
25 26 import thingsboardRpcWidgets from '../widget/lib/rpc';
26 27
... ... @@ -42,7 +43,7 @@ import thingsboardTypes from '../common/types.constant';
42 43 import thingsboardUtils from '../common/utils.service';
43 44
44 45 export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
45   - thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils])
  46 + thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardExtensionsTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils])
46 47 .factory('widgetService', WidgetService)
47 48 .name;
48 49
... ...
... ... @@ -39,6 +39,7 @@ import uiRouter from 'angular-ui-router';
39 39 import angularJwt from 'angular-jwt';
40 40 import 'angular-drag-and-drop-lists';
41 41 import mdDataTable from 'angular-material-data-table';
  42 +import 'angular-material-expansion-panel';
42 43 import ngTouch from 'angular-touch';
43 44 import 'angular-carousel';
44 45 import 'clipboard';
... ... @@ -82,6 +83,7 @@ import 'md-color-picker/dist/mdColorPicker.min.css';
82 83 import 'mdPickers/dist/mdPickers.min.css';
83 84 import 'angular-hotkeys/build/hotkeys.min.css';
84 85 import 'angular-carousel/dist/angular-carousel.min.css';
  86 +import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css';
85 87 import '../scss/main.scss';
86 88
87 89 import AppConfig from './app.config';
... ... @@ -103,6 +105,7 @@ angular.module('thingsboard', [
103 105 angularJwt,
104 106 'dndLists',
105 107 mdDataTable,
  108 + 'material.components.expansionPanels',
106 109 ngTouch,
107 110 'angular-carousel',
108 111 'ngclipboard',
... ...
... ... @@ -16,7 +16,7 @@
16 16
17 17 -->
18 18 <div flex layout="column" style="margin-top: -10px;">
19   - <div flex style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
  19 + <div style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
20 20 <div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'asset.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
21 21 <div class="tb-small" ng-show="vm.isPublic()">{{'asset.public' | translate}}</div>
22 22 </div>
... ...
... ... @@ -77,8 +77,8 @@ export default function AssignAssetToCustomerController(customerService, assetSe
77 77
78 78 function assign() {
79 79 var tasks = [];
80   - for (var assetId in assetIds) {
81   - tasks.push(assetService.assignAssetToCustomer(vm.customers.selection.id.id, assetIds[assetId]));
  80 + for (var i=0;i<assetIds.length;i++) {
  81 + tasks.push(assetService.assignAssetToCustomer(vm.customers.selection.id.id, assetIds[i]));
82 82 }
83 83 $q.all(tasks).then(function () {
84 84 $mdDialog.hide();
... ...
... ... @@ -425,12 +425,26 @@ function DashboardUtils(types, utils, timeService) {
425 425 var prevColumns = prevGridSettings ? prevGridSettings.columns : 24;
426 426 var ratio = gridSettings.columns / prevColumns;
427 427 layout.gridSettings = gridSettings;
  428 + var maxRow = 0;
428 429 for (var w in layout.widgets) {
429 430 var widget = layout.widgets[w];
  431 + maxRow = Math.max(maxRow, widget.row + widget.sizeY);
  432 + }
  433 + var newMaxRow = Math.round(maxRow * ratio);
  434 + for (w in layout.widgets) {
  435 + widget = layout.widgets[w];
  436 + if (widget.row + widget.sizeY == maxRow) {
  437 + widget.row = Math.round(widget.row * ratio);
  438 + widget.sizeY = newMaxRow - widget.row;
  439 + } else {
  440 + widget.row = Math.round(widget.row * ratio);
  441 + widget.sizeY = Math.round(widget.sizeY * ratio);
  442 + }
430 443 widget.sizeX = Math.round(widget.sizeX * ratio);
431   - widget.sizeY = Math.round(widget.sizeY * ratio);
432 444 widget.col = Math.round(widget.col * ratio);
433   - widget.row = Math.round(widget.row * ratio);
  445 + if (widget.col + widget.sizeX > gridSettings.columns) {
  446 + widget.sizeX = gridSettings.columns - widget.col;
  447 + }
434 448 }
435 449 }
436 450
... ...
... ... @@ -317,6 +317,53 @@ export default angular.module('thingsboard.types', [])
317 317 name: "event.type-stats"
318 318 }
319 319 },
  320 + extensionType: {
  321 + http: "HTTP",
  322 + mqtt: "MQTT",
  323 + opc: "OPC UA"
  324 + },
  325 + extensionValueType: {
  326 + string: 'value.string',
  327 + long: 'value.long',
  328 + double: 'value.double',
  329 + boolean: 'value.boolean'
  330 + },
  331 + extensionTransformerType: {
  332 + toDouble: 'extension.to-double',
  333 + custom: 'extension.custom'
  334 + },
  335 + mqttConverterTypes: {
  336 + json: 'extension.converter-json',
  337 + custom: 'extension.custom'
  338 + },
  339 + mqttCredentialTypes: {
  340 + anonymous: {
  341 + value: "anonymous",
  342 + name: "extension.anonymous"
  343 + },
  344 + basic: {
  345 + value: "basic",
  346 + name: "extension.basic"
  347 + },
  348 + pem: {
  349 + value: "cert.PEM",
  350 + name: "extension.pem"
  351 + }
  352 + },
  353 + extensionOpcSecurityTypes: {
  354 + Basic128Rsa15: "Basic128Rsa15",
  355 + Basic256: "Basic256",
  356 + Basic256Sha256: "Basic256Sha256",
  357 + None: "None"
  358 + },
  359 + extensionIdentityType: {
  360 + anonymous: "extension.anonymous",
  361 + username: "extension.username"
  362 + },
  363 + extensionKeystoreType: {
  364 + PKCS12: "PKCS12",
  365 + JKS: "JKS"
  366 + },
320 367 latestTelemetry: {
321 368 value: "LATEST_TELEMETRY",
322 369 name: "attribute.scope-latest-telemetry",
... ...
... ... @@ -18,17 +18,17 @@ export default angular.module('thingsboard.directives.confirmOnExit', [])
18 18 .name;
19 19
20 20 /*@ngInject*/
21   -function ConfirmOnExit($state, $mdDialog, $window, $filter) {
  21 +function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
22 22 return {
23 23 link: function ($scope) {
24 24
25 25 $window.onbeforeunload = function () {
26   - if (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty) {
  26 + if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
27 27 return $filter('translate')('confirm-on-exit.message');
28 28 }
29 29 }
30 30 $scope.$on('$stateChangeStart', function (event, next, current, params) {
31   - if (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty) {
  31 + if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
32 32 event.preventDefault();
33 33 var confirm = $mdDialog.confirm()
34 34 .title($filter('translate')('confirm-on-exit.title'))
... ...
... ... @@ -140,6 +140,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
140 140 vm.widgetLayoutInfo = {
141 141 };
142 142
  143 + vm.widgetIds = [];
  144 +
143 145 vm.widgetItemMap = {
144 146 sizeX: 'vm.widgetLayoutInfo[widget.id].sizeX',
145 147 sizeY: 'vm.widgetLayoutInfo[widget.id].sizeY',
... ... @@ -233,73 +235,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
233 235 removeResizeListener(gridsterParent[0], onGridsterParentResize); // eslint-disable-line no-undef
234 236 });
235 237
236   - watchWidgets();
237   -
238 238 function onGridsterParentResize() {
239 239 if (gridsterParent.height() && autofillHeight()) {
240 240 updateMobileOpts();
241 241 }
242 242 }
243 243
244   - function watchWidgets() {
245   - $scope.widgetsCollectionWatch = $scope.$watchCollection('vm.widgets', function () {
246   - if (vm.skipInitialWidgetsWatch) {
247   - $timeout(function() { vm.skipInitialWidgetsWatch = false; });
248   - return;
249   - }
250   - var ids = [];
251   - for (var i=0;i<vm.widgets.length;i++) {
252   - var widget = vm.widgets[i];
253   - if (!widget.id) {
254   - widget.id = utils.guid();
255   - }
256   - ids.push(widget.id);
257   - var layoutInfoObject = vm.widgetLayoutInfo[widget.id];
258   - if (!layoutInfoObject) {
259   - layoutInfoObject = {
260   - widget: widget
261   - };
262   - Object.defineProperty(layoutInfoObject, 'sizeX', {
263   - get: function() { return widgetSizeX(this.widget) },
264   - set: function(newSizeX) { setWidgetSizeX(this.widget, newSizeX)}
265   - });
266   - Object.defineProperty(layoutInfoObject, 'sizeY', {
267   - get: function() { return widgetSizeY(this.widget) },
268   - set: function(newSizeY) { setWidgetSizeY(this.widget, newSizeY)}
269   - });
270   - Object.defineProperty(layoutInfoObject, 'row', {
271   - get: function() { return widgetRow(this.widget) },
272   - set: function(newRow) { setWidgetRow(this.widget, newRow)}
273   - });
274   - Object.defineProperty(layoutInfoObject, 'col', {
275   - get: function() { return widgetCol(this.widget) },
276   - set: function(newCol) { setWidgetCol(this.widget, newCol)}
277   - });
278   - vm.widgetLayoutInfo[widget.id] = layoutInfoObject;
279   - }
280   - }
281   - for (var widgetId in vm.widgetLayoutInfo) {
282   - if (ids.indexOf(widgetId) === -1) {
283   - delete vm.widgetLayoutInfo[widgetId];
284   - }
285   - }
286   - $mdUtil.nextTick(function () {
287   - sortWidgets();
288   - if (autofillHeight()) {
289   - updateMobileOpts();
290   - }
291   - });
292   - });
293   - }
294   -
295   - function stopWatchWidgets() {
296   - if ($scope.widgetsCollectionWatch) {
297   - $scope.widgetsCollectionWatch();
298   - $scope.widgetsCollectionWatch = null;
299   - }
300   - }
301   -
302   -
303 244 //TODO: widgets visibility
304 245 /*gridsterParent.scroll(function () {
305 246 updateVisibleRect();
... ... @@ -344,30 +285,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
344 285 return isMobileSize;
345 286 }
346 287
347   - $scope.$watch(function() { return $mdMedia('gt-sm'); }, function() {
348   - updateMobileOpts();
349   - });
350   -
351   - $scope.$watch('vm.isMobile', function () {
352   - updateMobileOpts();
353   - });
354   -
355   - $scope.$watch('vm.autofillHeight', function () {
356   - updateMobileOpts();
357   - });
358   -
359   - $scope.$watch('vm.mobileAutofillHeight', function () {
360   - updateMobileOpts();
361   - });
362   -
363   - $scope.$watch('vm.mobileRowHeight', function () {
364   - updateMobileOpts();
365   - });
366   -
367   - $scope.$watch('vm.isMobileDisabled', function () {
368   - updateMobileOpts();
369   - });
370   -
371 288 $scope.$watch('vm.columns', function () {
372 289 var columns = vm.columns ? vm.columns : 24;
373 290 if (vm.gridsterOpts.columns != columns) {
... ... @@ -381,6 +298,19 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
381 298 }
382 299 });
383 300
  301 + $scope.$watch(function() {
  302 + return $mdMedia('gt-sm') + ',' + vm.isMobile + ',' + vm.isMobileDisabled;
  303 + }, function() {
  304 + updateMobileOpts();
  305 + sortWidgets();
  306 + });
  307 +
  308 + $scope.$watch(function() {
  309 + return vm.autofillHeight + ',' + vm.mobileAutofillHeight + ',' + vm.mobileRowHeight;
  310 + }, function () {
  311 + updateMobileOpts();
  312 + });
  313 +
384 314 $scope.$watch('vm.margins', function () {
385 315 var margins = vm.margins ? vm.margins : [10, 10];
386 316 if (!angular.equals(vm.gridsterOpts.margins, margins)) {
... ... @@ -407,9 +337,70 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
407 337 }
408 338 });
409 339
  340 + $scope.$watchCollection('vm.widgets', function () {
  341 + var ids = [];
  342 + for (var i=0;i<vm.widgets.length;i++) {
  343 + var widget = vm.widgets[i];
  344 + if (!widget.id) {
  345 + widget.id = utils.guid();
  346 + }
  347 + ids.push(widget.id);
  348 + }
  349 + ids.sort(function (id1, id2) {
  350 + return id1.localeCompare(id2);
  351 + });
  352 + if (angular.equals(ids, vm.widgetIds)) {
  353 + return;
  354 + }
  355 + vm.widgetIds = ids;
  356 + for (i=0;i<vm.widgets.length;i++) {
  357 + widget = vm.widgets[i];
  358 + var layoutInfoObject = vm.widgetLayoutInfo[widget.id];
  359 + if (!layoutInfoObject) {
  360 + layoutInfoObject = {
  361 + widget: widget
  362 + };
  363 + Object.defineProperty(layoutInfoObject, 'sizeX', {
  364 + get: function() { return widgetSizeX(this.widget) },
  365 + set: function(newSizeX) { setWidgetSizeX(this.widget, newSizeX)}
  366 + });
  367 + Object.defineProperty(layoutInfoObject, 'sizeY', {
  368 + get: function() { return widgetSizeY(this.widget) },
  369 + set: function(newSizeY) { setWidgetSizeY(this.widget, newSizeY)}
  370 + });
  371 + Object.defineProperty(layoutInfoObject, 'row', {
  372 + get: function() { return widgetRow(this.widget) },
  373 + set: function(newRow) { setWidgetRow(this.widget, newRow)}
  374 + });
  375 + Object.defineProperty(layoutInfoObject, 'col', {
  376 + get: function() { return widgetCol(this.widget) },
  377 + set: function(newCol) { setWidgetCol(this.widget, newCol)}
  378 + });
  379 + vm.widgetLayoutInfo[widget.id] = layoutInfoObject;
  380 + }
  381 + }
  382 + for (var widgetId in vm.widgetLayoutInfo) {
  383 + if (ids.indexOf(widgetId) === -1) {
  384 + delete vm.widgetLayoutInfo[widgetId];
  385 + }
  386 + }
  387 + sortWidgets();
  388 + $mdUtil.nextTick(function () {
  389 + if (autofillHeight()) {
  390 + updateMobileOpts();
  391 + }
  392 + });
  393 + });
  394 +
  395 + $scope.$watch('vm.widgetLayouts', function () {
  396 + updateMobileOpts();
  397 + sortWidgets();
  398 + });
  399 +
410 400 $scope.$on('gridster-resized', function (event, sizes, theGridster) {
411 401 if (checkIsLocalGridsterElement(theGridster)) {
412 402 vm.gridster = theGridster;
  403 + setupGridster(vm.gridster);
413 404 vm.isResizing = false;
414 405 //TODO: widgets visibility
415 406 //updateVisibleRect(false, true);
... ... @@ -419,6 +410,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
419 410 $scope.$on('gridster-mobile-changed', function (event, theGridster) {
420 411 if (checkIsLocalGridsterElement(theGridster)) {
421 412 vm.gridster = theGridster;
  413 + setupGridster(vm.gridster);
422 414 detectRowSize(vm.gridster.isMobile).then(
423 415 function(rowHeight) {
424 416 if (vm.gridsterOpts.rowHeight != rowHeight) {
... ... @@ -517,18 +509,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
517 509 loadDashboard();
518 510
519 511 function sortWidgets() {
520   - stopWatchWidgets();
521 512 vm.widgets.sort(function (widget1, widget2) {
522 513 var row1 = widgetOrder(widget1);
523 514 var row2 = widgetOrder(widget2);
524 515 var res = row1 - row2;
525 516 if (res === 0) {
526   - res = widget1.col - widget2.col;
  517 + res = widgetCol(widget1) - widgetCol(widget2);
527 518 }
528 519 return res;
529 520 });
530   - vm.skipInitialWidgetsWatch = true;
531   - watchWidgets();
532 521 }
533 522
534 523 function reload() {
... ... @@ -1037,6 +1026,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
1037 1026 $scope.gridsterScopeWatcher = null;
1038 1027 var gridsterScope = gridsterElement.scope();
1039 1028 vm.gridster = gridsterScope.gridster;
  1029 + setupGridster(vm.gridster);
1040 1030 if (vm.onInit) {
1041 1031 vm.onInit({dashboard: vm});
1042 1032 }
... ... @@ -1046,6 +1036,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
1046 1036 });
1047 1037 }
1048 1038
  1039 + function setupGridster(gridster) {
  1040 + if (gridster) {
  1041 + if (!gridster.origMoveOverlappingItems) {
  1042 + gridster.origMoveOverlappingItems = gridster.moveOverlappingItems;
  1043 + gridster.moveOverlappingItems = () => {};
  1044 + }
  1045 + }
  1046 + }
  1047 +
1049 1048 function loading() {
1050 1049 return !vm.ignoreLoading && $rootScope.loading;
1051 1050 }
... ...
... ... @@ -16,7 +16,10 @@
16 16 @import '../../scss/constants';
17 17
18 18 .tb-details-title {
19   - font-size: 1.600rem;
  19 + font-size: 1.000rem;
  20 + @media (min-width: $layout-breakpoint-gt-sm) {
  21 + font-size: 1.600rem;
  22 + }
20 23 font-weight: 400;
21 24 text-transform: uppercase;
22 25 margin: 20px 8px 0 0;
... ...
... ... @@ -22,7 +22,7 @@ tb-js-func {
22 22 border: 1px solid #C0C0C0;
23 23 height: 100%;
24 24 #tb-javascript-input {
25   - min-width: 400px;
  25 + min-width: 200px;
26 26 min-height: 200px;
27 27 width: 100%;
28 28 height: 100%;
... ...
... ... @@ -19,7 +19,7 @@
19 19 <div layout="row" layout-align="start center" style="height: 40px;">
20 20 <span style="font-style: italic;">function({{ functionArgsString }}) {</span>
21 21 <span flex></span>
22   - <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
  22 + <div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div>
23 23 </div>
24 24 <div flex id="tb-javascript-panel" class="tb-js-func-panel" layout="column">
25 25 <div flex id="tb-javascript-input"
... ...
... ... @@ -77,8 +77,8 @@ export default function AssignDeviceToCustomerController(customerService, device
77 77
78 78 function assign() {
79 79 var tasks = [];
80   - for (var deviceId in deviceIds) {
81   - tasks.push(deviceService.assignDeviceToCustomer(vm.customers.selection.id.id, deviceIds[deviceId]));
  80 + for (var i=0;i<deviceIds.length;i++) {
  81 + tasks.push(deviceService.assignDeviceToCustomer(vm.customers.selection.id.id, deviceIds[i]));
82 82 }
83 83 $q.all(tasks).then(function () {
84 84 $mdDialog.hide();
... ...
... ... @@ -16,7 +16,7 @@
16 16
17 17 -->
18 18 <div flex layout="column" style="margin-top: -10px;">
19   - <div flex style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
  19 + <div style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
20 20 <div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
21 21 <div class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div>
22 22 </div>
... ...
... ... @@ -67,4 +67,11 @@
67 67 entity-type="{{vm.types.entityType.device}}">
68 68 </tb-relation-table>
69 69 </md-tab>
  70 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.operatingItem().additionalInfo.gateway" md-on-select="vm.grid.triggerResize()" label="{{ 'extension.extensions' | translate }}">
  71 + <tb-extension-table flex
  72 + entity-id="vm.grid.operatingItem().id.id"
  73 + entity-name="vm.grid.operatingItem().name"
  74 + entity-type="{{vm.types.entityType.device}}">
  75 + </tb-extension-table>
  76 + </md-tab>
70 77 </tb-grid>
... ...
... ... @@ -16,7 +16,7 @@
16 16
17 17 -->
18 18 <md-content flex class="md-padding tb-absolute-fill" layout="column">
19   - <section layout="row" ng-show="!disableAttributeScopeSelection">
  19 + <section ng-show="!disableAttributeScopeSelection">
20 20 <md-input-container class="md-block" style="width: 200px;">
21 21 <label translate>attribute.attributes-scope</label>
22 22 <md-select ng-model="attributeScope" ng-disabled="loading() || attributeScopeSelectionReadonly">
... ... @@ -26,7 +26,7 @@
26 26 </md-select>
27 27 </md-input-container>
28 28 </section>
29   - <div layout="column" class="md-whiteframe-z1" ng-class="{flex: mode==='widget'}">
  29 + <div class="md-whiteframe-z1" ng-class="{flex: mode==='widget'}">
30 30 <md-toolbar class="md-table-toolbar md-default" ng-show="mode==='default'
31 31 && !selectedAttributes.length
32 32 && query.search === null">
... ...
... ... @@ -79,10 +79,8 @@ export default function RelationDialogController($scope, $mdDialog, types, entit
79 79 });
80 80
81 81 function updateEditorSize(element) {
82   - var newWidth = 600;
83 82 var newHeight = 200;
84   - angular.element('#tb-relation-additional-info', element).height(newHeight.toString() + "px")
85   - .width(newWidth.toString() + "px");
  83 + angular.element('#tb-relation-additional-info', element).height(newHeight.toString() + "px");
86 84 vm.editor.resize();
87 85 }
88 86
... ...
... ... @@ -19,7 +19,7 @@
19 19 border: 1px solid #C0C0C0;
20 20 height: 100%;
21 21 #tb-relation-additional-info {
22   - min-width: 600px;
  22 + min-width: 200px;
23 23 min-height: 200px;
24 24 width: 100%;
25 25 height: 100%;
... ...
... ... @@ -15,7 +15,7 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<md-dialog aria-label="{{ (vm.isAdd ? 'relation.add' : 'relation.edit' ) | translate }}" style="min-width: 400px;">
  18 +<md-dialog aria-label="{{ (vm.isAdd ? 'relation.add' : 'relation.edit' ) | translate }}" style="min-width: 600px;">
19 19 <form name="theForm" ng-submit="vm.save()">
20 20 <md-toolbar>
21 21 <div class="md-toolbar-tools">
... ...
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import beautify from 'js-beautify';
  18 +
  19 +const js_beautify = beautify.js;
  20 +
  21 +/*@ngInject*/
  22 +export default function ExtensionDialogController($scope, $mdDialog, $translate, isAdd, allExtensions, entityId, entityType, extension, types, attributeService) {
  23 +
  24 + var vm = this;
  25 +
  26 + vm.types = types;
  27 + vm.isAdd = isAdd;
  28 + vm.entityType = entityType;
  29 + vm.entityId = entityId;
  30 + vm.allExtensions = allExtensions;
  31 +
  32 +
  33 + if (extension) {
  34 + vm.extension = angular.copy(extension);
  35 + editTransformers(vm.extension);
  36 + } else {
  37 + vm.extension = {};
  38 + }
  39 +
  40 +
  41 + vm.extensionTypeChange = function () {
  42 +
  43 + if (vm.extension.type === "HTTP") {
  44 + vm.extension.configuration = {
  45 + "converterConfigurations": []
  46 + };
  47 + }
  48 + if (vm.extension.type === "MQTT") {
  49 + vm.extension.configuration = {
  50 + "brokers": []
  51 + };
  52 + }
  53 + if (vm.extension.type === "OPC UA") {
  54 + vm.extension.configuration = {
  55 + "servers": []
  56 + };
  57 + }
  58 + };
  59 +
  60 + vm.cancel = cancel;
  61 + function cancel() {
  62 + $mdDialog.cancel();
  63 + }
  64 +
  65 + vm.save = save;
  66 + function save() {
  67 + let $errorElement = angular.element('[name=theForm]').find('.ng-invalid');
  68 +
  69 + if ($errorElement.length) {
  70 +
  71 + let $mdDialogScroll = angular.element('md-dialog-content').scrollTop();
  72 + let $mdDialogTop = angular.element('md-dialog-content').offset().top;
  73 + let $errorElementTop = angular.element('[name=theForm]').find('.ng-invalid').eq(0).offset().top;
  74 +
  75 +
  76 + if ($errorElementTop !== $mdDialogTop) {
  77 + angular.element('md-dialog-content').animate({
  78 + scrollTop: $mdDialogScroll + ($errorElementTop - $mdDialogTop) - 50
  79 + }, 500);
  80 + $errorElement.eq(0).focus();
  81 + }
  82 + } else {
  83 +
  84 + if(vm.isAdd) {
  85 + vm.allExtensions.push(vm.extension);
  86 + } else {
  87 + var index = vm.allExtensions.indexOf(extension);
  88 + if(index > -1) {
  89 + vm.allExtensions[index] = vm.extension;
  90 + }
  91 + }
  92 +
  93 + $mdDialog.hide();
  94 + saveTransformers();
  95 +
  96 + var editedValue = angular.toJson(vm.allExtensions);
  97 +
  98 + attributeService
  99 + .saveEntityAttributes(
  100 + vm.entityType,
  101 + vm.entityId,
  102 + types.attributesScope.shared.value,
  103 + [{key:"configuration", value:editedValue}]
  104 + )
  105 + .then(function success() {
  106 + });
  107 +
  108 + }
  109 + }
  110 +
  111 + vm.validateId = function() {
  112 + var coincidenceArray = vm.allExtensions.filter(function(ext) {
  113 + return ext.id == vm.extension.id;
  114 + });
  115 + if(coincidenceArray.length) {
  116 + if(!vm.isAdd) {
  117 + if(coincidenceArray[0].id == extension.id) {
  118 + $scope.theForm.extensionId.$setValidity('uniqueIdValidation', true);
  119 + } else {
  120 + $scope.theForm.extensionId.$setValidity('uniqueIdValidation', false);
  121 + }
  122 + } else {
  123 + $scope.theForm.extensionId.$setValidity('uniqueIdValidation', false);
  124 + }
  125 + } else {
  126 + $scope.theForm.extensionId.$setValidity('uniqueIdValidation', true);
  127 + }
  128 + };
  129 +
  130 + function saveTransformers() {
  131 + if(vm.extension.type == types.extensionType.http) {
  132 + var config = vm.extension.configuration.converterConfigurations;
  133 + if(config && config.length > 0) {
  134 + for(let i=0;i<config.length;i++) {
  135 + for(let j=0;j<config[i].converters.length;j++){
  136 + for(let k=0;k<config[i].converters[j].attributes.length;k++){
  137 + if(config[i].converters[j].attributes[k].transformerType == "toDouble"){
  138 + config[i].converters[j].attributes[k].transformer = {type: "intToDouble"};
  139 + }
  140 + delete config[i].converters[j].attributes[k].transformerType;
  141 + }
  142 + for(let l=0;l<config[i].converters[j].timeseries.length;l++) {
  143 + if(config[i].converters[j].timeseries[l].transformerType == "toDouble"){
  144 + config[i].converters[j].timeseries[l].transformer = {type: "intToDouble"};
  145 + }
  146 + delete config[i].converters[j].timeseries[l].transformerType;
  147 + }
  148 + }
  149 + }
  150 + }
  151 + }
  152 + if(vm.extension.type == types.extensionType.mqtt) {
  153 + var brokers = vm.extension.configuration.brokers;
  154 + if(brokers && brokers.length > 0) {
  155 + for(let i=0;i<brokers.length;i++) {
  156 + if(brokers[i].mapping && brokers[i].mapping.length > 0) {
  157 + for(let j=0;j<brokers[i].mapping.length;j++) {
  158 + if(brokers[i].mapping[j].converterType == "json") {
  159 + delete brokers[i].mapping[j].converter.nameExp;
  160 + delete brokers[i].mapping[j].converter.typeExp;
  161 + }
  162 + delete brokers[i].mapping[j].converterType;
  163 + }
  164 + }
  165 + if(brokers[i].connectRequests && brokers[i].connectRequests.length > 0) {
  166 + for(let j=0;j<brokers[i].connectRequests.length;j++) {
  167 + delete brokers[i].connectRequests[j].nameExp;
  168 + }
  169 + }
  170 + if(brokers[i].disconnectRequests && brokers[i].disconnectRequests.length > 0) {
  171 + for(let j=0;j<brokers[i].disconnectRequests.length;j++) {
  172 + delete brokers[i].disconnectRequests[j].nameExp;
  173 + }
  174 + }
  175 + if(brokers[i].attributeRequests && brokers[i].attributeRequests.length > 0) {
  176 + for(let j=0;j<brokers[i].attributeRequests.length;j++) {
  177 + delete brokers[i].attributeRequests[j].nameExp;
  178 + }
  179 + for(let j=0;j<brokers[i].attributeRequests.length;j++) {
  180 + delete brokers[i].attributeRequests[j].attrKey;
  181 + }
  182 + for(let j=0;j<brokers[i].attributeRequests.length;j++) {
  183 + delete brokers[i].attributeRequests[j].requestId;
  184 + }
  185 + }
  186 + }
  187 + }
  188 + }
  189 + }
  190 +
  191 + function editTransformers(extension) {
  192 + if(extension.type == types.extensionType.http) {
  193 + var config = extension.configuration.converterConfigurations;
  194 + for(let i=0;i<config.length;i++) {
  195 + for(let j=0;j<config[i].converters.length;j++){
  196 + for(let k=0;k<config[i].converters[j].attributes.length;k++){
  197 + if(config[i].converters[j].attributes[k].transformer){
  198 + if(config[i].converters[j].attributes[k].transformer.type == "intToDouble"){
  199 + config[i].converters[j].attributes[k].transformerType = "toDouble";
  200 + } else {
  201 + config[i].converters[j].attributes[k].transformerType = "custom";
  202 + config[i].converters[j].attributes[k].transformer = js_beautify(config[i].converters[j].attributes[k].transformer, {indent_size: 4});
  203 + }
  204 + }
  205 + }
  206 + for(let l=0;l<config[i].converters[j].timeseries.length;l++) {
  207 + if(config[i].converters[j].timeseries[l].transformer){
  208 + if(config[i].converters[j].timeseries[l].transformer.type == "intToDouble"){
  209 + config[i].converters[j].timeseries[l].transformerType = "toDouble";
  210 + } else {
  211 + config[i].converters[j].timeseries[l].transformerType = "custom";
  212 + config[i].converters[j].timeseries[l].transformer = js_beautify(config[i].converters[j].timeseries[l].transformer, {indent_size: 4});
  213 + }
  214 + }
  215 + }
  216 + }
  217 + }
  218 + }
  219 + if(extension.type == types.extensionType.mqtt) {
  220 + var brokers = extension.configuration.brokers;
  221 + for(let i=0;i<brokers.length;i++) {
  222 + if(brokers[i].mapping && brokers[i].mapping.length > 0) {
  223 + for(let j=0;j<brokers[i].mapping.length;j++) {
  224 + if(brokers[i].mapping[j].converter.type == "json") {
  225 + if(brokers[i].mapping[j].converter.deviceNameTopicExpression) {
  226 + brokers[i].mapping[j].converter.nameExp = "deviceNameTopicExpression";
  227 + } else {
  228 + brokers[i].mapping[j].converter.nameExp = "deviceNameJsonExpression";
  229 + }
  230 + if(brokers[i].mapping[j].converter.deviceTypeTopicExpression) {
  231 + brokers[i].mapping[j].converter.typeExp = "deviceTypeTopicExpression";
  232 + } else {
  233 + brokers[i].mapping[j].converter.typeExp = "deviceTypeJsonExpression";
  234 + }
  235 + brokers[i].mapping[j].converterType = "json";
  236 + } else {
  237 + brokers[i].mapping[j].converterType = "custom";
  238 + }
  239 + }
  240 + }
  241 + if(brokers[i].connectRequests && brokers[i].connectRequests.length > 0) {
  242 + for(let j=0;j<brokers[i].connectRequests.length;j++) {
  243 + if(brokers[i].connectRequests[j].deviceNameTopicExpression) {
  244 + brokers[i].connectRequests[j].nameExp = "deviceNameTopicExpression";
  245 + } else {
  246 + brokers[i].connectRequests[j].nameExp = "deviceNameJsonExpression";
  247 + }
  248 + }
  249 + }
  250 + if(brokers[i].disconnectRequests && brokers[i].disconnectRequests.length > 0) {
  251 + for(let j=0;j<brokers[i].disconnectRequests.length;j++) {
  252 + if(brokers[i].disconnectRequests[j].deviceNameTopicExpression) {
  253 + brokers[i].disconnectRequests[j].nameExp = "deviceNameTopicExpression";
  254 + } else {
  255 + brokers[i].disconnectRequests[j].nameExp = "deviceNameJsonExpression";
  256 + }
  257 + }
  258 + }
  259 + if(brokers[i].attributeRequests && brokers[i].attributeRequests.length > 0) {
  260 + for(let j=0;j<brokers[i].attributeRequests.length;j++) {
  261 + if(brokers[i].attributeRequests[j].deviceNameTopicExpression) {
  262 + brokers[i].attributeRequests[j].nameExp = "deviceNameTopicExpression";
  263 + } else {
  264 + brokers[i].attributeRequests[j].nameExp = "deviceNameJsonExpression";
  265 + }
  266 + if(brokers[i].attributeRequests[j].attributeKeyTopicExpression) {
  267 + brokers[i].attributeRequests[j].attrKey = "attributeKeyTopicExpression";
  268 + } else {
  269 + brokers[i].attributeRequests[j].attrKey = "attributeKeyJsonExpression";
  270 + }
  271 + if(brokers[i].attributeRequests[j].requestIdTopicExpression) {
  272 + brokers[i].attributeRequests[j].requestId = "requestIdTopicExpression";
  273 + } else {
  274 + brokers[i].attributeRequests[j].requestId = "requestIdJsonExpression";
  275 + }
  276 + }
  277 + }
  278 + }
  279 + }
  280 + }
  281 +}
  282 +
  283 +/*@ngInject*/
  284 +export function ParseToNull() {
  285 + var linker = function (scope, elem, attrs, ngModel) {
  286 + ngModel.$parsers.push(function(value) {
  287 + if(value === "") {
  288 + return null;
  289 + }
  290 + return value;
  291 + })
  292 + };
  293 + return {
  294 + restrict: "A",
  295 + link: linker,
  296 + require: "ngModel"
  297 + }
  298 +}
\ No newline at end of file
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-dialog class="extensionDialog" aria-label="{{ (vm.isAdd ? 'extension.add' : 'extension.edit' ) | translate }}">
  19 + <form name="theForm" ng-submit="vm.save()" novalidate>
  20 + <md-toolbar>
  21 + <div class="md-toolbar-tools">
  22 + <h2 translate>{{ vm.isAdd ? 'extension.add' : 'extension.edit'}}</h2>
  23 + <span flex></span>
  24 + <md-button class="md-icon-button" ng-click="vm.cancel()">
  25 + <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
  26 + </md-button>
  27 + </div>
  28 + </md-toolbar>
  29 +
  30 + <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
  31 +
  32 + <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
  33 +
  34 + <md-dialog-content>
  35 + <div class="md-dialog-content">
  36 + <md-content class="md-padding" layout="column">
  37 + <fieldset ng-disabled="loading">
  38 + <section flex layout="row">
  39 + <md-input-container flex="60" class="md-block" md-is-error="theForm.extensionId.$touched && theForm.extensionId.$invalid">
  40 + <label translate>extension.extension-id</label>
  41 + <input required name="extensionId" ng-model="vm.extension.id" ng-change="vm.validateId()">
  42 + <div ng-messages="theForm.extensionId.$error">
  43 + <div translate ng-message="required">extension.field-required</div>
  44 + <div translate ng-message="uniqueIdValidation">extension.unique-id-required</div>
  45 + </div>
  46 + </md-input-container>
  47 +
  48 + <md-input-container flex="40" class="md-block" md-is-error="theForm.extensionType.$touched && theForm.extensionType.$invalid">
  49 + <label translate>extension.extension-type</label>
  50 +
  51 + <md-select ng-disabled="!vm.isAdd" required name="extensionType" ng-change="vm.extensionTypeChange()" ng-model="vm.extension.type">
  52 + <md-option ng-repeat="(key,value) in vm.types.extensionType" ng-value="value">
  53 + {{value}}
  54 + </md-option>
  55 + </md-select>
  56 +
  57 + <div ng-messages="theForm.extensionType.$error">
  58 + <div translate ng-message="required">extension.field-required</div>
  59 + </div>
  60 + </md-input-container>
  61 + </section>
  62 + <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>
  63 + <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>
  64 + <div tb-extension-form-opc configuration="vm.extension.configuration" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.opc"></div>
  65 + </fieldset>
  66 + </md-content>
  67 + </div>
  68 + </md-dialog-content>
  69 +
  70 + <md-dialog-actions layout="row">
  71 + <md-button type="submit"
  72 + class="md-raised md-primary"
  73 + >
  74 + {{ (vm.isAdd ? 'action.add' : 'action.save') | translate }}
  75 + </md-button>
  76 +
  77 + <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}
  78 + </md-button>
  79 + </md-dialog-actions>
  80 + </form>
  81 +</md-dialog>
\ No newline at end of file
... ...
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import 'angular-material-data-table/dist/md-data-table.min.css';
  18 +import './extension-table.scss';
  19 +
  20 +/* eslint-disable import/no-unresolved, import/default */
  21 +
  22 +import extensionTableTemplate from './extension-table.tpl.html';
  23 +import extensionDialogTemplate from './extension-dialog.tpl.html';
  24 +
  25 +/* eslint-enable import/no-unresolved, import/default */
  26 +
  27 +import ExtensionDialogController from './extension-dialog.controller'
  28 +import $ from 'jquery';
  29 +
  30 +/*@ngInject*/
  31 +export default function ExtensionTableDirective() {
  32 + return {
  33 + restrict: "E",
  34 + scope: true,
  35 + bindToController: {
  36 + entityId: '=',
  37 + entityType: '@',
  38 + inWidget: '@?',
  39 + ctx: '=?',
  40 + entityName: '='
  41 + },
  42 + controller: ExtensionTableController,
  43 + controllerAs: 'vm',
  44 + templateUrl: extensionTableTemplate
  45 + };
  46 +}
  47 +
  48 +/*@ngInject*/
  49 +function ExtensionTableController($scope, $filter, $document, $translate, types, $mdDialog, attributeService, telemetryWebsocketService, importExport) {
  50 +
  51 + let vm = this;
  52 +
  53 + vm.extensions = [];
  54 + vm.allExtensions = [];
  55 + vm.selectedExtensions = [];
  56 + vm.extensionsCount = 0;
  57 +
  58 + vm.query = {
  59 + order: 'id',
  60 + limit: 5,
  61 + page: 1,
  62 + search: null
  63 + };
  64 +
  65 + vm.enterFilterMode = enterFilterMode;
  66 + vm.exitFilterMode = exitFilterMode;
  67 + vm.onReorder = onReorder;
  68 + vm.onPaginate = onPaginate;
  69 + vm.addExtension = addExtension;
  70 + vm.editExtension = editExtension;
  71 + vm.deleteExtension = deleteExtension;
  72 + vm.deleteExtensions = deleteExtensions;
  73 + vm.reloadExtensions = reloadExtensions;
  74 + vm.updateExtensions = updateExtensions;
  75 +
  76 + $scope.$watch("vm.entityId", function(newVal) {
  77 + if (newVal) {
  78 + if ($scope.subscriber) {
  79 + telemetryWebsocketService.unsubscribe($scope.subscriber);
  80 + $scope.subscriber = null;
  81 + }
  82 +
  83 + vm.subscribed = false;
  84 + vm.syncLastTime = $translate.instant('extension.sync.not-available');
  85 +
  86 + subscribeForClientAttributes();
  87 +
  88 + reloadExtensions();
  89 + }
  90 + });
  91 +
  92 + $scope.$on('$destroy', function() {
  93 + if ($scope.subscriber) {
  94 + telemetryWebsocketService.unsubscribe($scope.subscriber);
  95 + $scope.subscriber = null;
  96 + }
  97 + });
  98 +
  99 + $scope.$watch("vm.query.search", function(newVal, prevVal) {
  100 + if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
  101 + updateExtensions();
  102 + }
  103 + });
  104 +
  105 + $scope.$watch('vm.selectedExtensions.length', function (newLength) {
  106 + var selectionMode = newLength ? true : false;
  107 + if (vm.ctx) {
  108 + if (selectionMode) {
  109 + vm.ctx.hideTitlePanel = true;
  110 + $scope.$emit("selectedExtensions", true);
  111 + } else if (vm.query.search == null) {
  112 + vm.ctx.hideTitlePanel = false;
  113 + $scope.$emit("selectedExtensions", false);
  114 + }
  115 + }
  116 + });
  117 +
  118 + $scope.$on("showSearch", function($event, source) {
  119 + if(source.entityId == vm.entityId) {
  120 + enterFilterMode();
  121 + $scope.$emit("filterMode", true);
  122 + }
  123 + });
  124 + $scope.$on("refreshExtensions", function($event, source) {
  125 + if(source.entityId == vm.entityId) {
  126 + reloadExtensions();
  127 + }
  128 + });
  129 + $scope.$on("addExtension", function($event, source) {
  130 + if(source.entityId == vm.entityId) {
  131 + addExtension();
  132 + }
  133 + });
  134 + $scope.$on("exportExtensions", function($event, source) {
  135 + if(source.entityId == vm.entityId) {
  136 + vm.exportExtensions(source.entityName);
  137 + }
  138 + });
  139 + $scope.$on("importExtensions", function($event, source) {
  140 + if(source.entityId == vm.entityId) {
  141 + vm.importExtensions();
  142 + }
  143 + });
  144 +
  145 + function enterFilterMode() {
  146 + vm.query.search = '';
  147 + if(vm.inWidget) {
  148 + vm.ctx.hideTitlePanel = true;
  149 + }
  150 + }
  151 +
  152 + function exitFilterMode() {
  153 + vm.query.search = null;
  154 + updateExtensions();
  155 + if(vm.inWidget) {
  156 + vm.ctx.hideTitlePanel = false;
  157 + $scope.$emit("filterMode", false);
  158 + }
  159 + }
  160 +
  161 + function onReorder() {
  162 + updateExtensions();
  163 + }
  164 +
  165 + function onPaginate() {
  166 + updateExtensions();
  167 + }
  168 +
  169 + function addExtension($event) {
  170 + if ($event) {
  171 + $event.stopPropagation();
  172 + }
  173 + openExtensionDialog($event);
  174 + }
  175 +
  176 + function editExtension($event, extension) {
  177 + if ($event) {
  178 + $event.stopPropagation();
  179 + }
  180 + openExtensionDialog($event, extension);
  181 + }
  182 +
  183 + function openExtensionDialog($event, extension) {
  184 + if ($event) {
  185 + $event.stopPropagation();
  186 + }
  187 + var isAdd = false;
  188 + if(!extension) {
  189 + isAdd = true;
  190 + }
  191 + $mdDialog.show({
  192 + controller: ExtensionDialogController,
  193 + controllerAs: 'vm',
  194 + templateUrl: extensionDialogTemplate,
  195 + parent: angular.element($document[0].body),
  196 + locals: {
  197 + isAdd: isAdd,
  198 + allExtensions: vm.allExtensions,
  199 + entityId: vm.entityId,
  200 + entityType: vm.entityType,
  201 + extension: extension
  202 + },
  203 + bindToController: true,
  204 + targetEvent: $event,
  205 + fullscreen: true,
  206 + skipHide: true
  207 + }).then(function() {
  208 + reloadExtensions();
  209 + }, function () {
  210 + });
  211 + }
  212 +
  213 + function deleteExtension($event, extension) {
  214 + if ($event) {
  215 + $event.stopPropagation();
  216 + }
  217 + if(extension) {
  218 + var title = $translate.instant('extension.delete-extension-title', {extensionId: extension.id});
  219 + var content = $translate.instant('extension.delete-extension-text');
  220 +
  221 + var confirm = $mdDialog.confirm()
  222 + .targetEvent($event)
  223 + .title(title)
  224 + .htmlContent(content)
  225 + .ariaLabel(title)
  226 + .cancel($translate.instant('action.no'))
  227 + .ok($translate.instant('action.yes'));
  228 + $mdDialog.show(confirm).then(function() {
  229 + var editedExtensions = vm.allExtensions.filter(function(ext) {
  230 + return ext.id !== extension.id;
  231 + });
  232 + var editedValue = angular.toJson(editedExtensions);
  233 + attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then(
  234 + function success() {
  235 + reloadExtensions();
  236 + }
  237 + );
  238 + });
  239 + }
  240 + }
  241 +
  242 + function deleteExtensions($event) {
  243 + if ($event) {
  244 + $event.stopPropagation();
  245 + }
  246 + if (vm.selectedExtensions && vm.selectedExtensions.length > 0) {
  247 + var title = $translate.instant('extension.delete-extensions-title', {count: vm.selectedExtensions.length}, 'messageformat');
  248 + var content = $translate.instant('extension.delete-extensions-text');
  249 +
  250 + var confirm = $mdDialog.confirm()
  251 + .targetEvent($event)
  252 + .title(title)
  253 + .htmlContent(content)
  254 + .ariaLabel(title)
  255 + .cancel($translate.instant('action.no'))
  256 + .ok($translate.instant('action.yes'));
  257 + $mdDialog.show(confirm).then(function () {
  258 + var editedExtensions = angular.copy(vm.allExtensions);
  259 + for (var i = 0; i < vm.selectedExtensions.length; i++) {
  260 + editedExtensions = editedExtensions.filter(function (ext) {
  261 + return ext.id !== vm.selectedExtensions[i].id;
  262 + });
  263 + }
  264 + var editedValue = angular.toJson(editedExtensions);
  265 + attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then(
  266 + function success() {
  267 + reloadExtensions();
  268 + }
  269 + );
  270 + });
  271 + }
  272 + }
  273 +
  274 + function reloadExtensions() {
  275 + vm.subscribed = false;
  276 + vm.allExtensions.length = 0;
  277 + vm.extensions.length = 0;
  278 + vm.extensionsPromise = attributeService.getEntityAttributesValues(vm.entityType, vm.entityId, types.attributesScope.shared.value, ["configuration"]);
  279 + vm.extensionsPromise.then(
  280 + function success(data) {
  281 + if (data.length) {
  282 + vm.allExtensions = angular.fromJson(data[0].value);
  283 + } else {
  284 + vm.allExtensions = [];
  285 + }
  286 +
  287 + vm.selectedExtensions = [];
  288 + updateExtensions();
  289 + vm.extensionsPromise = null;
  290 + },
  291 + function fail() {
  292 + vm.extensions = [];
  293 + vm.selectedExtensions = [];
  294 + updateExtensions();
  295 + vm.extensionsPromise = null;
  296 + }
  297 + );
  298 + }
  299 +
  300 + function updateExtensions() {
  301 + vm.selectedExtensions = [];
  302 + var result = $filter('orderBy')(vm.allExtensions, vm.query.order);
  303 + if (vm.query.search != null) {
  304 + result = $filter('filter')(result, function(extension) {
  305 + if(!vm.query.search || (extension.id.indexOf(vm.query.search) != -1) || (extension.type.indexOf(vm.query.search) != -1)) {
  306 + return true;
  307 + }
  308 + return false;
  309 + });
  310 + }
  311 + vm.extensionsCount = result.length;
  312 + var startIndex = vm.query.limit * (vm.query.page - 1);
  313 + vm.extensions = result.slice(startIndex, startIndex + vm.query.limit);
  314 +
  315 + vm.extensionsJSON = angular.toJson(vm.extensions);
  316 + checkForSync();
  317 + }
  318 +
  319 + function subscribeForClientAttributes() {
  320 + if (!vm.subscribed) {
  321 + if (vm.entityId && vm.entityType) {
  322 + $scope.subscriber = {
  323 + subscriptionCommands: [{
  324 + entityType: vm.entityType,
  325 + entityId: vm.entityId,
  326 + scope: 'CLIENT_SCOPE'
  327 + }],
  328 + type: 'attribute',
  329 + onData: function (data) {
  330 + if (data.data) {
  331 + onSubscriptionData(data.data);
  332 + }
  333 + vm.subscribed = true;
  334 + }
  335 + };
  336 + telemetryWebsocketService.subscribe($scope.subscriber);
  337 + }
  338 + }
  339 + }
  340 + function onSubscriptionData(data) {
  341 +
  342 + if ($.isEmptyObject(data)) {
  343 + vm.appliedConfiguration = undefined;
  344 + } else {
  345 + if (data.appliedConfiguration && data.appliedConfiguration[0] && data.appliedConfiguration[0][1]) {
  346 + vm.appliedConfiguration = data.appliedConfiguration[0][1];
  347 + }
  348 + }
  349 +
  350 + updateExtensions();
  351 + $scope.$digest();
  352 + }
  353 +
  354 +
  355 + function checkForSync() {
  356 + if (vm.appliedConfiguration && vm.extensionsJSON && vm.appliedConfiguration === vm.extensionsJSON) {
  357 + vm.syncStatus = $translate.instant('extension.sync.sync');
  358 + vm.syncLastTime = formatDate();
  359 + $scope.isSync = true;
  360 + } else {
  361 + vm.syncStatus = $translate.instant('extension.sync.not-sync');
  362 +
  363 + $scope.isSync = false;
  364 + }
  365 + }
  366 +
  367 + function formatDate(date) {
  368 + let d;
  369 + if (date) {
  370 + d = date;
  371 + } else {
  372 + d = new Date();
  373 + }
  374 +
  375 + d = d.getFullYear() +'/'+ addZero(d.getMonth()+1) +'/'+ addZero(d.getDate()) + ' ' + addZero(d.getHours()) + ':' + addZero(d.getMinutes()) +':'+ addZero(d.getSeconds());
  376 + return d;
  377 +
  378 + function addZero(num) {
  379 + if ((angular.isNumber(num) && num < 10) || (angular.isString(num) && num.length === 1)) {
  380 + num = '0' + num;
  381 + }
  382 + return num;
  383 + }
  384 + }
  385 +
  386 + vm.importExtensions = function($event) {
  387 + importExport.importExtension($event, {"entityType":vm.entityType, "entityId":vm.entityId, "successFunc":reloadExtensions});
  388 + };
  389 + vm.exportExtensions = function(widgetSourceEntityName) {
  390 + if(vm.inWidget) {
  391 + importExport.exportToPc(vm.extensionsJSON, widgetSourceEntityName + '_configuration.json');
  392 + } else {
  393 + importExport.exportToPc(vm.extensionsJSON, vm.entityName + '_configuration.json');
  394 + }
  395 + };
  396 +
  397 + /*change function for widget implementing, like vm.exportExtensions*/
  398 + vm.exportExtension = function($event, extension) {
  399 + if ($event) {
  400 + $event.stopPropagation();
  401 + }
  402 + importExport.exportToPc(extension, vm.entityName +'_'+ extension.id +'_configuration.json');
  403 + };
  404 +}
\ No newline at end of file
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +@import '../../scss/constants';
  17 +
  18 +
  19 +.extension-table {
  20 +
  21 + md-input-container .md-errors-spacer {
  22 + min-height: 0;
  23 + }
  24 +
  25 + /*&.tb-data-table table.md-table tbody tr td.tb-action-cell,
  26 + &.tb-data-table table.md-table.md-row-select tbody tr td.tb-action-cell {
  27 + width: 114px;
  28 + }*/
  29 + .sync-widget {
  30 + max-height: 90px;
  31 + overflow: hidden;
  32 + }
  33 + .toolbar-widget {
  34 + min-height: 39px;
  35 + max-height: 39px;
  36 + }
  37 +}
  38 +
  39 +.extension__syncStatus--black {
  40 + color: #000000!important;
  41 +}
  42 +.extension__syncStatus--green {
  43 + color: #228634!important;
  44 +}
  45 +.extension__syncStatus--red {
  46 + color: #862222!important;
  47 +}
\ No newline at end of file
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +
  19 +<md-content flex class="md-padding tb-absolute-fill tb-data-table extension-table" layout="column">
  20 + <div layout="column" class="md-whiteframe-z1" ng-class="{'tb-absolute-fill' : vm.inWidget}">
  21 + <md-toolbar ng-if="!vm.inWidget" class="md-table-toolbar md-default" ng-show="!vm.selectedExtensions.length
  22 + && vm.query.search === null">
  23 + <div class="md-toolbar-tools">
  24 + <span translate>{{ 'extension.extensions' }}</span>
  25 + <span flex></span>
  26 +
  27 + <md-button class="md-icon-button" ng-click="vm.importExtensions($event)">
  28 + <md-icon>file_upload</md-icon>
  29 + <md-tooltip md-direction="top">
  30 + {{ 'extension.import-extensions-configuration' | translate }}
  31 + </md-tooltip>
  32 + </md-button>
  33 + <md-button class="md-icon-button" ng-click="vm.exportExtensions()">
  34 + <md-icon>file_download</md-icon>
  35 + <md-tooltip md-direction="top">
  36 + {{ 'extension.export-extensions-configuration' | translate }}
  37 + </md-tooltip>
  38 + </md-button>
  39 + <md-button class="md-icon-button" ng-click="vm.addExtension($event)">
  40 + <md-icon>add</md-icon>
  41 + <md-tooltip md-direction="top">
  42 + {{ 'action.add' | translate }}
  43 + </md-tooltip>
  44 + </md-button>
  45 + <md-button class="md-icon-button" ng-click="vm.enterFilterMode()">
  46 + <md-icon>search</md-icon>
  47 + <md-tooltip md-direction="top">
  48 + {{ 'action.search' | translate }}
  49 + </md-tooltip>
  50 + </md-button>
  51 + <md-button class="md-icon-button" ng-click="vm.reloadExtensions()">
  52 + <md-icon>refresh</md-icon>
  53 + <md-tooltip md-direction="top">
  54 + {{ 'action.refresh' | translate }}
  55 + </md-tooltip>
  56 + </md-button>
  57 + </div>
  58 + </md-toolbar>
  59 + <md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedExtensions.length
  60 + && vm.query.search != null" ng-class="{'toolbar-widget' : vm.inWidget}">
  61 + <div class="md-toolbar-tools">
  62 + <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
  63 + <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
  64 + <md-tooltip md-direction="top">
  65 + {{ 'action.search' | translate }}
  66 + </md-tooltip>
  67 + </md-button>
  68 + <md-input-container flex>
  69 + <label>&nbsp;</label>
  70 + <input ng-model="vm.query.search" placeholder="{{ 'common.enter-search' | translate }}"/>
  71 + </md-input-container>
  72 + <md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="vm.exitFilterMode()">
  73 + <md-icon aria-label="{{ 'action.close' | translate }}" class="material-icons">close</md-icon>
  74 + <md-tooltip md-direction="{{vm.ctx.dashboard.isWidgetExpanded ? 'bottom' : 'top'}}">
  75 + {{ 'action.close' | translate }}
  76 + </md-tooltip>
  77 + </md-button>
  78 + </div>
  79 + </md-toolbar>
  80 + <md-toolbar class="md-table-toolbar alternate" ng-show="vm.selectedExtensions.length" ng-class="{'toolbar-widget' : vm.inWidget}">
  81 + <div class="md-toolbar-tools">
  82 + <span translate
  83 + translate-values="{count: vm.selectedExtensions.length}"
  84 + translate-interpolation="messageformat">extension.selected-extensions</span>
  85 + <span flex></span>
  86 + <md-button class="md-icon-button" ng-click="vm.deleteExtensions($event)">
  87 + <md-icon>delete</md-icon>
  88 + <md-tooltip md-direction="{{vm.ctx.dashboard.isWidgetExpanded ? 'bottom' : 'top'}}">
  89 + {{ 'action.delete' | translate }}
  90 + </md-tooltip>
  91 + </md-button>
  92 + </div>
  93 + </md-toolbar>
  94 +
  95 + <div class="md-padding" flex layout="row" ng-class="{'sync-widget' : vm.inWidget}">
  96 + <md-input-container flex="50" class="md-block">
  97 + <label translate>extension.sync.status</label>
  98 + <input ng-model="vm.syncStatus"
  99 + ng-class="{'extension__syncStatus--green':isSync, 'extension__syncStatus--red':!isSync}"
  100 + disabled
  101 + >
  102 + </md-input-container>
  103 +
  104 + <md-input-container flex="50" class="md-block">
  105 + <label translate>extension.sync.last-sync-time</label>
  106 + <input ng-model="vm.syncLastTime"
  107 + class="extension__syncStatus--black"
  108 + disabled
  109 + >
  110 + </md-input-container>
  111 + </div>
  112 +
  113 + <md-table-container flex>
  114 + <table md-table md-row-select multiple="" ng-model="vm.selectedExtensions" md-progress="vm.extensionsDeferred.promise">
  115 + <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
  116 + <tr md-row>
  117 + <th md-column md-order-by="id"><span translate>extension.id</span></th>
  118 + <th md-column md-order-by="type"><span translate>extension.type</span></th>
  119 + <th md-column><span>&nbsp</span></th>
  120 + </tr>
  121 + </thead>
  122 + <tbody md-body>
  123 + <tr md-row md-select="extension" md-select-id="extension" md-auto-select ng-repeat="extension in vm.extensions">
  124 + <td md-cell>{{ extension.id }}</td>
  125 + <td md-cell>{{ extension.type }}</td>
  126 + <td md-cell class="tb-action-cell">
  127 +
  128 + <!--<md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}" ng-click="vm.exportExtension($event, extension)">
  129 + <md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">file_download</md-icon>
  130 + <md-tooltip md-direction="top">
  131 + {{ 'extension.export-extension' | translate }}
  132 + </md-tooltip>
  133 + </md-button>-->
  134 +
  135 + <md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}" ng-click="vm.editExtension($event, extension)">
  136 + <md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">edit</md-icon>
  137 + <md-tooltip md-direction="top">
  138 + {{ 'extension.edit' | translate }}
  139 + </md-tooltip>
  140 + </md-button>
  141 + <md-button class="md-icon-button" aria-label="{{ 'action.delete' | translate }}" ng-click="vm.deleteExtension($event, extension)">
  142 + <md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">delete</md-icon>
  143 + <md-tooltip md-direction="top">
  144 + {{ 'extension.delete' | translate }}
  145 + </md-tooltip>
  146 + </md-button>
  147 + </td>
  148 + </tr>
  149 + </tbody>
  150 + </table>
  151 + <md-divider ng-if="vm.inWidget"></md-divider>
  152 + </md-table-container>
  153 + <md-table-pagination md-limit="vm.query.limit" md-limit-options="[5, 10, 15]"
  154 + md-page="vm.query.page" md-total="{{vm.extensionsCount}}"
  155 + md-on-paginate="vm.onPaginate" md-page-select>
  156 + </md-table-pagination>
  157 + </div>
  158 +
  159 +</md-content>
\ No newline at end of file
... ...
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import 'brace/ext/language_tools';
  18 +import 'brace/mode/json';
  19 +import 'brace/theme/github';
  20 +
  21 +import './extension-form.scss';
  22 +
  23 +/* eslint-disable angular/log */
  24 +
  25 +import extensionFormHttpTemplate from './extension-form-http.tpl.html';
  26 +
  27 +/* eslint-enable import/no-unresolved, import/default */
  28 +
  29 +/*@ngInject*/
  30 +export default function ExtensionFormHttpDirective($compile, $templateCache, $translate, types) {
  31 +
  32 + var linker = function(scope, element) {
  33 +
  34 + var template = $templateCache.get(extensionFormHttpTemplate);
  35 + element.html(template);
  36 +
  37 + scope.types = types;
  38 + scope.theForm = scope.$parent.theForm;
  39 +
  40 + scope.extensionCustomTransformerOptions = {
  41 + useWrapMode: false,
  42 + mode: 'json',
  43 + showGutter: true,
  44 + showPrintMargin: true,
  45 + theme: 'github',
  46 + advanced: {
  47 + enableSnippets: true,
  48 + enableBasicAutocompletion: true,
  49 + enableLiveAutocompletion: true
  50 + },
  51 + onLoad: function(_ace) {
  52 + _ace.$blockScrolling = 1;
  53 + }
  54 + };
  55 +
  56 +
  57 + scope.addConverterConfig = function() {
  58 + var newConverterConfig = {converterId:"", converters:[]};
  59 + scope.converterConfigs.push(newConverterConfig);
  60 +
  61 + scope.converterConfigs[scope.converterConfigs.length - 1].converters = [];
  62 + scope.addConverter(scope.converterConfigs[scope.converterConfigs.length - 1].converters);
  63 + };
  64 +
  65 + scope.removeConverterConfig = function(config) {
  66 + var index = scope.converterConfigs.indexOf(config);
  67 + if (index > -1) {
  68 + scope.converterConfigs.splice(index, 1);
  69 + }
  70 + };
  71 +
  72 + scope.addConverter = function(converters) {
  73 + var newConverter = {
  74 + deviceNameJsonExpression:"",
  75 + deviceTypeJsonExpression:"",
  76 + attributes:[],
  77 + timeseries:[]
  78 + };
  79 + converters.push(newConverter);
  80 + };
  81 +
  82 + scope.removeConverter = function(converter, converters) {
  83 + var index = converters.indexOf(converter);
  84 + if (index > -1) {
  85 + converters.splice(index, 1);
  86 + }
  87 + };
  88 +
  89 + scope.addAttribute = function(attributes) {
  90 + var newAttribute = {type:"", key:"", value:""};
  91 + attributes.push(newAttribute);
  92 + };
  93 +
  94 + scope.removeAttribute = function(attribute, attributes) {
  95 + var index = attributes.indexOf(attribute);
  96 + if (index > -1) {
  97 + attributes.splice(index, 1);
  98 + }
  99 + };
  100 +
  101 +
  102 + if(scope.isAdd) {
  103 + scope.converterConfigs = scope.config.converterConfigurations;
  104 + scope.addConverterConfig();
  105 + } else {
  106 + scope.converterConfigs = scope.config.converterConfigurations;
  107 + }
  108 +
  109 + scope.transformerTypeChange = function(attribute) {
  110 + attribute.transformer = "";
  111 + };
  112 +
  113 + scope.validateTransformer = function (model, editorName) {
  114 + if(model && model.length) {
  115 + try {
  116 + angular.fromJson(model);
  117 + scope.theForm[editorName].$setValidity('transformerJSON', true);
  118 + } catch(e) {
  119 + scope.theForm[editorName].$setValidity('transformerJSON', false);
  120 + }
  121 + }
  122 + };
  123 +
  124 + scope.collapseValidation = function(index, id) {
  125 + var invalidState = angular.element('#'+id+':has(.ng-invalid)');
  126 + if(invalidState.length) {
  127 + invalidState.addClass('inner-invalid');
  128 + }
  129 + };
  130 +
  131 + scope.expandValidation = function (index, id) {
  132 + var invalidState = angular.element('#'+id);
  133 + invalidState.removeClass('inner-invalid');
  134 + };
  135 +
  136 + $compile(element.contents())(scope);
  137 + };
  138 +
  139 + return {
  140 + restrict: "A",
  141 + link: linker,
  142 + scope: {
  143 + config: "=",
  144 + isAdd: "="
  145 + }
  146 + }
  147 +}
\ No newline at end of file
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-card class="extension-form extension-http">
  19 + <md-card-title>
  20 + <md-card-title-text>
  21 + <span translate class="md-headline">extension.configuration</span>
  22 + </md-card-title-text>
  23 + </md-card-title>
  24 + <md-card-content>
  25 + <v-accordion id="http-converter-configs-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  26 + <v-pane id="http-converters-pane" expanded="true">
  27 + <v-pane-header>
  28 + {{ 'extension.converter-configurations' | translate }}
  29 + </v-pane-header>
  30 + <v-pane-content>
  31 + <div ng-if="converterConfigs.length > 0">
  32 + <ol class="list-group">
  33 + <li class="list-group-item" ng-repeat="(configIndex, config) in converterConfigs">
  34 + <md-button aria-label="{{ 'action.remove' | translate }}"
  35 + class="md-icon-button"
  36 + ng-click="removeConverterConfig(config)"
  37 + ng-hide="converterConfigs.length < 2"
  38 + >
  39 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  40 + <md-tooltip md-direction="top">
  41 + {{ 'action.remove' | translate }}
  42 + </md-tooltip>
  43 + </md-button>
  44 + <md-card>
  45 + <md-card-content>
  46 +
  47 + <md-input-container class="md-block" md-is-error="theForm['httpConverterId_' + configIndex].$touched && theForm['httpConverterId_' + configIndex].$invalid">
  48 + <label translate>extension.converter-id</label>
  49 + <input required name="httpConverterId_{{configIndex}}" ng-model="config.converterId">
  50 + <div ng-messages="theForm['httpConverterId_' + configIndex].$error">
  51 + <div translate ng-message="required">extension.field-required</div>
  52 + </div>
  53 + </md-input-container>
  54 + <md-input-container class="md-block">
  55 + <label translate>extension.token</label>
  56 + <input name="httpToken" ng-model="config.token" parse-to-null>
  57 + </md-input-container>
  58 + <v-accordion id="http-converters-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  59 + <v-pane id="http-converters-pane_{{configIndex}}" expanded="true">
  60 + <v-pane-header>
  61 + {{ 'extension.converters' | translate }}
  62 + </v-pane-header>
  63 + <v-pane-content>
  64 + <div ng-if="config.converters.length > 0">
  65 + <ol class="list-group">
  66 + <li class="list-group-item"
  67 + ng-repeat="(converterIndex,converter) in config.converters"
  68 + >
  69 + <md-button aria-label="{{ 'action.remove' | translate }}"
  70 + class="md-icon-button"
  71 + ng-click="removeConverter(converter, config.converters)"
  72 + ng-hide="config.converters.length < 2"
  73 + >
  74 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  75 + <md-tooltip md-direction="top">
  76 + {{ 'action.remove' | translate }}
  77 + </md-tooltip>
  78 + </md-button>
  79 + <md-card>
  80 + <md-card-content>
  81 + <md-input-container class="md-block" md-is-error="theForm['httpDeviceNameExp_' + configIndex + converterIndex].$touched && theForm['httpDeviceNameExp_' + configIndex + converterIndex].$invalid">
  82 + <label translate>extension.device-name-expression</label>
  83 + <input required name="httpDeviceNameExp_{{configIndex}}{{converterIndex}}" ng-model="converter.deviceNameJsonExpression">
  84 + <div ng-messages="theForm['httpDeviceNameExp_' + configIndex + converterIndex].$error">
  85 + <div translate ng-message="required">extension.field-required</div>
  86 + </div>
  87 + </md-input-container>
  88 + <md-input-container class="md-block" md-is-error="theForm['httpDeviceTypeExp_' + configIndex + converterIndex].$touched && theForm['httpDeviceTypeExp_' + configIndex + converterIndex].$invalid">
  89 + <label translate>extension.device-type-expression</label>
  90 + <input required name="httpDeviceTypeExp_{{configIndex}}{{converterIndex}}" ng-model="converter.deviceTypeJsonExpression">
  91 + <div ng-messages="theForm['httpDeviceTypeExp_' + configIndex + converterIndex].$error">
  92 + <div translate ng-message="required">extension.field-required</div>
  93 + </div>
  94 + </md-input-container>
  95 +
  96 + <v-accordion id="http-attributes-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  97 + <v-pane id="http-attributes-pane_{{configIndex}}{{converterIndex}}">
  98 + <v-pane-header>
  99 + {{ 'extension.attributes' | translate }}
  100 + </v-pane-header>
  101 + <v-pane-content>
  102 + <div ng-if="converter.attributes.length > 0">
  103 + <ol class="list-group">
  104 + <li class="list-group-item" ng-repeat="(attributeIndex, attribute) in converter.attributes">
  105 + <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attribute, converter.attributes)">
  106 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  107 + <md-tooltip md-direction="top">
  108 + {{ 'action.remove' | translate }}
  109 + </md-tooltip>
  110 + </md-button>
  111 + <md-card>
  112 + <md-card-content>
  113 + <section flex layout="row">
  114 + <md-input-container flex="60" class="md-block" md-is-error="theForm['httpAttributeKey_' + configIndex + converterIndex + attributeIndex].$touched && theForm['httpAttributeKey_' + configIndex + converterIndex + attributeIndex].$invalid">
  115 + <label translate>extension.key</label>
  116 + <input required name="httpAttributeKey_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.key">
  117 + <div ng-messages="theForm['httpAttributeKey_' + configIndex + converterIndex + attributeIndex].$error">
  118 + <div translate ng-message="required">extension.field-required</div>
  119 + </div>
  120 + </md-input-container>
  121 + <md-input-container flex="40" class="md-block" md-is-error="theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$touched && theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$invalid">
  122 + <label translate>extension.type</label>
  123 + <md-select required name="httpAttributeType_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.type">
  124 + <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
  125 + {{attrTypeValue | translate}}
  126 + </md-option>
  127 + </md-select>
  128 + <div ng-messages="theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$error">
  129 + <div translate ng-message="required">extension.field-required</div>
  130 + </div>
  131 + </md-input-container>
  132 + </section>
  133 + <section flex layout="row">
  134 + <md-input-container flex="60" class="md-block" md-is-error="theForm['httpAttributeValue_' + configIndex + converterIndex + attributeIndex].$touched && theForm['httpAttributeValue_' + configIndex + converterIndex + attributeIndex].$invalid">
  135 + <label translate>extension.value</label>
  136 + <input required name="httpAttributeValue_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.value">
  137 + <div ng-messages="theForm['httpAttributeValue_' + configIndex + converterIndex + attributeIndex].$error">
  138 + <div translate ng-message="required">extension.field-required</div>
  139 + </div>
  140 + </md-input-container>
  141 +
  142 +
  143 + <md-input-container flex="40" class="md-block">
  144 + <label translate>extension.transformer</label>
  145 + <md-select name="httpAttributeTransformer" ng-model="attribute.transformerType" ng-change="transformerTypeChange(attribute)">
  146 + <md-option ng-repeat="(transformerType, value) in types.extensionTransformerType" ng-value="transformerType">
  147 + {{value | translate}}
  148 + </md-option>
  149 + </md-select>
  150 + </md-input-container>
  151 + </section>
  152 +
  153 + <div ng-if='attribute.transformerType == "custom"'>
  154 + <div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
  155 + <div flex class="tb-extension-custom-transformer-panel">
  156 + <div flex class="tb-extension-custom-transformer"
  157 + ui-ace="extensionCustomTransformerOptions"
  158 + ng-model="attribute.transformer"
  159 + name="attributeCustomTransformer_{{configIndex}}{{converterIndex}}{{attributeIndex}}"
  160 + ng-change='validateTransformer(attribute.transformer,"attributeCustomTransformer_" + configIndex + converterIndex + attributeIndex)'
  161 + required>
  162 + </div>
  163 + </div>
  164 + <div class="tb-error-messages" ng-messages="theForm['attributeCustomTransformer_' + configIndex + converterIndex + attributeIndex].$error" role="alert">
  165 + <div ng-message="required" class="tb-error-message" translate>extension.json-required</div>
  166 + <div ng-message="transformerJSON" class="tb-error-message" translate>extension.json-parse</div>
  167 + </div>
  168 + </div>
  169 +
  170 +
  171 + </md-card-content>
  172 + </md-card>
  173 + </li>
  174 + </ol>
  175 + </div>
  176 + <div flex layout="row" layout-align="start center">
  177 + <md-button class="md-primary md-raised"
  178 + ng-click="addAttribute(converter.attributes)" aria-label="{{ 'action.add' | translate }}">
  179 + <md-icon class="material-icons">add</md-icon>
  180 + <span translate>extension.add-attribute</span>
  181 + </md-button>
  182 + </div>
  183 + </v-pane-content>
  184 + </v-pane>
  185 + </v-accordion>
  186 +
  187 +
  188 + <v-accordion id="http-timeseries-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  189 + <v-pane id="http-timeseries-pane_{{configIndex}}{{converterIndex}}">
  190 + <v-pane-header>
  191 + {{ 'extension.timeseries' | translate }}
  192 + </v-pane-header>
  193 + <v-pane-content>
  194 + <div ng-if="converter.timeseries.length > 0">
  195 + <ol class="list-group">
  196 + <li class="list-group-item" ng-repeat="(timeseriesIndex, timeseries) in converter.timeseries">
  197 + <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(timeseries, converter.timeseries)">
  198 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  199 + <md-tooltip md-direction="top">
  200 + {{ 'action.remove' | translate }}
  201 + </md-tooltip>
  202 + </md-button>
  203 + <md-card>
  204 + <md-card-content>
  205 + <section flex layout="row">
  206 + <md-input-container flex="60" class="md-block" md-is-error="theForm['httpTimeseriesKey_' + configIndex + converterIndex + timeseriesIndex].$touched && theForm['httpTimeseriesKey_' + configIndex + converterIndex + timeseriesIndex].$invalid">
  207 + <label translate>extension.key</label>
  208 + <input required name="httpTimeseriesKey_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.key">
  209 + <div ng-messages="theForm['httpTimeseriesKey_' + configIndex + converterIndex + timeseriesIndex].$error">
  210 + <div translate ng-message="required">extension.field-required</div>
  211 + </div>
  212 + </md-input-container>
  213 + <md-input-container flex="40" class="md-block" md-is-error="theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$touched && theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$invalid">
  214 + <label translate>extension.type</label>
  215 + <md-select required name="httpTimeseriesType_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.type">
  216 + <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
  217 + {{attrTypeValue | translate}}
  218 + </md-option>
  219 + </md-select>
  220 + <div ng-messages="theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$error">
  221 + <div translate ng-message="required">extension.field-required</div>
  222 + </div>
  223 + </md-input-container>
  224 + </section>
  225 + <section flex layout="row">
  226 + <md-input-container flex="60" class="md-block" md-is-error="theForm['httpTimeseriesValue_' + configIndex + converterIndex + timeseriesIndex].$touched && theForm['httpTimeseriesValue_' + configIndex + converterIndex + timeseriesIndex].$invalid">
  227 + <label translate>extension.value</label>
  228 + <input required name="httpTimeseriesValue_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.value">
  229 + <div ng-messages="theForm['httpTimeseriesValue_' + configIndex + converterIndex + timeseriesIndex].$error">
  230 + <div translate ng-message="required">extension.field-required</div>
  231 + </div>
  232 + </md-input-container>
  233 +
  234 +
  235 + <md-input-container flex="40" class="md-block">
  236 + <label translate>extension.transformer</label>
  237 + <md-select name="httpTimeseriesTransformer" ng-model="timeseries.transformerType" ng-change="transformerTypeChange(timeseries)">
  238 + <md-option ng-repeat="(transformerType, value) in types.extensionTransformerType" ng-value="transformerType">
  239 + {{value | translate}}
  240 + </md-option>
  241 + </md-select>
  242 + </md-input-container>
  243 + </section>
  244 +
  245 + <div ng-if='timeseries.transformerType == "custom"'>
  246 + <div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
  247 + <div flex class="tb-extension-custom-transformer-panel">
  248 + <div flex class="tb-extension-custom-transformer"
  249 + ui-ace="extensionCustomTransformerOptions"
  250 + ng-model="timeseries.transformer"
  251 + name="timeseriesCustomTransformer_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}"
  252 + ng-change='validateTransformer(timeseries.transformer,"timeseriesCustomTransformer_" + configIndex + converterIndex + timeseriesIndex)'
  253 + required>
  254 + </div>
  255 + </div>
  256 + <div class="tb-error-messages" ng-messages="theForm['timeseriesCustomTransformer_' + configIndex + converterIndex + timeseriesIndex].$error" role="alert">
  257 + <div ng-message="required" class="tb-error-message" translate>extension.json-required</div>
  258 + <div ng-message="transformerJSON" class="tb-error-message" translate>extension.json-parse</div>
  259 + </div>
  260 + </div>
  261 +
  262 +
  263 + </md-card-content>
  264 + </md-card>
  265 + </li>
  266 + </ol>
  267 + </div>
  268 + <div flex layout="row" layout-align="start center">
  269 + <md-button class="md-primary md-raised"
  270 + ng-click="addAttribute(converter.timeseries)" aria-label="{{ 'action.add' | translate }}">
  271 + <md-icon class="material-icons">add</md-icon>
  272 + <span translate>extension.add-timeseries</span>
  273 + </md-button>
  274 + </div>
  275 + </v-pane-content>
  276 + </v-pane>
  277 + </v-accordion>
  278 + </md-card-content>
  279 + </md-card>
  280 + </li>
  281 + </ol>
  282 + </div>
  283 + <div flex layout="row" layout-align="start center">
  284 + <md-button class="md-primary md-raised"
  285 + ng-click="addConverter(config.converters)" aria-label="{{ 'action.add' | translate }}">
  286 + <md-icon class="material-icons">add</md-icon>
  287 + <span translate>extension.add-converter</span>
  288 + </md-button>
  289 + </div>
  290 + </v-pane-content>
  291 + </v-pane>
  292 + </v-accordion>
  293 +
  294 + </md-card-content>
  295 + </md-card>
  296 + </li>
  297 + </ol>
  298 + </div>
  299 + <div flex layout="row" layout-align="start center">
  300 + <md-button class="md-primary md-raised"
  301 + ng-click="addConverterConfig()" aria-label="{{ 'action.add' | translate }}">
  302 + <md-icon class="material-icons">add</md-icon>
  303 + <span translate>extension.add-config</span>
  304 + </md-button>
  305 + </div>
  306 + </v-pane-content>
  307 + </v-pane>
  308 + </v-accordion>
  309 + <!--{{config}}-->
  310 + </md-card-content>
  311 +</md-card>
... ...
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import './extension-form.scss';
  18 +
  19 +/* eslint-disable angular/log */
  20 +
  21 +import extensionFormMqttTemplate from './extension-form-mqtt.tpl.html';
  22 +
  23 +/* eslint-enable import/no-unresolved, import/default */
  24 +
  25 +/*@ngInject*/
  26 +export default function ExtensionFormHttpDirective($compile, $templateCache, $translate, types) {
  27 +
  28 + var linker = function(scope, element) {
  29 +
  30 + var template = $templateCache.get(extensionFormMqttTemplate);
  31 + element.html(template);
  32 +
  33 + scope.types = types;
  34 + scope.theForm = scope.$parent.theForm;
  35 +
  36 + scope.deviceNameExpressions = {
  37 + deviceNameJsonExpression: "extension.converter-json",
  38 + deviceNameTopicExpression: "extension.topic"
  39 + };
  40 + scope.deviceTypeExpressions = {
  41 + deviceTypeJsonExpression: "extension.converter-json",
  42 + deviceTypeTopicExpression: "extension.topic"
  43 + };
  44 + scope.attributeKeyExpressions = {
  45 + attributeKeyJsonExpression: "extension.converter-json",
  46 + attributeKeyTopicExpression: "extension.topic"
  47 + };
  48 + scope.requestIdExpressions = {
  49 + requestIdJsonExpression: "extension.converter-json",
  50 + requestIdTopicExpression: "extension.topic"
  51 + }
  52 +
  53 + scope.extensionCustomConverterOptions = {
  54 + useWrapMode: false,
  55 + mode: 'json',
  56 + showGutter: true,
  57 + showPrintMargin: true,
  58 + theme: 'github',
  59 + advanced: {
  60 + enableSnippets: true,
  61 + enableBasicAutocompletion: true,
  62 + enableLiveAutocompletion: true
  63 + },
  64 + onLoad: function(_ace) {
  65 + _ace.$blockScrolling = 1;
  66 + }
  67 + };
  68 +
  69 + scope.updateValidity = function () {
  70 + if(scope.brokers.length) {
  71 + for(let i=0;i<scope.brokers.length;i++) {
  72 + if(scope.brokers[i].credentials.type == scope.types.mqttCredentialTypes.pem.value) {
  73 + if(!(scope.brokers[i].credentials.caCert && scope.brokers[i].credentials.privateKey && scope.brokers[i].credentials.cert)) {
  74 + scope.theForm.$setValidity('cert.PEM', false);
  75 + break;
  76 + } else {
  77 + scope.theForm.$setValidity('cert.PEM', true);
  78 + }
  79 + }
  80 + }
  81 + }
  82 + };
  83 +
  84 + scope.$watch('brokers', function() {
  85 + scope.updateValidity();
  86 + }, true);
  87 +
  88 + scope.addBroker = function() {
  89 + var newBroker = {
  90 + host: "localhost",
  91 + port: 1882,
  92 + ssl: false,
  93 + retryInterval: 3000,
  94 + credentials: {type:"anonymous"},
  95 + mapping: [],
  96 + connectRequests: [],
  97 + disconnectRequests: [],
  98 + attributeRequests: [],
  99 + attributeUpdates: [],
  100 + serverSideRpc: []
  101 + };
  102 + scope.brokers.push(newBroker);
  103 + };
  104 +
  105 + scope.removeBroker = function(broker) {
  106 + var index = scope.brokers.indexOf(broker);
  107 + if (index > -1) {
  108 + scope.brokers.splice(index, 1);
  109 + }
  110 + };
  111 +
  112 + if(scope.isAdd) {
  113 + scope.brokers = [];
  114 + scope.config.brokers = scope.brokers;
  115 + scope.addBroker();
  116 + } else {
  117 + scope.brokers = scope.config.brokers;
  118 + }
  119 +
  120 + scope.addMap = function(mapping) {
  121 + var newMap = {topicFilter:"sensors", converter:{attributes:[],timeseries:[]}};
  122 +
  123 + mapping.push(newMap);
  124 + };
  125 +
  126 + scope.removeMap = function(map, mapping) {
  127 + var index = mapping.indexOf(map);
  128 + if (index > -1) {
  129 + mapping.splice(index, 1);
  130 + }
  131 + };
  132 +
  133 + scope.addAttribute = function(attributes) {
  134 + var newAttribute = {type:"", key:"", value:""};
  135 + attributes.push(newAttribute);
  136 + };
  137 +
  138 + scope.removeAttribute = function(attribute, attributes) {
  139 + var index = attributes.indexOf(attribute);
  140 + if (index > -1) {
  141 + attributes.splice(index, 1);
  142 + }
  143 + };
  144 +
  145 + scope.addConnectRequest = function(requests, type) {
  146 + var newRequest = {};
  147 + if(type == "connect") {
  148 + newRequest.topicFilter = "sensors/connect";
  149 + } else {
  150 + newRequest.topicFilter = "sensors/disconnect";
  151 + }
  152 + requests.push(newRequest);
  153 + };
  154 +
  155 + scope.addAttributeRequest = function(requests) {
  156 + var newRequest = {
  157 + topicFilter: "sensors/attributes",
  158 + clientScope: false,
  159 + responseTopicExpression: "sensors/${deviceName}/attributes/${responseId}",
  160 + valueExpression: "${attributeValue}"
  161 + };
  162 + requests.push(newRequest);
  163 + };
  164 +
  165 + scope.addAttributeUpdate = function(updates) {
  166 + var newUpdate = {
  167 + deviceNameFilter: ".*",
  168 + attributeFilter: ".*",
  169 + topicExpression: "sensor/${deviceName}/${attributeKey}",
  170 + valueExpression: "{\"${attributeKey}\":\"${attributeValue}\"}"
  171 + }
  172 + updates.push(newUpdate);
  173 + };
  174 +
  175 + scope.addServerSideRpc = function(rpcRequests) {
  176 + var newRpc = {
  177 + deviceNameFilter: ".*",
  178 + methodFilter: "echo",
  179 + requestTopicExpression: "sensor/${deviceName}/request/${methodName}/${requestId}",
  180 + responseTopicExpression: "sensor/${deviceName}/response/${methodName}/${requestId}",
  181 + responseTimeout: 10000,
  182 + valueExpression: "${params}"
  183 + };
  184 + rpcRequests.push(newRpc);
  185 + };
  186 +
  187 + scope.changeCredentials = function(broker) {
  188 + var type = broker.credentials.type;
  189 + broker.credentials = {};
  190 + broker.credentials.type = type;
  191 + };
  192 +
  193 + scope.changeConverterType = function(map) {
  194 + if(map.converterType == "custom"){
  195 + map.converter = "";
  196 + }
  197 + if(map.converterType == "json") {
  198 + map.converter = {attributes:[],timeseries:[]};
  199 + }
  200 + };
  201 +
  202 + scope.changeNameExpression = function(element, type) {
  203 + if(element.nameExp == "deviceNameJsonExpression") {
  204 + if(element.deviceNameTopicExpression) {
  205 + delete element.deviceNameTopicExpression;
  206 + }
  207 + if(type) {
  208 + element.deviceNameJsonExpression = "${$.serialNumber}";
  209 + }
  210 + }
  211 + if(element.nameExp == "deviceNameTopicExpression") {
  212 + if(element.deviceNameJsonExpression) {
  213 + delete element.deviceNameJsonExpression;
  214 + }
  215 + if(type && type == "connect") {
  216 + element.deviceNameTopicExpression = "(?<=sensor\\/)(.*?)(?=\\/connect)";
  217 + }
  218 + if(type && type == "disconnect") {
  219 + element.deviceNameTopicExpression = "(?<=sensor\\/)(.*?)(?=\\/disconnect)";
  220 + }
  221 + if(type && type == "attribute") {
  222 + element.deviceNameTopicExpression = "(?<=sensors\\/)(.*?)(?=\\/attributes)";
  223 + }
  224 + }
  225 + };
  226 +
  227 + scope.changeTypeExpression = function(converter) {
  228 + if(converter.typeExp == "deviceTypeJsonExpression") {
  229 + if(converter.deviceTypeTopicExpression) {
  230 + delete converter.deviceTypeTopicExpression;
  231 + }
  232 + }
  233 + if(converter.typeExp == "deviceTypeTopicExpression") {
  234 + if(converter.deviceTypeJsonExpression) {
  235 + delete converter.deviceTypeJsonExpression;
  236 + }
  237 + }
  238 + };
  239 +
  240 + scope.changeAttrKeyExpression = function(request) {
  241 + if(request.attrKey == "attributeKeyJsonExpression") {
  242 + if(request.attributeKeyTopicExpression) {
  243 + delete request.attributeKeyTopicExpression;
  244 + }
  245 + request.attributeKeyJsonExpression = "${$.key}";
  246 + }
  247 + if(request.attrKey == "attributeKeyTopicExpression") {
  248 + if(request.attributeKeyJsonExpression) {
  249 + delete request.attributeKeyJsonExpression;
  250 + }
  251 + request.attributeKeyTopicExpression = "(?<=attributes\\/)(.*?)(?=\\/request)";
  252 + }
  253 + };
  254 +
  255 + scope.changeRequestIdExpression = function(request) {
  256 + if(request.requestId == "requestIdJsonExpression") {
  257 + if(request.requestIdTopicExpression) {
  258 + delete request.requestIdTopicExpression;
  259 + }
  260 + request.requestIdJsonExpression = "${$.requestId}";
  261 + }
  262 + if(request.requestId == "requestIdTopicExpression") {
  263 + if(request.requestIdJsonExpression) {
  264 + delete request.requestIdJsonExpression;
  265 + }
  266 + request.requestIdTopicExpression = "(?<=request\\/)(.*?)($)";
  267 + }
  268 + };
  269 +
  270 + scope.validateCustomConverter = function(model, editorName) {
  271 + if(model && model.length) {
  272 + try {
  273 + angular.fromJson(model);
  274 + scope.theForm[editorName].$setValidity('converterJSON', true);
  275 + } catch(e) {
  276 + scope.theForm[editorName].$setValidity('converterJSON', false);
  277 + }
  278 + }
  279 + };
  280 +
  281 + scope.fileAdded = function($file, broker, fileType) {
  282 + var reader = new FileReader();
  283 + reader.onload = function(event) {
  284 + scope.$apply(function() {
  285 + if(event.target.result) {
  286 + scope.theForm.$setDirty();
  287 + var addedFile = event.target.result;
  288 + if (addedFile && addedFile.length > 0) {
  289 + if(fileType == "caCert") {
  290 + broker.credentials.caCertFileName = $file.name;
  291 + broker.credentials.caCert = addedFile.replace(/^data.*base64,/, "");
  292 + }
  293 + if(fileType == "privateKey") {
  294 + broker.credentials.privateKeyFileName = $file.name;
  295 + broker.credentials.privateKey = addedFile.replace(/^data.*base64,/, "");
  296 + }
  297 + if(fileType == "Cert") {
  298 + broker.credentials.certFileName = $file.name;
  299 + broker.credentials.cert = addedFile.replace(/^data.*base64,/, "");
  300 + }
  301 + }
  302 + }
  303 + });
  304 + };
  305 + reader.readAsDataURL($file.file);
  306 + };
  307 +
  308 + scope.clearFile = function(broker, fileType) {
  309 + scope.theForm.$setDirty();
  310 + if(fileType == "caCert") {
  311 + broker.credentials.caCertFileName = null;
  312 + broker.credentials.caCert = null;
  313 + }
  314 + if(fileType == "privateKey") {
  315 + broker.credentials.privateKeyFileName = null;
  316 + broker.credentials.privateKey = null;
  317 + }
  318 + if(fileType == "Cert") {
  319 + broker.credentials.certFileName = null;
  320 + broker.credentials.cert = null;
  321 + }
  322 + };
  323 +
  324 + scope.collapseValidation = function(index, id) {
  325 + var invalidState = angular.element('#'+id+':has(.ng-invalid)');
  326 + if(invalidState.length) {
  327 + invalidState.addClass('inner-invalid');
  328 + }
  329 + };
  330 +
  331 + scope.expandValidation = function (index, id) {
  332 + var invalidState = angular.element('#'+id);
  333 + invalidState.removeClass('inner-invalid');
  334 + };
  335 +
  336 + $compile(element.contents())(scope);
  337 + };
  338 +
  339 + return {
  340 + restrict: "A",
  341 + link: linker,
  342 + scope: {
  343 + config: "=",
  344 + isAdd: "="
  345 + }
  346 + }
  347 +}
\ No newline at end of file
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-card class="extension-form extension-mqtt">
  19 + <md-card-title name="testValid">
  20 + <md-card-title-text>
  21 + <span translate class="md-headline">extension.configuration</span>
  22 + </md-card-title-text>
  23 + </md-card-title>
  24 + <md-card-content>
  25 + <v-accordion id="mqtt-brokers-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  26 + <v-pane id="mqtt-brokers-pane" expanded="true">
  27 + <v-pane-header>
  28 + {{ 'extension.brokers' | translate }}
  29 + </v-pane-header>
  30 + <v-pane-content>
  31 + <div ng-if="brokers.length > 0">
  32 + <ol class="list-group">
  33 + <li class="list-group-item" ng-repeat="(brokerIndex,broker) in brokers">
  34 + <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeBroker(broker)" ng-hide="brokers.length < 2">
  35 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  36 + <md-tooltip md-direction="top">
  37 + {{ 'action.remove' | translate }}
  38 + </md-tooltip>
  39 + </md-button>
  40 + <md-card>
  41 + <md-card-content>
  42 + <section flex layout="row">
  43 + <md-input-container flex="40" class="md-block">
  44 + <label translate>extension.port</label>
  45 + <input required type="number" min="1" max="65535" name="mqttPort_{{brokerIndex}}" ng-model="broker.port">
  46 + <div ng-messages="theForm['mqttPort_' + brokerIndex].$error">
  47 + <div translate ng-message="required">extension.field-required</div>
  48 + <div translate ng-message="min">extension.port-range</div>
  49 + <div translate ng-message="max">extension.port-range</div>
  50 + </div>
  51 + </md-input-container>
  52 + <md-input-container flex="60" class="md-block">
  53 + <label translate>extension.host</label>
  54 + <input required name="mqttHost_{{brokerIndex}}" ng-model="broker.host">
  55 + <div ng-messages="theForm['mqttHost_' + brokerIndex].$error">
  56 + <div translate ng-message="required">extension.field-required</div>
  57 + </div>
  58 + </md-input-container>
  59 + </section>
  60 + <section flex layout="row">
  61 + <md-input-container flex="40" class="md-block">
  62 + <label translate>extension.retry-interval</label>
  63 + <input required type="number" name="mqttRetryInterval_{{brokerIndex}}" ng-model="broker.retryInterval">
  64 + <div ng-messages="theForm['mqttRetryInterval_' + brokerIndex].$error">
  65 + <div translate ng-message="required">extension.field-required</div>
  66 + </div>
  67 + </md-input-container>
  68 + <md-input-container flex="50" class="md-block">
  69 + <label translate>extension.credentials</label>
  70 + <md-select required name="mqttCredentials_{{brokerIndex}}" ng-model="broker.credentials.type" ng-change="changeCredentials(broker)">
  71 + <md-option ng-repeat="(credentialsType, credentialsValue) in types.mqttCredentialTypes" ng-value="credentialsValue.value">
  72 + {{credentialsValue.name | translate}}
  73 + </md-option>
  74 + </md-select>
  75 + </md-input-container>
  76 + <md-input-container flex="10" class="md-block">
  77 + <md-checkbox flex aria-label="{{ 'extension.ssl' | translate }}"
  78 + ng-model="broker.ssl">{{ 'extension.ssl' | translate }}
  79 + </md-checkbox>
  80 + </md-input-container>
  81 + </section>
  82 + <section flex layout="row" ng-if='broker.credentials.type == "basic"'>
  83 + <md-input-container flex="40" class="md-block" md-is-error="theForm['mqttUsername_' + brokerIndex].$touched && theForm['mqttUsername_' + brokerIndex].$invalid">
  84 + <label translate>extension.username</label>
  85 + <input required name="mqttUsername_{{brokerIndex}}" ng-model="broker.credentials.username">
  86 + <div ng-messages="theForm['mqttUsername_' + brokerIndex].$error">
  87 + <div translate ng-message="required">extension.field-required</div>
  88 + </div>
  89 + </md-input-container>
  90 + <md-input-container flex="60" class="md-block" md-is-error="theForm['mqttPassword_' + brokerIndex].$touched && theForm['mqttPassword_' + brokerIndex].$invalid">
  91 + <label translate>extension.password</label>
  92 + <input required name="mqttPassword_{{brokerIndex}}" ng-model="broker.credentials.password">
  93 + <div ng-messages="theForm['mqttPassword_' + brokerIndex].$error">
  94 + <div translate ng-message="required">extension.field-required</div>
  95 + </div>
  96 + </md-input-container>
  97 + </section>
  98 + <section flex layout="column" ng-if='broker.credentials.type == "cert.PEM"' class="dropdown-section">
  99 + <div class="tb-container" ng-class="broker.credentials.caCertFileName ? 'ng-valid' : 'ng-invalid'">
  100 + <label class="tb-label" translate>extension.ca-cert</label>
  101 + <div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, broker, "caCert")' class="tb-file-select-container">
  102 + <div class="tb-file-clear-container">
  103 + <md-button ng-click='clearFile(broker, "caCert")' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
  104 + <md-tooltip md-direction="top">
  105 + {{ 'action.remove' | translate }}
  106 + </md-tooltip>
  107 + <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
  108 + </md-button>
  109 + </div>
  110 + <div class="alert tb-flow-drop" flow-drop>
  111 + <label for="caCertSelect_{{brokerIndex}}" translate>extension.drop-file</label>
  112 + <input class="file-input" flow-btn flow-attrs="{accept:'.pem'}" id="caCertSelect_{{brokerIndex}}">
  113 + </div>
  114 + </div>
  115 + </div>
  116 + <div class="dropdown-messages">
  117 + <div ng-if="!broker.credentials.caCertFileName" class="tb-error-message" translate>extension.no-file</div>
  118 + <div ng-if="broker.credentials.caCertFileName">{{broker.credentials.caCertFileName}}</div>
  119 + </div>
  120 + <div class="tb-container" ng-class="broker.credentials.privateKeyFileName ? 'ng-valid' : 'ng-invalid'">
  121 + <label class="tb-label" translate>extension.private-key</label>
  122 + <div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, broker, "privateKey")' class="tb-file-select-container">
  123 + <div class="tb-file-clear-container">
  124 + <md-button ng-click='clearFile(broker, "privateKey")' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
  125 + <md-tooltip md-direction="top">
  126 + {{ 'action.remove' | translate }}
  127 + </md-tooltip>
  128 + <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
  129 + </md-button>
  130 + </div>
  131 + <div class="alert tb-flow-drop" flow-drop>
  132 + <label for="privateKeySelect_{{brokerIndex}}" translate>extension.drop-file</label>
  133 + <input class="file-input" flow-btn flow-attrs="{accept:'.pem'}" id="privateKeySelect_{{brokerIndex}}">
  134 + </div>
  135 + </div>
  136 + </div>
  137 + <div class="dropdown-messages">
  138 + <div ng-if="!broker.credentials.privateKeyFileName" class="tb-error-message" translate>extension.no-file</div>
  139 + <div ng-if="broker.credentials.privateKeyFileName">{{broker.credentials.privateKeyFileName}}</div>
  140 + </div>
  141 + <div class="tb-container" ng-class="broker.credentials.certFileName ? 'ng-valid' : 'ng-invalid'">
  142 + <label class="tb-label" translate>extension.cert</label>
  143 + <div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, broker, "Cert")' class="tb-file-select-container">
  144 + <div class="tb-file-clear-container">
  145 + <md-button ng-click='clearFile(broker, "Cert")' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
  146 + <md-tooltip md-direction="top">
  147 + {{ 'action.remove' | translate }}
  148 + </md-tooltip>
  149 + <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
  150 + </md-button>
  151 + </div>
  152 + <div class="alert tb-flow-drop" flow-drop>
  153 + <label for="CertSelect_{{brokerIndex}}" translate>extension.drop-file</label>
  154 + <input class="file-input" flow-btn flow-attrs="{accept:'.pem'}" id="CertSelect_{{brokerIndex}}">
  155 + </div>
  156 + </div>
  157 + </div>
  158 + <div class="dropdown-messages">
  159 + <div ng-if="!broker.credentials.certFileName" class="tb-error-message" translate>extension.no-file</div>
  160 + <div ng-if="broker.credentials.certFileName">{{broker.credentials.certFileName}}</div>
  161 + </div>
  162 + </section>
  163 +
  164 + <v-accordion id="mqtt-mapping-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  165 + <v-pane id="mqtt-mapping-pane_{{brokerIndex}}">
  166 + <v-pane-header>
  167 + {{ 'extension.mapping' | translate }}
  168 + </v-pane-header>
  169 + <v-pane-content>
  170 + <div ng-if="broker.mapping.length > 0">
  171 + <ol class="list-group">
  172 + <li class="list-group-item" ng-repeat="(mapIndex,map) in broker.mapping">
  173 + <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeMap(map, broker.mapping)">
  174 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  175 + <md-tooltip md-direction="top">
  176 + {{ 'action.remove' | translate }}
  177 + </md-tooltip>
  178 + </md-button>
  179 + <md-card>
  180 + <md-card-content>
  181 + <section flex layout="row">
  182 + <md-input-container flex="40" class="md-block" md-is-error="theForm['mqttConverterType_' + brokerIndex + mapIndex].$touched && theForm['mqttConverterType_' + brokerIndex + mapIndex].$invalid">
  183 + <label translate>extension.converter-type</label>
  184 + <md-select required name="mqttConverterType_{{brokerIndex}}{{mapIndex}}" ng-model="map.converterType" ng-change="changeConverterType(map)">
  185 + <md-option ng-repeat="(converterType, value) in types.mqttConverterTypes" ng-value="converterType">
  186 + {{value | translate}}
  187 + </md-option>
  188 + </md-select>
  189 + <div ng-messages="theForm['mqttConverterType_' + brokerIndex + mapIndex].$error">
  190 + <div translate ng-message="required">extension.field-required</div>
  191 + </div>
  192 + </md-input-container>
  193 + <md-input-container flex="60" class="md-block">
  194 + <label translate>extension.topic-filter</label>
  195 + <input required name="mqttTopicFilter_{{brokerIndex}}{{mapIndex}}" ng-model="map.topicFilter">
  196 + <div ng-messages="theForm['mqttTopicFilter_' + brokerIndex + mapIndex].$error">
  197 + <div translate ng-message="required">extension.field-required</div>
  198 + </div>
  199 + </md-input-container>
  200 + </section>
  201 +
  202 + <div ng-if='map.converterType =="json"' ng-init="map.converter.type = 'json'">
  203 + <section flex layout="row">
  204 + <md-input-container flex="40" class="md-block" md-is-error="theForm['mqttDeviceNameExpression_' + brokerIndex + mapIndex].$touched && theForm['mqttDeviceNameExpression_' + brokerIndex + mapIndex].$invalid">
  205 + <label translate>extension.device-name-expression</label>
  206 + <md-select required name="mqttDeviceNameExpression_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.nameExp" ng-change="changeNameExpression(map.converter)">
  207 + <md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
  208 + {{value | translate}}
  209 + </md-option>
  210 + </md-select>
  211 + <div ng-messages="theForm['mqttDeviceNameExpression_' + brokerIndex + mapIndex].$error">
  212 + <div translate ng-message="required">extension.field-required</div>
  213 + </div>
  214 + </md-input-container>
  215 + <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">
  216 + <label translate>extension.json-name-expression</label>
  217 + <input required name="mqttJsonNameExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceNameJsonExpression">
  218 + <div ng-messages="theForm['mqttJsonNameExp_' + brokerIndex + mapIndex].$error">
  219 + <div translate ng-message="required">extension.field-required</div>
  220 + </div>
  221 + </md-input-container>
  222 + <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">
  223 + <label translate>extension.topic-name-expression</label>
  224 + <input required name="mqttTopicNameExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceNameTopicExpression">
  225 + <div ng-messages="theForm['mqttTopicNameExp_' + brokerIndex + mapIndex].$error">
  226 + <div translate ng-message="required">extension.field-required</div>
  227 + </div>
  228 + </md-input-container>
  229 + </section>
  230 + <section flex layout="row">
  231 + <md-input-container flex="40" class="md-block" md-is-error="theForm['mqttDeviceTypeExpression_' + brokerIndex + mapIndex].$touched && theForm['mqttDeviceTypeExpression_' + brokerIndex + mapIndex].$invalid">
  232 + <label translate>extension.device-type-expression</label>
  233 + <md-select required name="mqttDeviceTypeExpression_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.typeExp" ng-change="changeTypeExpression(map.converter)">
  234 + <md-option ng-repeat="(key, value) in deviceTypeExpressions" ng-value='key'>
  235 + {{value | translate}}
  236 + </md-option>
  237 + </md-select>
  238 + <div ng-messages="theForm['mqttDeviceTypeExpression_' + brokerIndex + mapIndex].$error">
  239 + <div translate ng-message="required">extension.field-required</div>
  240 + </div>
  241 + </md-input-container>
  242 + <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">
  243 + <label translate>extension.json-type-expression</label>
  244 + <input required name="mqttJsonTypeExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceTypeJsonExpression">
  245 + <div ng-messages="theForm['mqttJsonTypeExp_' + brokerIndex + mapIndex].$error">
  246 + <div translate ng-message="required">extension.field-required</div>
  247 + </div>
  248 + </md-input-container>
  249 + <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">
  250 + <label translate>extension.topic-type-expression</label>
  251 + <input required name="mqttTopicTypeExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceTypeTopicExpression">
  252 + <div ng-messages="theForm['mqttTopicTypeExp_' + brokerIndex + mapIndex].$error">
  253 + <div translate ng-message="required">extension.field-required</div>
  254 + </div>
  255 + </md-input-container>
  256 + </section>
  257 + <section flex layout="row">
  258 + <md-input-container flex="40" class="md-block">
  259 + <label translate>extension.timeout</label>
  260 + <input type="number" name="mqttTimeout_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.timeout" parse-to-null>
  261 + </md-input-container>
  262 + <md-input-container flex="60" class="md-block" md-is-error="theForm['mqttFilterExpression' + brokerIndex + mapIndex].$touched && theForm['mqttFilterExpression' + brokerIndex + mapIndex].$invalid">
  263 + <label translate>extension.filter-expression</label>
  264 + <input required name="mqttFilterExpression{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.filterExpression">
  265 + <div ng-messages="theForm['mqttFilterExpression' + brokerIndex + mapIndex].$error">
  266 + <div translate ng-message="required">extension.field-required</div>
  267 + </div>
  268 + </md-input-container>
  269 + </section>
  270 + </div>
  271 +
  272 + <div ng-if='map.converterType == "custom"'>
  273 + <div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
  274 + <div flex class="tb-extension-custom-transformer-panel">
  275 + <div flex class="tb-extension-custom-transformer"
  276 + ui-ace="extensionCustomConverterOptions"
  277 + ng-model="map.converter"
  278 + name="mqttCustomConverter_{{brokerIndex}}{{mapIndex}}"
  279 + ng-change='validateCustomConverter(map.converter, "mqttCustomConverter_" + brokerIndex + mapIndex)'
  280 + required>
  281 + </div>
  282 + </div>
  283 + <div class="tb-error-messages" ng-messages="theForm['mqttCustomConverter_' + brokerIndex + mapIndex].$error" role="alert">
  284 + <div ng-message="required" class="tb-error-message" translate>extension.converter-json-required</div>
  285 + <div ng-message="converterJSON" class="tb-error-message" translate>extension.converter-json-parse</div>
  286 + </div>
  287 + </div>
  288 +
  289 + <v-accordion ng-if='map.converterType =="json"' id="mqtt-attributes-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  290 + <v-pane id="mqtt-attributes-pane_{{brokerIndex}}{{mapIndex}}">
  291 + <v-pane-header>
  292 + {{ 'extension.attributes' | translate }}
  293 + </v-pane-header>
  294 + <v-pane-content>
  295 + <div ng-if="map.converter.attributes.length > 0">
  296 + <ol class="list-group">
  297 + <li class="list-group-item" ng-repeat="(attributeIndex, attribute) in map.converter.attributes">
  298 + <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attribute, map.converter.attributes)">
  299 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  300 + <md-tooltip md-direction="top">
  301 + {{ 'action.remove' | translate }}
  302 + </md-tooltip>
  303 + </md-button>
  304 + <md-card>
  305 + <md-card-content>
  306 + <section flex layout="row">
  307 + <md-input-container flex="60" class="md-block" md-is-error="theForm['mqttAttributeKey_' + brokerIndex + mapIndex + attributeIndex].$touched && theForm['mqttAttributeKey_' + brokerIndex + mapIndex + attributeIndex].$invalid">
  308 + <label translate>extension.key</label>
  309 + <input required name="mqttAttributeKey_{{brokerIndex}}{{mapIndex}}{{attributeIndex}}" ng-model="attribute.key">
  310 + <div ng-messages="theForm['mqttAttributeKey_' + brokerIndex + mapIndex + attributeIndex].$error">
  311 + <div translate ng-message="required">extension.field-required</div>
  312 + </div>
  313 + </md-input-container>
  314 + <md-input-container flex="40" class="md-block" md-is-error="theForm['mqttAttributeType_' + brokerIndex + mapIndex + attributeIndex].$touched && theForm['mqttAttributeType_' + brokerIndex + mapIndex + attributeIndex].$invalid">
  315 + <label translate>extension.type</label>
  316 + <md-select required name="mqttAttributeType_{{brokerIndex}}{{mapIndex}}{{attributeIndex}}" ng-model="attribute.type">
  317 + <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
  318 + {{attrTypeValue | translate}}
  319 + </md-option>
  320 + </md-select>
  321 + <div ng-messages="theForm['mqttAttributeType_' + brokerIndex + mapIndex + attributeIndex].$error">
  322 + <div translate ng-message="required">extension.field-required</div>
  323 + </div>
  324 + </md-input-container>
  325 + </section>
  326 + <md-input-container class="md-block" md-is-error="theForm['mqttAttributeValue_' + brokerIndex + mapIndex + attributeIndex].$touched && theForm['mqttAttributeValue_' + brokerIndex + mapIndex + attributeIndex].$invalid">
  327 + <label translate>extension.value</label>
  328 + <input required name="mqttAttributeValue_{{brokerIndex}}{{mapIndex}}{{attributeIndex}}" ng-model="attribute.value">
  329 + <div ng-messages="theForm['mqttAttributeValue_' + brokerIndex + mapIndex + attributeIndex].$error">
  330 + <div translate ng-message="required">extension.field-required</div>
  331 + </div>
  332 + </md-input-container>
  333 + </md-card-content>
  334 + </md-card>
  335 + </li>
  336 + </ol>
  337 + </div>
  338 + <div flex layout="row" layout-align="start center">
  339 + <md-button class="md-primary md-raised"
  340 + ng-click="addAttribute(map.converter.attributes)" aria-label="{{ 'action.add' | translate }}">
  341 + <md-icon class="material-icons">add</md-icon>
  342 + <span translate>extension.add-attribute</span>
  343 + </md-button>
  344 + </div>
  345 + </v-pane-content>
  346 + </v-pane>
  347 + </v-accordion>
  348 +
  349 + <v-accordion ng-if='map.converterType =="json"' id="mqtt-timeseries-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  350 + <v-pane id="mqtt-timeseries-pane_{{brokerIndex}}{{mapIndex}}">
  351 + <v-pane-header>
  352 + {{ 'extension.timeseries' | translate }}
  353 + </v-pane-header>
  354 + <v-pane-content>
  355 + <div ng-if="map.converter.timeseries.length > 0">
  356 + <ol class="list-group">
  357 + <li class="list-group-item" ng-repeat="(timeseriesIndex, timeseries) in map.converter.timeseries">
  358 + <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(timeseries, map.converter.timeseries)">
  359 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  360 + <md-tooltip md-direction="top">
  361 + {{ 'action.remove' | translate }}
  362 + </md-tooltip>
  363 + </md-button>
  364 + <md-card>
  365 + <md-card-content>
  366 + <section flex layout="row">
  367 + <md-input-container flex="60" class="md-block" md-is-error="theForm['mqttTimeseriesKey_' + brokerIndex + mapIndex + timeseriesIndex].$touched && theForm['mqttTimeseriesKey_' + brokerIndex + mapIndex + timeseriesIndex].$invalid">
  368 + <label translate>extension.key</label>
  369 + <input required name="mqttTimeseriesKey_{{brokerIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.key">
  370 + <div ng-messages="theForm['mqttTimeseriesKey_' + brokerIndex + mapIndex + timeseriesIndex].$error">
  371 + <div translate ng-message="required">extension.field-required</div>
  372 + </div>
  373 + </md-input-container>
  374 + <md-input-container flex="40" class="md-block" md-is-error="theForm['mqttTimeseriesType_' + brokerIndex + mapIndex + timeseriesIndex].$touched && theForm['mqttTimeseriesType_' + brokerIndex + mapIndex + timeseriesIndex].$invalid">
  375 + <label translate>extension.type</label>
  376 + <md-select required name="mqttTimeseriesType_{{brokerIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.type">
  377 + <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
  378 + {{attrTypeValue | translate}}
  379 + </md-option>
  380 + </md-select>
  381 + <div ng-messages="theForm['mqttTimeseriesType_' + brokerIndex + mapIndex + timeseriesIndex].$error">
  382 + <div translate ng-message="required">extension.field-required</div>
  383 + </div>
  384 + </md-input-container>
  385 + </section>
  386 + <md-input-container class="md-block" md-is-error="theForm['mqttTimeseriesValue_' + brokerIndex + mapIndex + timeseriesIndex].$touched && theForm['mqttTimeseriesValue_' + brokerIndex + mapIndex + timeseriesIndex].$invalid">
  387 + <label translate>extension.value</label>
  388 + <input required name="mqttTimeseriesValue_{{brokerIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.value">
  389 + <div ng-messages="theForm['mqttTimeseriesValue_' + brokerIndex + mapIndex + timeseriesIndex].$error">
  390 + <div translate ng-message="required">extension.field-required</div>
  391 + </div>
  392 + </md-input-container>
  393 + </md-card-content>
  394 + </md-card>
  395 + </li>
  396 + </ol>
  397 + </div>
  398 + <div flex layout="row" layout-align="start center">
  399 + <md-button class="md-primary md-raised"
  400 + ng-click="addAttribute(map.converter.timeseries)" aria-label="{{ 'action.add' | translate }}">
  401 + <md-icon class="material-icons">add</md-icon>
  402 + <span translate>extension.add-timeseries</span>
  403 + </md-button>
  404 + </div>
  405 + </v-pane-content>
  406 + </v-pane>
  407 + </v-accordion>
  408 +
  409 + </md-card-content>
  410 + </md-card>
  411 + </li>
  412 + </ol>
  413 + </div>
  414 + <div flex layout="row" layout-align="start center">
  415 + <md-button class="md-primary md-raised"
  416 + ng-click="addMap(broker.mapping)" aria-label="{{ 'action.add' | translate }}">
  417 + <md-icon class="material-icons">add</md-icon>
  418 + <span translate>extension.add-map</span>
  419 + </md-button>
  420 + </div>
  421 + </v-pane-content>
  422 + </v-pane>
  423 + </v-accordion>
  424 +
  425 + <v-accordion id="mqtt-connect-requests-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  426 + <v-pane id="mqtt-connect-requests-pane_{{brokerIndex}}">
  427 + <v-pane-header>
  428 + {{ 'extension.connect-requests' | translate }}
  429 + </v-pane-header>
  430 + <v-pane-content>
  431 + <div ng-if="broker.connectRequests.length > 0">
  432 + <ol class="list-group">
  433 + <li class="list-group-item" ng-repeat="(connectRequestIndex, connectRequest) in broker.connectRequests">
  434 + <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(connectRequest, broker.connectRequests)">
  435 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  436 + <md-tooltip md-direction="top">
  437 + {{ 'action.remove' | translate }}
  438 + </md-tooltip>
  439 + </md-button>
  440 + <md-card>
  441 + <md-card-content>
  442 + <md-input-container class="md-block">
  443 + <label translate>extension.topic-filter</label>
  444 + <input required name="conRequestTopicFilter_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.topicFilter">
  445 + <div ng-messages="theForm['conRequestTopicFilter_' + brokerIndex + connectRequestIndex].$error">
  446 + <div translate ng-message="required">extension.field-required</div>
  447 + </div>
  448 + </md-input-container>
  449 + <section flex layout="row">
  450 + <md-input-container flex="40" class="md-block" md-is-error="theForm['connectDeviceNameExpression_' + brokerIndex + connectRequestIndex].$touched && theForm['connectDeviceNameExpression_' + brokerIndex + connectRequestIndex].$invalid">
  451 + <label translate>extension.device-name-expression</label>
  452 + <md-select required name="connectDeviceNameExpression_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.nameExp" ng-change="changeNameExpression(connectRequest, 'connect')">
  453 + <md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
  454 + {{value | translate}}
  455 + </md-option>
  456 + </md-select>
  457 + <div ng-messages="theForm['connectDeviceNameExpression_' + brokerIndex + connectRequestIndex].$error">
  458 + <div translate ng-message="required">extension.field-required</div>
  459 + </div>
  460 + </md-input-container>
  461 + <md-input-container ng-if="connectRequest.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block">
  462 + <label translate>extension.json-name-expression</label>
  463 + <input required name="connectJsonNameExp_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.deviceNameJsonExpression">
  464 + <div ng-messages="theForm['connectJsonNameExp_' + brokerIndex + connectRequestIndex].$error">
  465 + <div translate ng-message="required">extension.field-required</div>
  466 + </div>
  467 + </md-input-container>
  468 + <md-input-container ng-if="connectRequest.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block">
  469 + <label translate>extension.topic-name-expression</label>
  470 + <input required name="connectTopicNameExp_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.deviceNameTopicExpression">
  471 + <div ng-messages="theForm['connectTopicNameExp_' + brokerIndex + connectRequestIndex].$error">
  472 + <div translate ng-message="required">extension.field-required</div>
  473 + </div>
  474 + </md-input-container>
  475 + </section>
  476 + </md-card-content>
  477 + </md-card>
  478 + </li>
  479 + </ol>
  480 + </div>
  481 + <div flex layout="row" layout-align="start center">
  482 + <md-button class="md-primary md-raised"
  483 + ng-click='addConnectRequest(broker.connectRequests, "connect")' aria-label="{{ 'action.add' | translate }}">
  484 + <md-icon class="material-icons">add</md-icon>
  485 + <span translate>extension.add-connect-request</span>
  486 + </md-button>
  487 + </div>
  488 + </v-pane-content>
  489 + </v-pane>
  490 + </v-accordion>
  491 +
  492 + <v-accordion id="mqtt-disconnect-requests-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  493 + <v-pane id="mqtt-disconnect-requests-pane_{{brokerIndex}}">
  494 + <v-pane-header>
  495 + {{ 'extension.disconnect-requests' | translate }}
  496 + </v-pane-header>
  497 + <v-pane-content>
  498 + <div ng-if="broker.disconnectRequests.length > 0">
  499 + <ol class="list-group">
  500 + <li class="list-group-item" ng-repeat="(disconnectRequestIndex, disconnectRequest) in broker.disconnectRequests">
  501 + <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(disconnectRequest, broker.disconnectRequests)">
  502 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  503 + <md-tooltip md-direction="top">
  504 + {{ 'action.remove' | translate }}
  505 + </md-tooltip>
  506 + </md-button>
  507 + <md-card>
  508 + <md-card-content>
  509 + <md-input-container class="md-block">
  510 + <label translate>extension.topic-filter</label>
  511 + <input required name="disconRequestTopicFilter_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.topicFilter">
  512 + <div ng-messages="theForm['disconRequestTopicFilter_' + brokerIndex + disconnectRequestIndex].$error">
  513 + <div translate ng-message="required">extension.field-required</div>
  514 + </div>
  515 + </md-input-container>
  516 + <section flex layout="row">
  517 + <md-input-container flex="40" class="md-block" md-is-error="theForm['disconnectDeviceNameExpression_' + brokerIndex + disconnectRequestIndex].$touched && theForm['disconnectDeviceNameExpression_' + brokerIndex + disconnectRequestIndex].$invalid">
  518 + <label translate>extension.device-name-expression</label>
  519 + <md-select required name="disconnectDeviceNameExpression_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.nameExp" ng-change="changeNameExpression(disconnectRequest, 'disconnect')">
  520 + <md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
  521 + {{value | translate}}
  522 + </md-option>
  523 + </md-select>
  524 + <div ng-messages="theForm['disconnectDeviceNameExpression_' + brokerIndex + disconnectRequestIndex].$error">
  525 + <div translate ng-message="required">extension.field-required</div>
  526 + </div>
  527 + </md-input-container>
  528 + <md-input-container ng-if="disconnectRequest.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block">
  529 + <label translate>extension.json-name-expression</label>
  530 + <input required name="disconnectJsonNameExp_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.deviceNameJsonExpression">
  531 + <div ng-messages="theForm['disconnectJsonNameExp_' + brokerIndex + disconnectRequestIndex].$error">
  532 + <div translate ng-message="required">extension.field-required</div>
  533 + </div>
  534 + </md-input-container>
  535 + <md-input-container ng-if="disconnectRequest.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block">
  536 + <label translate>extension.topic-name-expression</label>
  537 + <input required name="disconnectTopicNameExp_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.deviceNameTopicExpression">
  538 + <div ng-messages="theForm['disconnectTopicNameExp_' + brokerIndex + disconnectRequestIndex].$error">
  539 + <div translate ng-message="required">extension.field-required</div>
  540 + </div>
  541 + </md-input-container>
  542 + </section>
  543 + </md-card-content>
  544 + </md-card>
  545 + </li>
  546 + </ol>
  547 + </div>
  548 + <div flex layout="row" layout-align="start center">
  549 + <md-button class="md-primary md-raised"
  550 + ng-click='addConnectRequest(broker.disconnectRequests, "disconnect")' aria-label="{{ 'action.add' | translate }}">
  551 + <md-icon class="material-icons">add</md-icon>
  552 + <span translate>extension.add-disconnect-request</span>
  553 + </md-button>
  554 + </div>
  555 + </v-pane-content>
  556 + </v-pane>
  557 + </v-accordion>
  558 +
  559 + <v-accordion id="mqtt-attribute-requests-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  560 + <v-pane id="mqtt-attribute-requests-pane_{{brokerIndex}}">
  561 + <v-pane-header>
  562 + {{ 'extension.attribute-requests' | translate }}
  563 + </v-pane-header>
  564 + <v-pane-content>
  565 + <div ng-if="broker.attributeRequests.length > 0">
  566 + <ol class="list-group">
  567 + <li class="list-group-item" ng-repeat="(attributeRequestIndex, attributeRequest) in broker.attributeRequests">
  568 + <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attributeRequest, broker.attributeRequests)">
  569 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  570 + <md-tooltip md-direction="top">
  571 + {{ 'action.remove' | translate }}
  572 + </md-tooltip>
  573 + </md-button>
  574 + <md-card>
  575 + <md-card-content>
  576 + <section flex layout="row">
  577 + <md-input-container flex="80" class="md-block">
  578 + <label translate>extension.topic-filter</label>
  579 + <input required name="attributeRequestTopicFilter_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.topicFilter">
  580 + <div ng-messages="theForm['attributeRequestTopicFilter_' + brokerIndex + attributeRequestIndex].$error">
  581 + <div translate ng-message="required">extension.field-required</div>
  582 + </div>
  583 + </md-input-container>
  584 + <md-input-container flex="20" class="md-block">
  585 + <md-checkbox flex aria-label="{{ 'extension.client-scope' | translate }}"
  586 + ng-model="attributeRequest.clientScope">{{ 'extension.client-scope' | translate }}
  587 + </md-checkbox>
  588 + </md-input-container>
  589 + </section>
  590 + <section flex layout="row">
  591 + <md-input-container flex="40" class="md-block" md-is-error="theForm['attrRequestDeviceNameExpression_' + brokerIndex + attributeRequestIndex].$touched && theForm['attrRequestDeviceNameExpression_' + brokerIndex + attributeRequestIndex].$invalid">
  592 + <label translate>extension.device-name-expression</label>
  593 + <md-select required name="attrRequestDeviceNameExpression_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.nameExp" ng-change="changeNameExpression(attributeRequest, 'attribute')">
  594 + <md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
  595 + {{value | translate}}
  596 + </md-option>
  597 + </md-select>
  598 + <div ng-messages="theForm['attrRequestDeviceNameExpression_' + brokerIndex + attributeRequestIndex].$error">
  599 + <div translate ng-message="required">extension.field-required</div>
  600 + </div>
  601 + </md-input-container>
  602 + <md-input-container ng-if="attributeRequest.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block">
  603 + <label translate>extension.json-name-expression</label>
  604 + <input required name="attrRequestJsonNameExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.deviceNameJsonExpression">
  605 + <div ng-messages="theForm['attrRequestJsonNameExp_' + brokerIndex + attributeRequestIndex].$error">
  606 + <div translate ng-message="required">extension.field-required</div>
  607 + </div>
  608 + </md-input-container>
  609 + <md-input-container ng-if="attributeRequest.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block">
  610 + <label translate>extension.topic-name-expression</label>
  611 + <input required name="attrRequestTopicNameExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.deviceNameTopicExpression">
  612 + <div ng-messages="theForm['attrRequestTopicNameExp_' + brokerIndex + attributeRequestIndex].$error">
  613 + <div translate ng-message="required">extension.field-required</div>
  614 + </div>
  615 + </md-input-container>
  616 + </section>
  617 +
  618 + <section flex layout="row">
  619 + <md-input-container flex="40" class="md-block" md-is-error="theForm['attrRequestAttributeKeyExpression_' + brokerIndex + attributeRequestIndex].$touched && theForm['attrRequestAttributeKeyExpression_' + brokerIndex + attributeRequestIndex].$invalid">
  620 + <label translate>extension.attribute-key-expression</label>
  621 + <md-select required name="attrRequestAttributeKeyExpression_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.attrKey" ng-change="changeAttrKeyExpression(attributeRequest)">
  622 + <md-option ng-repeat="(key, value) in attributeKeyExpressions" ng-value='key'>
  623 + {{value | translate}}
  624 + </md-option>
  625 + </md-select>
  626 + <div ng-messages="theForm['attrRequestAttributeKeyExpression_' + brokerIndex + attributeRequestIndex].$error">
  627 + <div translate ng-message="required">extension.field-required</div>
  628 + </div>
  629 + </md-input-container>
  630 + <md-input-container ng-if="attributeRequest.attrKey == 'attributeKeyJsonExpression'" flex="60" class="md-block">
  631 + <label translate>extension.attr-json-key-expression</label>
  632 + <input required name="attrRequestJsonKeyExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.attributeKeyJsonExpression">
  633 + <div ng-messages="theForm['attrRequestJsonKeyExp_' + brokerIndex + attributeRequestIndex].$error">
  634 + <div translate ng-message="required">extension.field-required</div>
  635 + </div>
  636 + </md-input-container>
  637 + <md-input-container ng-if="attributeRequest.attrKey == 'attributeKeyTopicExpression'" flex="60" class="md-block">
  638 + <label translate>extension.attr-topic-key-expression</label>
  639 + <input required name="attrRequestTopicKeyExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.attributeKeyTopicExpression">
  640 + <div ng-messages="theForm['attrRequestTopicKeyExp_' + brokerIndex + attributeRequestIndex].$error">
  641 + <div translate ng-message="required">extension.field-required</div>
  642 + </div>
  643 + </md-input-container>
  644 + </section>
  645 +
  646 + <section flex layout="row">
  647 + <md-input-container flex="40" class="md-block" md-is-error="theForm['attrRequestIdExpression_' + brokerIndex + attributeRequestIndex].$touched && theForm['attrRequestIdExpression_' + brokerIndex + attributeRequestIndex].$invalid">
  648 + <label translate>extension.request-id-expression</label>
  649 + <md-select required name="attrRequestIdExpression_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.requestId" ng-change="changeRequestIdExpression(attributeRequest)">
  650 + <md-option ng-repeat="(key, value) in requestIdExpressions" ng-value='key'>
  651 + {{value | translate}}
  652 + </md-option>
  653 + </md-select>
  654 + <div ng-messages="theForm['attrRequestIdExpression_' + brokerIndex + attributeRequestIndex].$error">
  655 + <div translate ng-message="required">extension.field-required</div>
  656 + </div>
  657 + </md-input-container>
  658 + <md-input-container ng-if="attributeRequest.requestId == 'requestIdJsonExpression'" flex="60" class="md-block">
  659 + <label translate>extension.request-id-json-expression</label>
  660 + <input required name="attrRequestJsonIdExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.requestIdJsonExpression">
  661 + <div ng-messages="theForm['attrRequestJsonIdExp_' + brokerIndex + attributeRequestIndex].$error">
  662 + <div translate ng-message="required">extension.field-required</div>
  663 + </div>
  664 + </md-input-container>
  665 + <md-input-container ng-if="attributeRequest.requestId == 'requestIdTopicExpression'" flex="60" class="md-block">
  666 + <label translate>extension.request-id-topic-expression</label>
  667 + <input required name="attrRequestTopicIdExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.requestIdTopicExpression">
  668 + <div ng-messages="theForm['attrRequestTopicIdExp_' + brokerIndex + attributeRequestIndex].$error">
  669 + <div translate ng-message="required">extension.field-required</div>
  670 + </div>
  671 + </md-input-container>
  672 + </section>
  673 +
  674 + <md-input-container class="md-block">
  675 + <label translate>extension.response-topic-expression</label>
  676 + <input required name="attributeRequestResponseTopicExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.responseTopicExpression">
  677 + <div ng-messages="theForm['attributeRequestResponseTopicExp_' + brokerIndex + attributeRequestIndex].$error">
  678 + <div translate ng-message="required">extension.field-required</div>
  679 + </div>
  680 + </md-input-container>
  681 + <md-input-container class="md-block">
  682 + <label translate>extension.value-expression</label>
  683 + <input required name="attributeRequestValueExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.valueExpression">
  684 + <div ng-messages="theForm['attributeRequestValueExp_' + brokerIndex + attributeRequestIndex].$error">
  685 + <div translate ng-message="required">extension.field-required</div>
  686 + </div>
  687 + </md-input-container>
  688 + </md-card-content>
  689 + </md-card>
  690 + </li>
  691 + </ol>
  692 + </div>
  693 + <div flex layout="row" layout-align="start center">
  694 + <md-button class="md-primary md-raised"
  695 + ng-click="addAttributeRequest(broker.attributeRequests)" aria-label="{{ 'action.add' | translate }}">
  696 + <md-icon class="material-icons">add</md-icon>
  697 + <span translate>extension.add-attribute-request</span>
  698 + </md-button>
  699 + </div>
  700 + </v-pane-content>
  701 + </v-pane>
  702 + </v-accordion>
  703 +
  704 + <v-accordion id="mqtt-attribute-updates-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  705 + <v-pane id="mqtt-attribute-updates-pane_{{brokerIndex}}">
  706 + <v-pane-header>
  707 + {{ 'extension.attribute-updates' | translate }}
  708 + </v-pane-header>
  709 + <v-pane-content>
  710 + <div ng-if="broker.attributeUpdates.length > 0">
  711 + <ol class="list-group">
  712 + <li class="list-group-item" ng-repeat="(attributeUpdateIndex, attributeUpdate) in broker.attributeUpdates">
  713 + <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attributeUpdate, broker.attributeUpdates)">
  714 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  715 + <md-tooltip md-direction="top">
  716 + {{ 'action.remove' | translate }}
  717 + </md-tooltip>
  718 + </md-button>
  719 + <md-card>
  720 + <md-card-content>
  721 + <section flex layout="row">
  722 + <md-input-container flex="50" class="md-block">
  723 + <label translate>extension.device-name-filter</label>
  724 + <input required name="attributeUpdateDeviceNameFilter_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.deviceNameFilter">
  725 + <div ng-messages="theForm['attributeUpdateDeviceNameFilter_' + brokerIndex + attributeUpdateIndex].$error">
  726 + <div translate ng-message="required">extension.field-required</div>
  727 + </div>
  728 + </md-input-container>
  729 + <md-input-container flex="50" class="md-block">
  730 + <label translate>extension.attribute-filter</label>
  731 + <input required name="attributeUpdateAttributeFilter_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.attributeFilter">
  732 + <div ng-messages="theForm['attributeUpdateAttributeFilter_' + brokerIndex + attributeUpdateIndex].$error">
  733 + <div translate ng-message="required">extension.field-required</div>
  734 + </div>
  735 + </md-input-container>
  736 + </section>
  737 + <md-input-container class="md-block">
  738 + <label translate>extension.topic-expression</label>
  739 + <input required name="attributeUpdateTopicExp_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.topicExpression">
  740 + <div ng-messages="theForm['attributeUpdateTopicExp_' + brokerIndex + attributeUpdateIndex].$error">
  741 + <div translate ng-message="required">extension.field-required</div>
  742 + </div>
  743 + </md-input-container>
  744 + <md-input-container class="md-block">
  745 + <label translate>extension.value-expression</label>
  746 + <input required name="attributeUpdateValueExp_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.valueExpression">
  747 + <div ng-messages="theForm['attributeUpdateValueExp_' + brokerIndex + attributeUpdateIndex].$error">
  748 + <div translate ng-message="required">extension.field-required</div>
  749 + </div>
  750 + </md-input-container>
  751 + </md-card-content>
  752 + </md-card>
  753 + </li>
  754 + </ol>
  755 + </div>
  756 + <div flex layout="row" layout-align="start center">
  757 + <md-button class="md-primary md-raised"
  758 + ng-click='addAttributeUpdate(broker.attributeUpdates)' aria-label="{{ 'action.add' | translate }}">
  759 + <md-icon class="material-icons">add</md-icon>
  760 + <span translate>extension.add-attribute-update</span>
  761 + </md-button>
  762 + </div>
  763 + </v-pane-content>
  764 + </v-pane>
  765 + </v-accordion>
  766 +
  767 + <v-accordion id="mqtt-server-side-rpc-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  768 + <v-pane id="mqtt-server-side-rpc-pane_{{brokerIndex}}">
  769 + <v-pane-header>
  770 + {{ 'extension.server-side-rpc' | translate }}
  771 + </v-pane-header>
  772 + <v-pane-content>
  773 + <div ng-if="broker.serverSideRpc.length > 0">
  774 + <ol class="list-group">
  775 + <li class="list-group-item" ng-repeat="(rpcIndex, rpc) in broker.serverSideRpc">
  776 + <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(rpc, broker.serverSideRpc)">
  777 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  778 + <md-tooltip md-direction="top">
  779 + {{ 'action.remove' | translate }}
  780 + </md-tooltip>
  781 + </md-button>
  782 + <md-card>
  783 + <md-card-content>
  784 + <section flex layout="row">
  785 + <md-input-container flex="50" class="md-block">
  786 + <label translate>extension.device-name-filter</label>
  787 + <input required name="serverSideRpcDeviceNameFilter_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.deviceNameFilter">
  788 + <div ng-messages="theForm['serverSideRpcDeviceNameFilter_' + brokerIndex + rpcIndex].$error">
  789 + <div translate ng-message="required">extension.field-required</div>
  790 + </div>
  791 + </md-input-container>
  792 + <md-input-container flex="50" class="md-block">
  793 + <label translate>extension.method-filter</label>
  794 + <input required name="serverSideRpcMethodFilter_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.methodFilter">
  795 + <div ng-messages="theForm['serverSideRpcMethodFilter_' + brokerIndex + rpcIndex].$error">
  796 + <div translate ng-message="required">extension.field-required</div>
  797 + </div>
  798 + </md-input-container>
  799 + </section>
  800 + <md-input-container class="md-block">
  801 + <label translate>extension.request-topic-expression</label>
  802 + <input required name="serverSideRpcRequestTopicExp_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.requestTopicExpression">
  803 + <div ng-messages="theForm['serverSideRpcRequestTopicExp_' + brokerIndex + rpcIndex].$error">
  804 + <div translate ng-message="required">extension.field-required</div>
  805 + </div>
  806 + </md-input-container>
  807 + <md-input-container class="md-block">
  808 + <label translate>extension.response-topic-expression</label>
  809 + <input name="serverSideRpcResponseTopicExp_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.responseTopicExpression" parse-to-null>
  810 + </md-input-container>
  811 + <section flex layout="row">
  812 + <md-input-container flex="50" class="md-block">
  813 + <label translate>extension.response-timeout</label>
  814 + <input type="number" name="serverSideRpcResponseTimeout_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.responseTimeout" parse-to-null>
  815 + </md-input-container>
  816 + <md-input-container flex="50" class="md-block">
  817 + <label translate>extension.value-expression</label>
  818 + <input required name="serverSideRpcValueExp_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.valueExpression">
  819 + <div ng-messages="theForm['serverSideRpcValueExp_' + brokerIndex + rpcIndex].$error">
  820 + <div translate ng-message="required">extension.field-required</div>
  821 + </div>
  822 + </md-input-container>
  823 + </section>
  824 + </md-card-content>
  825 + </md-card>
  826 + </li>
  827 + </ol>
  828 + </div>
  829 + <div flex layout="row" layout-align="start center">
  830 + <md-button class="md-primary md-raised"
  831 + ng-click='addServerSideRpc(broker.serverSideRpc)' aria-label="{{ 'action.add' | translate }}">
  832 + <md-icon class="material-icons">add</md-icon>
  833 + <span translate>extension.add-server-side-rpc-request</span>
  834 + </md-button>
  835 + </div>
  836 + </v-pane-content>
  837 + </v-pane>
  838 + </v-accordion>
  839 +
  840 + </md-card-content>
  841 + </md-card>
  842 + </li>
  843 + </ol>
  844 + </div>
  845 +
  846 + <div flex layout="row" layout-align="start center">
  847 + <md-button class="md-primary md-raised"
  848 + ng-click="addBroker()" aria-label="{{ 'action.add' | translate }}">
  849 + <md-icon class="material-icons">add</md-icon>
  850 + <span translate>extension.add-broker</span>
  851 + </md-button>
  852 + </div>
  853 + </v-pane-content>
  854 + </v-pane>
  855 + </v-accordion>
  856 +<!--<pre>
  857 +{{config | json}}
  858 +</pre>-->
  859 + </md-card-content>
  860 +</md-card>
... ...
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import 'brace/ext/language_tools';
  18 +import 'brace/mode/json';
  19 +import 'brace/theme/github';
  20 +
  21 +import './extension-form.scss';
  22 +
  23 +/* eslint-disable angular/log */
  24 +
  25 +import extensionFormOpcTemplate from './extension-form-opc.tpl.html';
  26 +
  27 +/* eslint-enable import/no-unresolved, import/default */
  28 +
  29 +/*@ngInject*/
  30 +export default function ExtensionFormOpcDirective($compile, $templateCache, $translate, types) {
  31 +
  32 +
  33 + var linker = function(scope, element) {
  34 +
  35 +
  36 + function Server() {
  37 + this.applicationName = "Thingsboard OPC-UA client";
  38 + this.applicationUri = "";
  39 + this.host = "localhost";
  40 + this.port = 49320;
  41 + this.scanPeriodInSeconds = 10;
  42 + this.timeoutInMillis = 5000;
  43 + this.security = "Basic128Rsa15";
  44 + this.identity = {
  45 + "type": "anonymous"
  46 + };
  47 + this.keystore = {
  48 + "type": "PKCS12",
  49 + "location": "example.pfx",
  50 + "password": "secret",
  51 + "alias": "gateway",
  52 + "keyPassword": "secret"
  53 + };
  54 + this.mapping = []
  55 + }
  56 +
  57 + function Map() {
  58 + this.deviceNodePattern = "Channel1\\.Device\\d+$";
  59 + this.deviceNamePattern = "Device ${_System._DeviceId}";
  60 + this.attributes = [];
  61 + this.timeseries = [];
  62 + }
  63 +
  64 + function Attribute() {
  65 + this.key = "Tag1";
  66 + this.type = "string";
  67 + this.value = "${Tag1}";
  68 + }
  69 +
  70 + function Timeseries() {
  71 + this.key = "Tag2";
  72 + this.type = "long";
  73 + this.value = "${Tag2}";
  74 + }
  75 +
  76 +
  77 + var template = $templateCache.get(extensionFormOpcTemplate);
  78 + element.html(template);
  79 +
  80 + scope.types = types;
  81 + scope.theForm = scope.$parent.theForm;
  82 +
  83 +
  84 + if (!scope.configuration.servers.length) {
  85 + scope.configuration.servers.push(new Server());
  86 + }
  87 +
  88 + scope.addServer = function(serversList) {
  89 + serversList.push(new Server());
  90 + // scope.addMap(serversList[serversList.length-1].mapping);
  91 +
  92 + scope.theForm.$setDirty();
  93 + };
  94 +
  95 + scope.addMap = function(mappingList) {
  96 + mappingList.push(new Map());
  97 + scope.theForm.$setDirty();
  98 + };
  99 +
  100 + scope.addNewAttribute = function(attributesList) {
  101 + attributesList.push(new Attribute());
  102 + scope.theForm.$setDirty();
  103 + };
  104 +
  105 + scope.addNewTimeseries = function(timeseriesList) {
  106 + timeseriesList.push(new Timeseries());
  107 + scope.theForm.$setDirty();
  108 + };
  109 +
  110 +
  111 + scope.removeItem = (item, itemList) => {
  112 + var index = itemList.indexOf(item);
  113 + if (index > -1) {
  114 + itemList.splice(index, 1);
  115 + }
  116 + scope.theForm.$setDirty();
  117 + };
  118 +
  119 +
  120 + $compile(element.contents())(scope);
  121 +
  122 +
  123 + scope.fileAdded = function($file, model, options) {
  124 + let reader = new FileReader();
  125 + reader.onload = function(event) {
  126 + scope.$apply(function() {
  127 + if(event.target.result) {
  128 + scope.theForm.$setDirty();
  129 + let addedFile = event.target.result;
  130 +
  131 + if (addedFile && addedFile.length > 0) {
  132 + model[options.fileName] = $file.name;
  133 + model[options.file] = addedFile.replace(/^data.*base64,/, "");
  134 +
  135 + }
  136 + }
  137 + });
  138 + };
  139 + reader.readAsDataURL($file.file);
  140 +
  141 + };
  142 +
  143 + scope.clearFile = function(model, options) {
  144 + scope.theForm.$setDirty();
  145 +
  146 + model[options.fileName] = null;
  147 + model[options.file] = null;
  148 +
  149 + };
  150 +
  151 + scope.collapseValidation = function(index, id) {
  152 + var invalidState = angular.element('#'+id+':has(.ng-invalid)');
  153 + if(invalidState.length) {
  154 + invalidState.addClass('inner-invalid');
  155 + }
  156 + };
  157 +
  158 + scope.expandValidation = function (index, id) {
  159 + var invalidState = angular.element('#'+id);
  160 + invalidState.removeClass('inner-invalid');
  161 + };
  162 +
  163 + };
  164 +
  165 + return {
  166 + restrict: "A",
  167 + link: linker,
  168 + scope: {
  169 + configuration: "=",
  170 + isAdd: "="
  171 + }
  172 + }
  173 +}
\ No newline at end of file
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-card class="extension-form extension-opc">
  19 + <md-card-title>
  20 + <md-card-title-text>
  21 + <span translate class="md-headline">extension.configuration</span>
  22 + </md-card-title-text>
  23 + </md-card-title>
  24 +
  25 + <md-card-content>
  26 + <v-accordion id="opc-server-configs-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  27 + <v-pane id="opc-servers-pane" expanded="true">
  28 + <v-pane-header>
  29 + {{ 'extension.opc-server' | translate }}
  30 + </v-pane-header>
  31 +
  32 + <v-pane-content>
  33 + <div ng-if="configuration.servers.length === 0">
  34 + <span translate layout-align="center center" class="tb-prompt">extension.opc-add-server-prompt</span>
  35 + </div>
  36 +
  37 + <div ng-if="configuration.servers.length > 0">
  38 + <ol class="list-group">
  39 + <li class="list-group-item" ng-repeat="(serverIndex, server) in configuration.servers">
  40 + <md-button aria-label="{{ 'action.remove' | translate }}"
  41 + class="md-icon-button"
  42 + ng-click="removeItem(server, configuration.servers)"
  43 + ng-hide="configuration.servers.length < 2"
  44 + >
  45 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  46 + <md-tooltip md-direction="top">
  47 + {{ 'action.remove' | translate }}
  48 + </md-tooltip>
  49 + </md-button>
  50 +
  51 + <md-card>
  52 + <md-card-content>
  53 +
  54 + <div layout="row">
  55 + <md-input-container flex="50" class="md-block">
  56 + <label translate>extension.opc-application-name</label>
  57 + <input required name="applicationName_{{serverIndex}}" ng-model="server.applicationName">
  58 + <div ng-messages="theForm['applicationName_' + serverIndex].$error">
  59 + <div translate ng-message="required">extension.field-required</div>
  60 + </div>
  61 + </md-input-container>
  62 +
  63 +
  64 + <md-input-container flex="50" class="md-block" md-is-error="theForm['applicationUri_' + serverIndex].$touched && theForm['applicationUri_' + serverIndex].$invalid">
  65 + <label translate>extension.opc-application-uri</label>
  66 + <input required name="applicationUri_{{serverIndex}}" ng-model="server.applicationUri">
  67 + <div ng-messages="theForm['applicationUri_' + serverIndex].$error">
  68 + <div translate ng-message="required">extension.field-required</div>
  69 + </div>
  70 + </md-input-container>
  71 + </div>
  72 +
  73 +
  74 + <div layout="row">
  75 + <md-input-container flex="50" class="md-block">
  76 + <label translate>extension.host</label>
  77 + <input required name="host_{{serverIndex}}" ng-model="server.host">
  78 + <div ng-messages="theForm['host_' + serverIndex].$error">
  79 + <div translate ng-message="required">extension.field-required</div>
  80 + </div>
  81 + </md-input-container>
  82 +
  83 + <md-input-container flex="50" class="md-block">
  84 + <label translate>extension.port</label>
  85 + <input type="number"
  86 + required
  87 + name="port_{{serverIndex}}"
  88 + ng-model="server.port"
  89 + min="1"
  90 + max="65535"
  91 + >
  92 + <div ng-messages="theForm['port_' + serverIndex].$error">
  93 + <div translate
  94 + ng-message="required"
  95 + >extension.field-required</div>
  96 + <div translate
  97 + ng-message="min"
  98 + >extension.port-range</div>
  99 + <div translate
  100 + ng-message="max"
  101 + >extension.port-range</div>
  102 + </div>
  103 + </md-input-container>
  104 + </div>
  105 +
  106 + <div layout="row">
  107 + <md-input-container flex="50" class="md-block">
  108 + <label translate>extension.opc-scan-period-in-seconds</label>
  109 + <input type="number"
  110 + required
  111 + name="scanPeriodInSeconds_{{serverIndex}}"
  112 + ng-model="server.scanPeriodInSeconds">
  113 + <div ng-messages="theForm['scanPeriodInSeconds_' + serverIndex].$error">
  114 + <div translate
  115 + ng-message="required"
  116 + >extension.field-required</div>
  117 + </div>
  118 + </md-input-container>
  119 +
  120 + <md-input-container flex="50" class="md-block">
  121 + <label translate>extension.timeout</label>
  122 + <input type="number"
  123 + required name="timeoutInMillis_{{serverIndex}}"
  124 + ng-model="server.timeoutInMillis"
  125 + >
  126 + <div ng-messages="theForm['timeoutInMillis_' + serverIndex].$error">
  127 + <div translate
  128 + ng-message="required"
  129 + >extension.field-required</div>
  130 + </div>
  131 + </md-input-container>
  132 + </div>
  133 +
  134 + <div layout="row">
  135 +
  136 + <md-input-container flex="50" class="md-block tb-container-for-select">
  137 + <label translate>extension.opc-security</label>
  138 + <md-select required
  139 + name="securityType_{{serverIndex}}"
  140 + ng-model="server.security">
  141 + <md-option ng-value="securityType"
  142 + ng-repeat="(securityType, securityValue) in types.extensionOpcSecurityTypes"
  143 + ><span ng-bind="::securityValue"></span></md-option>
  144 + </md-select>
  145 + <div ng-messages="theForm['securityType_' + serverIndex].$error">
  146 + <div translate
  147 + ng-message="required"
  148 + >extension.field-required</div>
  149 + </div>
  150 + </md-input-container>
  151 +
  152 + <md-input-container flex="50" class="md-block tb-container-for-select">
  153 + <label translate>extension.opc-identity</label>
  154 + <md-select required
  155 + name="identityType_{{serverIndex}}"
  156 + ng-model="server.identity.type"
  157 + >
  158 + <md-option ng-value="identityType"
  159 + ng-repeat="(identityType, identityValue) in types.extensionIdentityType"
  160 + ><span ng-bind="identityValue | translate"></span></md-option>
  161 + </md-select>
  162 + <div ng-messages="theForm['identityType_' + serverIndex].$error">
  163 + <div translate
  164 + ng-message="required"
  165 + >extension.field-required</div>
  166 + </div>
  167 + </md-input-container>
  168 + </div>
  169 +
  170 + <div ng-if="server.identity.type != 'username'">
  171 + <span class=""
  172 + ng-init="server.identity = {'type':'anonymous'}"></span>
  173 + </div>
  174 + <div layout="row" ng-if="server.identity.type == 'username'">
  175 + <md-input-container flex="50" class="md-block" md-is-error="theForm['identityUsername_' + serverIndex].$touched && theForm['identityUsername_' + serverIndex].$invalid">
  176 + <label translate>extension.username</label>
  177 + <input required
  178 + name="identityUsername_{{serverIndex}}"
  179 + ng-model="server.identity.username"
  180 + >
  181 + <div ng-messages="theForm['identityUsername_' + serverIndex].$error">
  182 + <div translate
  183 + ng-message="required"
  184 + >extension.field-required</div>
  185 + </div>
  186 + </md-input-container>
  187 +
  188 + <md-input-container flex="50" class="md-block" md-is-error="theForm['identityPassword_' + serverIndex].$touched && theForm['identityPassword_' + serverIndex].$invalid">
  189 + <label translate>extension.password</label>
  190 + <input required
  191 + name="identityPassword_{{serverIndex}}" ng-model="server.identity.password">
  192 + <div ng-messages="theForm['identityPassword_' + serverIndex].$error">
  193 + <div translate
  194 + ng-message="required"
  195 + >extension.field-required</div>
  196 + </div>
  197 + </md-input-container>
  198 + </div>
  199 +
  200 + <v-accordion id="opc-keystore-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  201 + <v-pane id="opc-keystore-pane__{{serverIndex}}" expanded="true">
  202 + <v-pane-header>
  203 + {{ 'extension.opc-keystore' | translate }}
  204 + </v-pane-header>
  205 + <v-pane-content>
  206 +
  207 + <md-input-container class="md-block tb-container-for-select">
  208 + <label translate>extension.opc-keystore-type</label>
  209 + <md-select required name="keystoreType_{{serverIndex}}" ng-model="server.keystore.type">
  210 + <md-option ng-value="keystoreType" ng-repeat="(keystoreType, keystoreValue) in types.extensionKeystoreType"><span ng-bind="::keystoreValue"></span></md-option>
  211 + </md-select>
  212 + <div ng-messages="theForm['keystoreType_'+serverIndex].$error">
  213 + <div translate ng-message="required">extension.field-required</div>
  214 + </div>
  215 + </md-input-container>
  216 +
  217 + <section class="dropdown-section">
  218 + <div class="tb-container" ng-class="{'ng-invalid':!server.keystore.file}">
  219 + <span ng-init='fieldsToFill = {"fileName":"fileName", "file":"file"}'></span>
  220 + <label class="tb-label" translate>extension.opc-keystore-location</label>
  221 + <div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, server.keystore, fieldsToFill)' class="tb-file-select-container">
  222 + <div class="tb-file-clear-container">
  223 + <md-button ng-click='clearFile(server.keystore, fieldsToFill)' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
  224 + <md-tooltip md-direction="top">
  225 + {{ 'action.remove' | translate }}
  226 + </md-tooltip>
  227 + <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
  228 + </md-button>
  229 + </div>
  230 + <div class="alert tb-flow-drop" flow-drop>
  231 + <label for="dropFileKeystore_{{serverIndex}}" translate>extension.drop-file</label>
  232 + <input flow-attrs="{accept:'.pfx,.p12'}"
  233 + type="file"
  234 + class="file-input"
  235 + flow-btn id="dropFileKeystore_{{serverIndex}}"
  236 + name="keystoreFile"
  237 + ng-model="server.keystore.file"
  238 + >
  239 + </div>
  240 + </div>
  241 + </div>
  242 + <div class="dropdown-messages">
  243 + <div ng-if="!server.keystore[fieldsToFill.fileName]" class="tb-error-message" translate>extension.no-file</div>
  244 + <div ng-if="server.keystore[fieldsToFill.fileName]">{{server.keystore[fieldsToFill.fileName]}}</div>
  245 + </div>
  246 + </section>
  247 +
  248 +
  249 + <div flex layout="row">
  250 + <md-input-container flex="50" class="md-block">
  251 + <label translate>extension.opc-keystore-password</label>
  252 + <input required name="keystorePassword_{{serverIndex}}" ng-model="server.keystore.password">
  253 + <div ng-messages="theForm['keystorePassword_' + serverIndex].$error">
  254 + <div translate ng-message="required">extension.field-required</div>
  255 + </div>
  256 + </md-input-container>
  257 +
  258 + <md-input-container flex="50" class="md-block">
  259 + <label translate>extension.opc-keystore-alias</label>
  260 + <input required name="keystoreAlias_{{serverIndex}}" ng-model="server.keystore.alias">
  261 + <div ng-messages="theForm['keystoreAlias_' + serverIndex].$error">
  262 + <div translate ng-message="required">extension.field-required</div>
  263 + </div>
  264 + </md-input-container>
  265 + </div>
  266 +
  267 + <md-input-container class="md-block">
  268 + <label translate>extension.opc-keystore-key-password</label>
  269 + <input required name="keystoreKeyPassword_{{serverIndex}}" ng-model="server.keystore.keyPassword">
  270 + <div ng-messages="theForm['keystoreKeyPassword_' + serverIndex].$error">
  271 + <div translate ng-message="required">extension.field-required</div>
  272 + </div>
  273 + </md-input-container>
  274 +
  275 + </v-pane-content>
  276 + </v-pane>
  277 + </v-accordion>
  278 +
  279 +
  280 + <v-accordion id="opc-mapping-accordion"
  281 + class="vAccordion--default"
  282 + onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  283 + <v-pane id="opc-mapping-pane_{{serverIndex}}">
  284 + <v-pane-header>
  285 + {{ 'extension.mapping' | translate }}
  286 + </v-pane-header>
  287 + <v-pane-content>
  288 + <div ng-if="server.mapping.length > 0">
  289 + <ol class="list-group">
  290 + <li class="list-group-item"
  291 + ng-repeat="(mapIndex, map) in server.mapping"
  292 + >
  293 + <md-button aria-label="{{ 'action.remove' | translate }}"
  294 + class="md-icon-button"
  295 + ng-click="removeItem(map, server.mapping)"
  296 + >
  297 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  298 + <md-tooltip md-direction="top">
  299 + {{ 'action.remove' | translate }}
  300 + </md-tooltip>
  301 + </md-button>
  302 +
  303 + <md-card>
  304 + <md-card-content>
  305 + <div flex layout="row">
  306 + <md-input-container flex="50" class="md-block">
  307 + <label translate>extension.opc-device-node-pattern</label>
  308 + <input required
  309 + name="deviceNodePattern_{{serverIndex}}{{mapIndex}}"
  310 + ng-model="map.deviceNodePattern"
  311 + >
  312 + <div ng-messages="theForm['deviceNodePattern_' + serverIndex + mapIndex].$error">
  313 + <div translate
  314 + ng-message="required"
  315 + >extension.field-required</div>
  316 + </div>
  317 + </md-input-container>
  318 +
  319 + <md-input-container flex="50" class="md-block">
  320 + <label translate>extension.opc-device-name-pattern</label>
  321 + <input required
  322 + name="deviceNamePattern_{{serverIndex}}{{mapIndex}}"
  323 + ng-model="map.deviceNamePattern"
  324 + >
  325 + <div ng-messages="theForm['deviceNamePattern_' + serverIndex + mapIndex].$error">
  326 + <div translate
  327 + ng-message="required"
  328 + >extension.field-required</div>
  329 + </div>
  330 + </md-input-container>
  331 + </div>
  332 +
  333 +
  334 + <v-accordion id="opc-attributes-accordion"
  335 + class="vAccordion--default"
  336 + onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  337 + <v-pane id="opc-attributes-pane_{{serverIndex}}{{mapIndex}}">
  338 + <v-pane-header>
  339 + {{ 'extension.attributes' | translate }}
  340 + </v-pane-header>
  341 + <v-pane-content>
  342 + <div ng-show="map.attributes.length > 0">
  343 + <ol class="list-group">
  344 + <li class="list-group-item"
  345 + ng-repeat="(attributeIndex, attribute) in map.attributes"
  346 + >
  347 + <md-button aria-label="{{ 'action.remove' | translate }}"
  348 + class="md-icon-button"
  349 + ng-click="removeItem(attribute, map.attributes)">
  350 + <ng-md-icon icon="close"
  351 + aria-label="{{ 'action.remove' | translate }}"
  352 + ></ng-md-icon>
  353 + <md-tooltip md-direction="top">
  354 + {{ 'action.remove' | translate }}
  355 + </md-tooltip>
  356 + </md-button>
  357 + <md-card>
  358 + <md-card-content>
  359 +
  360 + <section flex
  361 + layout="row"
  362 + >
  363 + <md-input-container flex="60" class="md-block">
  364 + <label translate>extension.key</label>
  365 + <input required
  366 + name="opcAttributeKey_{{serverIndex}}{{mapIndex}}{{attributeIndex}}"
  367 + ng-model="attribute.key"
  368 + >
  369 + <div ng-messages="theForm['opcAttributeKey_' + serverIndex + mapIndex + attributeIndex].$error">
  370 + <div translate
  371 + ng-message="required"
  372 + >extension.field-required</div>
  373 + </div>
  374 + </md-input-container>
  375 + <md-input-container flex="40" class="md-block tb-container-for-select">
  376 + <label translate>extension.type</label>
  377 + <md-select required name="opcAttributeType_{{serverIndex}}{{mapIndex}}{{attributeIndex}}"
  378 + ng-model="attribute.type"
  379 + >
  380 + <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType"
  381 + ng-value="attrType"
  382 + >
  383 + {{attrTypeValue | translate}}
  384 + </md-option>
  385 + </md-select>
  386 + <div ng-messages="theForm['opcAttributeType_' + serverIndex + mapIndex + attributeIndex].$error">
  387 + <div translate
  388 + ng-message="required"
  389 + >extension.field-required</div>
  390 + </div>
  391 + </md-input-container>
  392 + </section>
  393 +
  394 + <section flex layout="row">
  395 + <md-input-container flex="100" class="md-block">
  396 + <label translate>extension.value</label>
  397 + <input required name="opcAttributeValue_{{serverIndex}}{{mapIndex}}{{attributeIndex}}"
  398 + ng-model="attribute.value"
  399 + >
  400 + <div ng-messages="theForm['opcAttributeValue_' + serverIndex + mapIndex + attributeIndex].$error">
  401 + <div translate
  402 + ng-message="required"
  403 + >extension.field-required</div>
  404 + </div>
  405 + </md-input-container>
  406 +
  407 + </section>
  408 +
  409 +
  410 + </md-card-content>
  411 + </md-card>
  412 + </li>
  413 + </ol>
  414 + </div>
  415 + <div flex layout="row" layout-align="start center">
  416 + <md-button class="md-primary md-raised"
  417 + ng-click="addNewAttribute(map.attributes)"
  418 + aria-label="{{ 'action.add' | translate }}"
  419 + >
  420 + <md-icon class="material-icons">add</md-icon>
  421 + <span translate>extension.add-attribute</span>
  422 + </md-button>
  423 + </div>
  424 + </v-pane-content>
  425 + </v-pane>
  426 + </v-accordion>
  427 +
  428 + <v-accordion id="opc-timeseries-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  429 + <v-pane id="opc-timeseries-pane_{{serverIndex}}{{mapIndex}}">
  430 + <v-pane-header>
  431 + {{ 'extension.timeseries' | translate }}
  432 + </v-pane-header>
  433 + <v-pane-content>
  434 + <div ng-show="map.timeseries.length > 0">
  435 + <ol class="list-group">
  436 + <li class="list-group-item"
  437 + ng-repeat="(timeseriesIndex, timeseries) in map.timeseries"
  438 + >
  439 + <md-button aria-label="{{ 'action.remove' | translate }}"
  440 + class="md-icon-button"
  441 + ng-click="removeItem(timeseries, map.timeseries)"
  442 + >
  443 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  444 + <md-tooltip md-direction="top">
  445 + {{ 'action.remove' | translate }}
  446 + </md-tooltip>
  447 + </md-button>
  448 + <md-card>
  449 + <md-card-content>
  450 + <section flex layout="row">
  451 + <md-input-container flex="60" class="md-block">
  452 + <label translate>extension.key</label>
  453 + <input required
  454 + name="opcTimeseriesKey_{{serverIndex}}{{mapIndex}}{{timeseriesIndex}}"
  455 + ng-model="timeseries.key"
  456 + >
  457 + <div ng-messages="theForm['opcTimeseriesKey_' + serverIndex + mapIndex + timeseriesIndex].$error">
  458 + <div translate
  459 + ng-message="required"
  460 + >extension.field-required</div>
  461 + </div>
  462 + </md-input-container>
  463 + <md-input-container flex="40"
  464 + class="md-block tb-container-for-select"
  465 + >
  466 + <label translate>extension.type</label>
  467 + <md-select required
  468 + name="opcTimeseriesType_{{serverIndex}}{{mapIndex}}{{timeseriesIndex}}"
  469 + ng-model="timeseries.type"
  470 + >
  471 + <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType"
  472 + ng-value="attrType"
  473 + >
  474 + {{attrTypeValue | translate}}
  475 + </md-option>
  476 + </md-select>
  477 + <div ng-messages="theForm['opcTimeseriesType_' + serverIndex + mapIndex + timeseriesIndex].$error">
  478 + <div translate
  479 + ng-message="required"
  480 + >extension.field-required</div>
  481 + </div>
  482 + </md-input-container>
  483 + </section>
  484 + <section flex layout="row">
  485 + <md-input-container flex="100" class="md-block">
  486 + <label translate>extension.value</label>
  487 + <input required name="opcTimeseriesValue_{{serverIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.value">
  488 + <div ng-messages="theForm['opcTimeseriesValue_' + serverIndex + mapIndex + timeseriesIndex].$error">
  489 + <div translate ng-message="required">extension.field-required</div>
  490 + </div>
  491 + </md-input-container>
  492 + </section>
  493 + </md-card-content>
  494 + </md-card>
  495 + </li>
  496 + </ol>
  497 + </div>
  498 + <div flex layout="row" layout-align="start center">
  499 + <md-button class="md-primary md-raised"
  500 + ng-click="addNewAttribute(map.timeseries)"
  501 + aria-label="{{ 'action.add' | translate }}"
  502 + >
  503 + <md-icon class="material-icons">add</md-icon>
  504 + <span translate>extension.add-timeseries</span>
  505 + </md-button>
  506 + </div>
  507 + </v-pane-content>
  508 + </v-pane>
  509 + </v-accordion>
  510 +
  511 +
  512 + </md-card-content>
  513 + </md-card>
  514 + </li>
  515 + </ol>
  516 + </div>
  517 + <div flex
  518 + layout="row"
  519 + layout-align="start center"
  520 + >
  521 + <md-button class="md-primary md-raised"
  522 + ng-click="addMap(server.mapping)"
  523 + aria-label="{{ 'action.add' | translate }}"
  524 + >
  525 + <md-icon class="material-icons">add</md-icon>
  526 + <span translate>extension.add-map</span>
  527 + </md-button>
  528 + </div>
  529 + </v-pane-content>
  530 + </v-pane>
  531 + </v-accordion>
  532 +
  533 + </md-card-content>
  534 + </md-card>
  535 + </li>
  536 + </ol>
  537 +
  538 + <div flex
  539 + layout="row"
  540 + layout-align="start center"
  541 + >
  542 + <md-button class="md-primary md-raised"
  543 + ng-click="addServer(configuration.servers)"
  544 + aria-label="{{ 'action.add' | translate }}"
  545 + >
  546 + <md-icon class="material-icons">add</md-icon>
  547 + <span translate>extension.opc-add-server</span>
  548 + </md-button>
  549 + </div>
  550 +
  551 + </div>
  552 + </v-pane-content>
  553 + </v-pane>
  554 + </v-accordion>
  555 + <!--{{config}}-->
  556 + </md-card-content>
  557 +</md-card>
\ No newline at end of file
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +.extension-form {
  17 + li > .md-button {
  18 + color: rgba(0, 0, 0, 0.7);
  19 + margin: 0;
  20 + }
  21 + .vAccordion--default {
  22 + margin-top: 0;
  23 + padding-left: 3px;
  24 + }
  25 + .tb-container {
  26 + width:100%;
  27 + }
  28 + .dropdown-messages {
  29 + .tb-error-message {
  30 + padding: 5px 0 0 0;
  31 + }
  32 + }
  33 + .dropdown-section {
  34 + margin-bottom: 30px;
  35 + }
  36 + v-pane.inner-invalid > v-pane-header {
  37 + border-bottom: 2px solid rgb(221,44,0);
  38 + }
  39 +}
  40 +
  41 +.extension-form.extension-mqtt {
  42 + md-checkbox{
  43 + margin-left: 10px;
  44 + }
  45 +}
  46 +
  47 +.tb-extension-custom-transformer-panel {
  48 + margin-left: 15px;
  49 + border: 1px solid #C0C0C0;
  50 + height: 100%;
  51 + .tb-extension-custom-transformer {
  52 + min-width: 600px;
  53 + min-height: 200px;
  54 + width: 100%;
  55 + height: 100%;
  56 + }
  57 + .ace_text-input {
  58 + position:absolute!important
  59 + }
  60 +}
  61 +
  62 +.extensionDialog {
  63 + min-width: 1000px;
  64 +}
  65 +
  66 +.tb-container-for-select {
  67 + height: 58px;
  68 +}
  69 +
  70 +.tb-drop-file-input-hide {
  71 + height: 200%;
  72 + display: block;
  73 + position: absolute;
  74 + bottom: 0;
  75 + width: 100%;
  76 +}
\ No newline at end of file
... ...
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import ExtensionTableDirective from './extension-table.directive';
  18 +import ExtensionFormHttpDirective from './extensions-forms/extension-form-http.directive';
  19 +import ExtensionFormMqttDirective from './extensions-forms/extension-form-mqtt.directive'
  20 +import ExtensionFormOpcDirective from './extensions-forms/extension-form-opc.directive';
  21 +import {ParseToNull} from './extension-dialog.controller';
  22 +
  23 +export default angular.module('thingsboard.extension', [])
  24 + .directive('tbExtensionTable', ExtensionTableDirective)
  25 + .directive('tbExtensionFormHttp', ExtensionFormHttpDirective)
  26 + .directive('tbExtensionFormMqtt', ExtensionFormMqttDirective)
  27 + .directive('tbExtensionFormOpc', ExtensionFormOpcDirective)
  28 + .directive('parseToNull', ParseToNull)
  29 + .name;
\ No newline at end of file
... ...
... ... @@ -13,8 +13,44 @@
13 13 * See the License for the specific language governing permissions and
14 14 * limitations under the License.
15 15 */
  16 +
  17 +import './home-links.scss';
  18 +
16 19 /*@ngInject*/
17   -export default function HomeLinksController($scope, menu) {
  20 +export default function HomeLinksController($scope, $mdMedia, menu) {
  21 +
18 22 var vm = this;
  23 +
  24 + vm.sectionColspan = sectionColspan;
  25 +
  26 + $scope.$watch(function() { return $mdMedia('lg'); }, function() {
  27 + updateColumnCount();
  28 + });
  29 +
  30 + $scope.$watch(function() { return $mdMedia('gt-lg'); }, function() {
  31 + updateColumnCount();
  32 + });
  33 +
  34 + updateColumnCount();
  35 +
19 36 vm.model = menu.getHomeSections();
  37 +
  38 + function updateColumnCount() {
  39 + vm.cols = 2;
  40 + if ($mdMedia('lg')) {
  41 + vm.cols = 3;
  42 + }
  43 + if ($mdMedia('gt-lg')) {
  44 + vm.cols = 4;
  45 + }
  46 + }
  47 +
  48 + function sectionColspan(section) {
  49 + var colspan = vm.cols;
  50 + if (section && section.places && section.places.length <= colspan) {
  51 + colspan = section.places.length;
  52 + }
  53 + return colspan;
  54 + }
  55 +
20 56 }
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +@import "../../scss/constants";
  18 +
  19 +.tb-home-links {
  20 + .md-headline {
  21 + font-size: 20px;
  22 + @media (min-width: $layout-breakpoint-xmd) {
  23 + font-size: 24px;
  24 + }
  25 + }
  26 +}
\ No newline at end of file
... ...
... ... @@ -15,8 +15,8 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<md-grid-list md-cols="2" md-cols-gt-sm="4" md-row-height="280px">
19   - <md-grid-tile md-colspan="2" md-colspan-gt-sm="{{section.places.length}}" ng-repeat="section in vm.model">
  18 +<md-grid-list class="tb-home-links" md-cols="{{vm.cols}}" md-row-height="280px">
  19 + <md-grid-tile md-colspan="2" md-colspan-gt-sm="{{vm.sectionColspan(section)}}" ng-repeat="section in vm.model">
20 20 <md-card style='width: 100%;'>
21 21 <md-card-title>
22 22 <md-card-title-text>
... ... @@ -25,12 +25,12 @@
25 25 </md-card-title>
26 26 <md-card-content>
27 27 <md-grid-list md-row-height="170px" md-cols="{{section.places.length}}" md-cols-gt-md="{{section.places.length}}">
28   - <md-grid-tile class="card-tile" ng-repeat="place in section.places">
29   - <md-button class="tb-card-button md-raised md-primary" layout="column" ui-sref="{{place.state}}">
30   - <md-icon class="material-icons tb-md-96" aria-label="{{place.icon}}">{{place.icon}}</md-icon>
31   - <span translate>{{place.name}}</span>
32   - </md-button>
33   - </md-grid-tile>
  28 + <md-grid-tile class="card-tile" ng-repeat="place in section.places">
  29 + <md-button class="tb-card-button md-raised md-primary" layout="column" ui-sref="{{place.state}}">
  30 + <md-icon class="material-icons tb-md-96" aria-label="{{place.icon}}">{{place.icon}}</md-icon>
  31 + <span translate>{{place.name}}</span>
  32 + </md-button>
  33 + </md-grid-tile>
34 34 </md-grid-list>
35 35 </md-card-content>
36 36 </md-card>
... ...
... ... @@ -24,8 +24,9 @@ import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
24 24 /* eslint-disable no-undef, angular/window-service, angular/document-service */
25 25
26 26 /*@ngInject*/
27   -export default function ImportExport($log, $translate, $q, $mdDialog, $document, itembuffer, utils, types, dashboardUtils,
28   - entityService, dashboardService, pluginService, ruleService, widgetService, toast) {
  27 +export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types,
  28 + dashboardUtils, entityService, dashboardService, pluginService, ruleService,
  29 + widgetService, toast, attributeService) {
29 30
30 31
31 32 var service = {
... ... @@ -40,8 +41,11 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
40 41 exportWidgetType: exportWidgetType,
41 42 importWidgetType: importWidgetType,
42 43 exportWidgetsBundle: exportWidgetsBundle,
43   - importWidgetsBundle: importWidgetsBundle
44   - }
  44 + importWidgetsBundle: importWidgetsBundle,
  45 + exportExtension: exportExtension,
  46 + importExtension: importExtension,
  47 + exportToPc: exportToPc
  48 + };
45 49
46 50 return service;
47 51
... ... @@ -614,6 +618,84 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
614 618 return true;
615 619 }
616 620
  621 +
  622 +
  623 + function exportExtension(extensionId) {
  624 +
  625 + getExtension(extensionId)
  626 + .then(
  627 + function success(extension) {
  628 + var name = extension.title;
  629 + name = name.toLowerCase().replace(/\W/g,"_");
  630 + exportToPc(prepareExport(extension), name + '.json');
  631 + },
  632 + function fail(rejection) {
  633 + var message = rejection;
  634 + if (!message) {
  635 + message = $translate.instant('error.unknown-error');
  636 + }
  637 + toast.showError($translate.instant('extension.export-failed-error', {error: message}));
  638 + }
  639 + );
  640 +
  641 + function getExtension(extensionId) {
  642 + var deferred = $q.defer();
  643 + var url = '/api/plugins/telemetry/DEVICE/' + extensionId;
  644 + $http.get(url, null)
  645 + .then(function success(response) {
  646 + deferred.resolve(response.data);
  647 + }, function fail() {
  648 + deferred.reject();
  649 + });
  650 + return deferred.promise;
  651 + }
  652 +
  653 + }
  654 +
  655 + function importExtension($event, options) {
  656 + var deferred = $q.defer();
  657 + openImportDialog($event, 'extension.import-extensions', 'extension.file')
  658 + .then(
  659 + function success(extension) {
  660 + if (!validateImportedExtension(extension)) {
  661 + toast.showError($translate.instant('extension.invalid-file-error'));
  662 + deferred.reject();
  663 + } else {
  664 + attributeService
  665 + .saveEntityAttributes(
  666 + options.entityType,
  667 + options.entityId,
  668 + types.attributesScope.shared.value,
  669 + [{
  670 + key: "configuration",
  671 + value: angular.toJson(extension)
  672 + }]
  673 + )
  674 + .then(function success() {
  675 + options.successFunc();
  676 + });
  677 + }
  678 + },
  679 + function fail() {
  680 + deferred.reject();
  681 + }
  682 + );
  683 + return deferred.promise;
  684 + }
  685 +
  686 + function validateImportedExtension(configuration) {
  687 + if (configuration.length) {
  688 + for (let i = 0; i < configuration.length; i++) {
  689 + if (angular.isUndefined(configuration[i].configuration) || angular.isUndefined(configuration[i].id )|| angular.isUndefined(configuration[i].type)) {
  690 + return false;
  691 + }
  692 + }
  693 + } else {
  694 + return false;
  695 + }
  696 + return true;
  697 + }
  698 +
617 699 function processEntityAliases(entityAliases, aliasIds) {
618 700 var deferred = $q.defer();
619 701 var missingEntityAliases = {};
... ...
... ... @@ -35,6 +35,7 @@ import thingsboardUserMenu from './user-menu.directive';
35 35 import thingsboardEntity from '../entity';
36 36 import thingsboardEvent from '../event';
37 37 import thingsboardAlarm from '../alarm';
  38 +import thingsboardExtension from '../extension';
38 39 import thingsboardTenant from '../tenant';
39 40 import thingsboardCustomer from '../customer';
40 41 import thingsboardUser from '../user';
... ... @@ -66,6 +67,7 @@ export default angular.module('thingsboard.home', [
66 67 thingsboardEntity,
67 68 thingsboardEvent,
68 69 thingsboardAlarm,
  70 + thingsboardExtension,
69 71 thingsboardTenant,
70 72 thingsboardCustomer,
71 73 thingsboardUser,
... ...
... ... @@ -729,6 +729,133 @@ export default angular.module('thingsboard.locale', [])
729 729 "messages-processed": "Messages processed",
730 730 "errors-occurred": "Errors occurred"
731 731 },
  732 + "extension": {
  733 + "extensions": "Extensions",
  734 + "selected-extensions": "{ count, select, 1 {1 extension} other {# extensions} } selected",
  735 + "type": "Type",
  736 + "key": "Key",
  737 + "value": "Value",
  738 + "id": "Id",
  739 + "extension-id": "Extension id",
  740 + "extension-type": "Extension type",
  741 + "transformer-json": "JSON *",
  742 + "unique-id-required": "Current extension id already exists.",
  743 + "delete": "Delete extension",
  744 + "add": "Add extension",
  745 + "edit": "Edit extension",
  746 + "delete-extension-title": "Are you sure you want to delete the extension '{{extensionId}}'?",
  747 + "delete-extension-text": "Be careful, after the confirmation the extension and all related data will become unrecoverable.",
  748 + "delete-extensions-title": "Are you sure you want to delete { count, select, 1 {1 extension} other {# extensions} }?",
  749 + "delete-extensions-text": "Be careful, after the confirmation all selected extensions will be removed.",
  750 + "converters": "Converters",
  751 + "converter-id": "Converter id",
  752 + "configuration": "Configuration",
  753 + "converter-configurations": "Converter configurations",
  754 + "token": "Security token",
  755 + "add-converter": "Add converter",
  756 + "add-config": "Add converter configuration",
  757 + "device-name-expression": "Device name expression",
  758 + "device-type-expression": "Device type expression",
  759 + "custom": "Custom",
  760 + "to-double": "To Double",
  761 + "transformer": "Transformer",
  762 + "json-required": "Transformer json is required.",
  763 + "json-parse": "Unable to parse transformer json.",
  764 + "attributes": "Attributes",
  765 + "add-attribute": "Add attribute",
  766 + "add-map": "Add mapping element",
  767 + "timeseries": "Timeseries",
  768 + "add-timeseries": "Add timeseries",
  769 + "field-required": "Field is required",
  770 + "brokers": "Brokers",
  771 + "add-broker": "Add broker",
  772 + "host": "Host",
  773 + "port": "Port",
  774 + "port-range": "Port should be in a range from 1 to 65535.",
  775 + "ssl": "Ssl",
  776 + "credentials": "Credentials",
  777 + "username": "Username",
  778 + "password": "Password",
  779 + "retry-interval": "Retry interval in milliseconds",
  780 + "anonymous": "Anonymous",
  781 + "basic": "Basic",
  782 + "pem": "PEM",
  783 + "ca-cert": "CA certificate file *",
  784 + "private-key": "Private key file *",
  785 + "cert": "Certificate file *",
  786 + "no-file": "No file selected.",
  787 + "drop-file": "Drop a file or click to select a file to upload.",
  788 + "mapping": "Mapping",
  789 + "topic-filter": "Topic filter",
  790 + "converter-type": "Converter type",
  791 + "converter-json": "Json",
  792 + "json-name-expression": "Device name json expression",
  793 + "topic-name-expression": "Device name topic expression",
  794 + "json-type-expression": "Device type json expression",
  795 + "topic-type-expression": "Device type topic expression",
  796 + "attribute-key-expression": "Attribute key expression",
  797 + "attr-json-key-expression": "Attribute key json expression",
  798 + "attr-topic-key-expression": "Attribute key topic expression",
  799 + "request-id-expression": "Request id expression",
  800 + "request-id-json-expression": "Request id json expression",
  801 + "request-id-topic-expression": "Request id topic expression",
  802 + "response-topic-expression": "Response topic expression",
  803 + "value-expression": "Value expression",
  804 + "topic": "Topic",
  805 + "timeout": "Timeout in milliseconds",
  806 + "converter-json-required": "Converter json is required.",
  807 + "converter-json-parse": "Unable to parse converter json.",
  808 + "filter-expression": "Filter expression",
  809 + "connect-requests": "Connect requests",
  810 + "add-connect-request": "Add connect request",
  811 + "disconnect-requests": "Disconnect requests",
  812 + "add-disconnect-request": "Add disconnect request",
  813 + "attribute-requests": "Attribute requests",
  814 + "add-attribute-request": "Add attribute request",
  815 + "attribute-updates": "Attribute updates",
  816 + "add-attribute-update": "Add attribute update",
  817 + "server-side-rpc": "Server side RPC",
  818 + "add-server-side-rpc-request": "Add server-side RPC request",
  819 + "device-name-filter": "Device name filter",
  820 + "attribute-filter": "Attribute filter",
  821 + "method-filter": "Method filter",
  822 + "request-topic-expression": "Request topic expression",
  823 + "response-timeout": "Response timeout in milliseconds",
  824 + "topic-expression": "Topic expression",
  825 + "client-scope": "Client scope",
  826 + "opc-server": "Servers",
  827 + "opc-add-server": "Add server",
  828 + "opc-application-name": "Application name",
  829 + "opc-application-uri": "Application uri",
  830 + "opc-scan-period-in-seconds": "Scan period in seconds",
  831 + "opc-security": "Security",
  832 + "opc-identity": "Identity",
  833 + "opc-keystore": "Keystore",
  834 + "opc-type": "Type",
  835 + "opc-keystore-type":"Type",
  836 + "opc-keystore-location":"Location *",
  837 + "opc-keystore-password":"Password",
  838 + "opc-keystore-alias":"Alias",
  839 + "opc-keystore-key-password":"Key password",
  840 + "opc-device-node-pattern":"Device node pattern",
  841 + "opc-device-name-pattern":"Device name pattern",
  842 +
  843 + "sync": {
  844 + "status": "Status",
  845 + "sync": "Sync",
  846 + "not-sync": "Not sync",
  847 + "last-sync-time": "Last sync time",
  848 + "not-available": "Not available"
  849 + },
  850 +
  851 + "export-extensions-configuration":"Export extensions configuration",
  852 + "import-extensions-configuration":"Import extensions configuration",
  853 + "import-extensions": "Import extensions",
  854 + "import-extension": "Import extension",
  855 + "export-extension": "Export extension",
  856 + "file": "Extensions file",
  857 + "invalid-file-error": "Invalid extension file"
  858 + },
732 859 "fullscreen": {
733 860 "expand": "Expand to fullscreen",
734 861 "exit": "Exit fullscreen",
... ... @@ -851,7 +978,6 @@ export default angular.module('thingsboard.locale', [])
851 978 "invalid-plugin-file-error": "Unable to import plugin: Invalid plugin data structure.",
852 979 "copyId": "Copy plugin Id",
853 980 "idCopiedMessage": "Plugin Id has been copied to clipboard"
854   -
855 981 },
856 982 "position": {
857 983 "top": "Top",
... ... @@ -1071,7 +1197,8 @@ export default angular.module('thingsboard.locale', [])
1071 1197 "boolean": "Boolean",
1072 1198 "boolean-value": "Boolean value",
1073 1199 "false": "False",
1074   - "true": "True"
  1200 + "true": "True",
  1201 + "long": "Long"
1075 1202 },
1076 1203 "widget": {
1077 1204 "widget-library": "Widgets Library",
... ...
... ... @@ -155,6 +155,15 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
155 155 return result;
156 156 }
157 157
  158 + set timestamp(timestamp) {
  159 + this.options.timestamp = timestamp;
  160 + this.draw();
  161 + }
  162 +
  163 + get timestamp() {
  164 + return this.options.timestamp;
  165 + }
  166 +
158 167 draw() {
159 168 try {
160 169
... ... @@ -195,7 +204,9 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
195 204 canvas.elementClone.initialized = true;
196 205 }
197 206
198   - if (!this.elementValueClone.initialized || this.elementValueClone.renderedValue !== this.value) {
  207 + var valueChanged = false;
  208 +
  209 + if (!this.elementValueClone.initialized || this.elementValueClone.renderedValue !== this.value || (options.showTimestamp && this.elementValueClone.renderedTimestamp !== this.timestamp)) {
199 210 let context = this.contextValueClone;
200 211 // clear the cache
201 212 context.clearRect(x, y, w, h);
... ... @@ -208,10 +219,13 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
208 219
209 220 if (options.showTimestamp) {
210 221 drawDigitalLabel(context, options);
  222 + this.elementValueClone.renderedTimestamp = this.timestamp;
211 223 }
212 224
213 225 this.elementValueClone.initialized = true;
214 226 this.elementValueClone.renderedValue = this.value;
  227 +
  228 + valueChanged = true;
215 229 }
216 230
217 231 var progress = (canvasGauges.drawings.normalizedValue(options).normal - options.minValue) /
... ... @@ -219,7 +233,7 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
219 233
220 234 var fixedProgress = progress.toFixed(3);
221 235
222   - if (!this.elementProgressClone.initialized || this.elementProgressClone.renderedProgress !== fixedProgress) {
  236 + if (!this.elementProgressClone.initialized || this.elementProgressClone.renderedProgress !== fixedProgress || valueChanged) {
223 237 let context = this.contextProgressClone;
224 238 // clear the cache
225 239 context.clearRect(x, y, w, h);
... ...
... ... @@ -104,7 +104,9 @@ export default class TbAnalogueCompass {
104 104 var tvPair = cellData.data[cellData.data.length -
105 105 1];
106 106 var value = tvPair[1];
107   - this.gauge.value = value;
  107 + if(value !== this.gauge.value) {
  108 + this.gauge.value = value;
  109 + }
108 110 }
109 111 }
110 112 }
... ...
... ... @@ -212,7 +212,9 @@ export default class TbAnalogueLinearGauge {
212 212 var tvPair = cellData.data[cellData.data.length -
213 213 1];
214 214 var value = tvPair[1];
215   - this.gauge.value = value;
  215 + if(value !== this.gauge.value) {
  216 + this.gauge.value = value;
  217 + }
216 218 }
217 219 }
218 220 }
... ...
... ... @@ -221,7 +221,9 @@ export default class TbAnalogueRadialGauge {
221 221 var tvPair = cellData.data[cellData.data.length -
222 222 1];
223 223 var value = tvPair[1];
224   - this.gauge.value = value;
  224 + if(value !== this.gauge.value) {
  225 + this.gauge.value = value;
  226 + }
225 227 }
226 228 }
227 229
... ...
... ... @@ -197,14 +197,19 @@ export default class TbCanvasDigitalGauge {
197 197 if (cellData.data.length > 0) {
198 198 var tvPair = cellData.data[cellData.data.length -
199 199 1];
  200 + var timestamp;
200 201 if (this.localSettings.showTimestamp) {
201   - var timestamp = tvPair[0];
  202 + timestamp = tvPair[0];
202 203 var filter= this.ctx.$scope.$injector.get('$filter');
203 204 var timestampDisplayValue = filter('date')(timestamp, this.localSettings.timestampFormat);
204 205 this.gauge.options.label = timestampDisplayValue;
205 206 }
206 207 var value = tvPair[1];
207   - this.gauge.value = value;
  208 + if(value !== this.gauge.value) {
  209 + this.gauge.value = value;
  210 + } else if (this.localSettings.showTimestamp && this.gauge.timestamp != timestamp) {
  211 + this.gauge.timestamp = timestamp;
  212 + }
208 213 }
209 214 }
210 215 }
... ...
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import './extensions-table-widget.scss';
  18 +
  19 +/* eslint-disable import/no-unresolved, import/default */
  20 +
  21 +import extensionsTableWidgetTemplate from './extensions-table-widget.tpl.html';
  22 +
  23 +/* eslint-enable import/no-unresolved, import/default */
  24 +
  25 +export default angular.module('thingsboard.widgets.extensionsTableWidget', [])
  26 + .directive('tbExtensionsTableWidget', ExtensionsTableWidget)
  27 + .name;
  28 +
  29 +/*@ngInject*/
  30 +function ExtensionsTableWidget() {
  31 + return {
  32 + restrict: "E",
  33 + scope: true,
  34 + bindToController: {
  35 + ctx: '='
  36 + },
  37 + controller: ExtensionsTableWidgetController,
  38 + controllerAs: 'vm',
  39 + templateUrl: extensionsTableWidgetTemplate
  40 + };
  41 +}
  42 +
  43 +/*@ngInject*/
  44 +function ExtensionsTableWidgetController($scope, $translate, utils) {
  45 + var vm = this;
  46 +
  47 + vm.datasources = null;
  48 + vm.tabsHidden = false;
  49 +
  50 + $scope.$watch('vm.ctx', function() {
  51 + if (vm.ctx && vm.ctx.defaultSubscription) {
  52 + vm.settings = vm.ctx.settings;
  53 + vm.subscription = vm.ctx.defaultSubscription;
  54 + vm.datasources = vm.subscription.datasources;
  55 + initializeConfig();
  56 + updateDatasources();
  57 + }
  58 + });
  59 +
  60 + function initializeConfig() {
  61 +
  62 + if (vm.settings.extensionsTitle && vm.settings.extensionsTitle.length) {
  63 + vm.extensionsTitle = utils.customTranslation(vm.settings.extensionsTitle, vm.settings.extensionsTitle);
  64 + } else {
  65 + vm.extensionsTitle = $translate.instant('extension.extensions');
  66 + }
  67 + vm.ctx.widgetTitle = vm.extensionsTitle;
  68 +
  69 + vm.ctx.widgetActions = [vm.importExtensionsAction, vm.exportExtensionsAction, vm.addAction, vm.searchAction, vm.refreshAction];
  70 + }
  71 +
  72 + function updateDatasources() {
  73 +
  74 + var datasource = vm.datasources[0];
  75 + vm.selectedSource = vm.datasources[0];
  76 + vm.ctx.widgetTitle = utils.createLabelFromDatasource(datasource, vm.extensionsTitle);
  77 + }
  78 +
  79 + vm.changeSelectedSource = function(source) {
  80 + vm.selectedSource = source;
  81 + };
  82 +
  83 + vm.searchAction = {
  84 + name: "action.search",
  85 + show: true,
  86 + onAction: function() {
  87 + $scope.$broadcast("showSearch", vm.selectedSource);
  88 + },
  89 + icon: "search"
  90 + };
  91 +
  92 + vm.refreshAction = {
  93 + name: "action.refresh",
  94 + show: true,
  95 + onAction: function() {
  96 + $scope.$broadcast("refreshExtensions", vm.selectedSource);
  97 + },
  98 + icon: "refresh"
  99 + };
  100 +
  101 + vm.addAction = {
  102 + name: "action.add",
  103 + show: true,
  104 + onAction: function() {
  105 + $scope.$broadcast("addExtension", vm.selectedSource);
  106 + },
  107 + icon: "add"
  108 + };
  109 +
  110 + vm.exportExtensionsAction = {
  111 + name: "extension.export-extensions-configuration",
  112 + show: true,
  113 + onAction: function() {
  114 + $scope.$broadcast("exportExtensions", vm.selectedSource);
  115 + },
  116 + icon: "file_download"
  117 + };
  118 +
  119 + vm.importExtensionsAction = {
  120 + name: "extension.import-extensions-configuration",
  121 + show: true,
  122 + onAction: function() {
  123 + $scope.$broadcast("importExtensions", vm.selectedSource);
  124 + },
  125 + icon: "file_upload"
  126 + };
  127 +
  128 + $scope.$on("filterMode", function($event, mode) {
  129 + vm.tabsHidden = mode;
  130 + });
  131 +
  132 + $scope.$on("selectedExtensions", function($event, mode) {
  133 + vm.tabsHidden = mode;
  134 + });
  135 +}
\ No newline at end of file
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +tb-extension-table {
  18 + md-content {
  19 + background-color: #fff;
  20 + }
  21 +}
  22 +md-tabs.hide-tabs-menu {
  23 + md-tabs-wrapper {
  24 + display: none;
  25 + }
  26 + md-tabs-content-wrapper {
  27 + top: 0 !important;
  28 + }
  29 +}
\ No newline at end of file
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-tabs id="tabs" md-border-bottom flex class="tb-absolute-fill" ng-class="{'hide-tabs-menu': vm.datasources.length == 1 || vm.tabsHidden}">
  19 + <md-tab ng-repeat="source in vm.datasources" label="{{ source.name }}" md-on-select="vm.changeSelectedSource(source)">
  20 + <tb-extension-table flex
  21 + entity-id="source.entityId"
  22 + entity-type="{{source.entityType}}"
  23 + in-widget="true"
  24 + ctx="vm.ctx">
  25 + </tb-extension-table>
  26 + </md-tab>
  27 +</md-tabs>
\ No newline at end of file
... ...
... ... @@ -16,7 +16,7 @@
16 16
17 17 -->
18 18 <!DOCTYPE html>
19   -<html ng-app="thingsboard" ng-strict-di>
  19 +<html ng-app="thingsboard" ng-strict-di style="width: 100%;">
20 20 <head>
21 21 <title ng-bind="pageTitle"></title>
22 22 <base href="/" />
... ...
... ... @@ -39,6 +39,6 @@
39 39 @include keyframes(tbMoveToBottomFade) {
40 40 to {
41 41 opacity: 0;
42   - @include transform(translate(0, 100%));
  42 + @include transform(translate(0, 150%));
43 43 }
44 44 }
\ No newline at end of file
... ...
... ... @@ -494,11 +494,15 @@ md-tabs.tb-headless {
494 494 height: 100%;
495 495 max-width: 240px;
496 496 span {
497   - padding: 10px 10px 20px 10px;
  497 + padding: 0 0 20px 0;
498 498 font-size: 18px;
499 499 font-weight: 400;
500 500 white-space: normal;
501 501 line-height: 18px;
  502 + max-height: 18px;
  503 + min-height: 18px;
  504 + height: 18px;
  505 + margin: auto;
502 506 }
503 507 }
504 508
... ...