thingsboard-developers

added mqtt form

11/24/2017 1:25:40 PM

Details

diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index dff7635..f78e0a8 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -332,6 +332,24 @@ export default angular.module('thingsboard.types', [])
                 toDouble: 'extension.to-double',
                 custom: 'extension.custom'
             },
+            mqttConverterTypes: {
+                json: 'extension.converter-json',
+                custom: 'extension.custom'
+            },
+            mqttCredentialTypes: {
+                anonymous:  {
+                    value: "anonymous",
+                    name: "extension.anonymous"
+                },
+                basic: {
+                    value: "basic",
+                    name: "extension.basic"
+                },
+                pem: {
+                    value: "cert.PEM",
+                    name: "extension.pem"
+                }
+            },
             latestTelemetry: {
                 value: "LATEST_TELEMETRY",
                 name: "attribute.scope-latest-telemetry",
diff --git a/ui/src/app/extension/extension-dialog.controller.js b/ui/src/app/extension/extension-dialog.controller.js
index 3b13214..e580929 100644
--- a/ui/src/app/extension/extension-dialog.controller.js
+++ b/ui/src/app/extension/extension-dialog.controller.js
@@ -45,6 +45,7 @@ export default function ExtensionDialogController($scope, $mdDialog, $translate,
         $mdDialog.cancel();
     }
     function save() {
+        $mdDialog.hide();
         saveTransformers();
         if(vm.isAdd) {
             vm.allExtensions.push(vm.newExtension);
@@ -60,7 +61,6 @@ export default function ExtensionDialogController($scope, $mdDialog, $translate,
         attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then(
             function success() {
                 $scope.theForm.$setPristine();
-                $mdDialog.hide();
             }
         );
     }
@@ -85,21 +85,39 @@ export default function ExtensionDialogController($scope, $mdDialog, $translate,
     }
 
     function saveTransformers() {
-        var config = vm.newExtension.configuration.converterConfigurations;
         if(vm.newExtension.type == types.extensionType.http) {
-            for(let i=0;i<config.length;i++) {
-                for(let j=0;j<config[i].converters.length;j++){
-                    for(let k=0;k<config[i].converters[j].attributes.length;k++){
-                        if(config[i].converters[j].attributes[k].transformerType == "toDouble"){
-                            config[i].converters[j].attributes[k].transformer = {type: "intToDouble"};
+            var config = vm.newExtension.configuration.converterConfigurations;
+            if(config && config.length > 0) {
+                for(let i=0;i<config.length;i++) {
+                    for(let j=0;j<config[i].converters.length;j++){
+                        for(let k=0;k<config[i].converters[j].attributes.length;k++){
+                            if(config[i].converters[j].attributes[k].transformerType == "toDouble"){
+                                config[i].converters[j].attributes[k].transformer = {type: "intToDouble"};
+                            }
+                            delete config[i].converters[j].attributes[k].transformerType;
+                        }
+                        for(let l=0;l<config[i].converters[j].timeseries.length;l++) {
+                            if(config[i].converters[j].timeseries[l].transformerType == "toDouble"){
+                                config[i].converters[j].timeseries[l].transformer = {type: "intToDouble"};
+                            }
+                            delete config[i].converters[j].timeseries[l].transformerType;
                         }
-                        delete config[i].converters[j].attributes[k].transformerType;
                     }
-                    for(let l=0;l<config[i].converters[j].timeseries.length;l++) {
-                        if(config[i].converters[j].timeseries[l].transformerType == "toDouble"){
-                            config[i].converters[j].timeseries[l].transformer = {type: "intToDouble"};
+                }
+            }
+        }
+        if(vm.newExtension.type == types.extensionType.mqtt) {
+            var brokers = vm.newExtension.configuration.brokers;
+            if(brokers && brokers.length > 0) {
+                for(let i=0;i<brokers.length;i++) {
+                    if(brokers[i].mapping && brokers[i].mapping.length > 0) {
+                        for(let j=0;j<brokers[i].mapping.length;j++) {
+                            if(brokers[i].mapping[j].converterType == "json") {
+                                delete brokers[i].mapping[j].converter.nameExp;
+                                delete brokers[i].mapping[j].converter.typeExp;
+                            }
+                            delete brokers[i].mapping[j].converterType;
                         }
-                        delete config[i].converters[j].timeseries[l].transformerType;
                     }
                 }
             }
@@ -107,8 +125,8 @@ export default function ExtensionDialogController($scope, $mdDialog, $translate,
     }
 
     function editTransformers(extension) {
-        var config = extension.configuration.converterConfigurations;
         if(extension.type == types.extensionType.http) {
+            var config = extension.configuration.converterConfigurations;
             for(let i=0;i<config.length;i++) {
                 for(let j=0;j<config[i].converters.length;j++){
                     for(let k=0;k<config[i].converters[j].attributes.length;k++){
@@ -134,6 +152,30 @@ export default function ExtensionDialogController($scope, $mdDialog, $translate,
                 }
             }
         }
+        if(extension.type == types.extensionType.mqtt) {
+            var brokers = extension.configuration.brokers;
+            for(let i=0;i<brokers.length;i++) {
+                if(brokers[i].mapping && brokers[i].mapping.length > 0) {
+                    for(let j=0;j<brokers[i].mapping.length;j++) {
+                        if(brokers[i].mapping[j].converter.type == "json") {
+                            if(brokers[i].mapping[j].converter.deviceNameTopicExpression) {
+                                brokers[i].mapping[j].converter.nameExp = "deviceNameTopicExpression";
+                            } else {
+                                brokers[i].mapping[j].converter.nameExp = "deviceNameJsonExpression";
+                            }
+                            if(brokers[i].mapping[j].converter.deviceTypeTopicExpression) {
+                                brokers[i].mapping[j].converter.typeExp = "deviceTypeTopicExpression";
+                            } else {
+                                brokers[i].mapping[j].converter.typeExp = "deviceTypeJsonExpression";
+                            }
+                            brokers[i].mapping[j].converterType = "json";
+                        } else {
+                            brokers[i].mapping[j].converterType = "custom";
+                        }
+                    }
+                }
+            }
+        }
     }
 }
 
diff --git a/ui/src/app/extension/extension-dialog.tpl.html b/ui/src/app/extension/extension-dialog.tpl.html
index edb6918..e3f3663 100644
--- a/ui/src/app/extension/extension-dialog.tpl.html
+++ b/ui/src/app/extension/extension-dialog.tpl.html
@@ -55,6 +55,7 @@
                         </section>
 
                         <div tb-extension-form-http config="vm.configuration" is-add="vm.isAdd" ng-if="vm.newExtension.type && vm.newExtension.type == vm.types.extensionType.http"></div>
+                        <div tb-extension-form-mqtt config="vm.configuration" is-add="vm.isAdd" ng-if="vm.newExtension.type && vm.newExtension.type == vm.types.extensionType.mqtt"></div>
 
                     </fieldset>
 
diff --git a/ui/src/app/extension/extensions-forms/extension-form.scss b/ui/src/app/extension/extensions-forms/extension-form.scss
index 82d065f..f40970b 100644
--- a/ui/src/app/extension/extensions-forms/extension-form.scss
+++ b/ui/src/app/extension/extensions-forms/extension-form.scss
@@ -1,4 +1,4 @@
-/*
+/**
  * Copyright © 2016-2017 The Thingsboard Authors
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,7 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 .extension-form {
   li > .md-button {
     color: rgba(0, 0, 0, 0.7);
@@ -23,6 +22,17 @@
     margin-top: 0;
     padding-left: 3px;
   }
+  .t-right {
+    text-align: right;
+  }
+  .tb-container {
+    width:100%;
+  }
+  .dropdown-messages {
+    .tb-error-message {
+      padding: 5px 0 0 0;
+    }
+  }
 }
 
 .tb-extension-custom-transformer-panel {
diff --git a/ui/src/app/extension/extensions-forms/extension-form-http.directive.js b/ui/src/app/extension/extensions-forms/extension-form-http.directive.js
index 9a07f2b..b185b4b 100644
--- a/ui/src/app/extension/extensions-forms/extension-form-http.directive.js
+++ b/ui/src/app/extension/extensions-forms/extension-form-http.directive.js
@@ -132,7 +132,7 @@ export default function ExtensionFormHttpDirective($compile, $templateCache, $tr
                 }
             }
         }
-        
+
         $compile(element.contents())(scope);
     }
 
diff --git a/ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js b/ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js
new file mode 100644
index 0000000..4101bb5
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js
@@ -0,0 +1,236 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './extension-form.scss';
+
+/* eslint-disable angular/log */
+
+import extensionFormMqttTemplate from './extension-form-mqtt.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function ExtensionFormHttpDirective($compile, $templateCache, $translate, types) {
+
+    var linker = function(scope, element) {
+
+        var template = $templateCache.get(extensionFormMqttTemplate);
+        element.html(template);
+
+        scope.types = types;
+        scope.theForm = scope.$parent.theForm;
+
+        scope.nameExpressions = {
+            deviceNameJsonExpression: "extension.converter-json",
+            deviceNameTopicExpression: "extension.topic"
+        };
+        scope.typeExpressions = {
+            deviceTypeJsonExpression: "extension.converter-json",
+            deviceTypeTopicExpression: "extension.topic"
+        };
+
+        scope.extensionCustomConverterOptions = {
+            useWrapMode: false,
+            mode: 'json',
+            showGutter: true,
+            showPrintMargin: true,
+            theme: 'github',
+            advanced: {
+                enableSnippets: true,
+                enableBasicAutocompletion: true,
+                enableLiveAutocompletion: true
+            },
+            onLoad: function(_ace) {
+                _ace.$blockScrolling = 1;
+            }
+        };
+
+
+        if(scope.isAdd) {
+            scope.brokers = [];
+            scope.config.brokers = scope.brokers;
+        } else {
+            scope.brokers = scope.config.brokers;
+        }
+
+        scope.updateValidity = function () {
+            var valid = scope.brokers && scope.brokers.length > 0;
+            scope.theForm.$setValidity('brokers', valid);
+            if(scope.brokers.length) {
+                for(let i=0;i<scope.brokers.length;i++) {
+                    if(scope.brokers[i].credentials.type == scope.types.mqttCredentialTypes.pem.value) {
+                        if(!(scope.brokers[i].credentials.caCert && scope.brokers[i].credentials.privateKey && scope.brokers[i].credentials.cert)) {
+                            scope.theForm.$setValidity('cert.PEM', false);
+                            break;
+                        } else {
+                            scope.theForm.$setValidity('cert.PEM', true);
+                        }
+                    }
+                }
+            }
+        }
+
+        scope.$watch('brokers', function() {
+            scope.updateValidity();
+        }, true);
+
+        scope.addBroker = function() {
+            var newBroker = {host:"localhost", port:1882, ssl:false, retryInterval:3000, credentials:{type:"anonymous"}, mapping:[]};
+            scope.brokers.push(newBroker);
+        }
+
+        scope.removeBroker = function(broker) {
+            var index = scope.brokers.indexOf(broker);
+            if (index > -1) {
+                scope.brokers.splice(index, 1);
+            }
+            scope.theForm.$setDirty();
+        }
+
+        scope.addMap = function(mapping) {
+            var newMap = {topicFilter:"sensors", converter:{attributes:[],timeseries:[]}};
+
+            mapping.push(newMap);
+        }
+
+        scope.removeMap = function(map, mapping) {
+            var index = mapping.indexOf(map);
+            if (index > -1) {
+                mapping.splice(index, 1);
+            }
+            scope.theForm.$setDirty();
+        }
+
+        scope.addAttribute = function(attributes) {
+            var newAttribute = {type:"", key:"", value:""};
+            attributes.push(newAttribute);
+        }
+
+        scope.removeAttribute = function(attribute, attributes) {
+            var index = attributes.indexOf(attribute);
+            if (index > -1) {
+                attributes.splice(index, 1);
+            }
+            scope.theForm.$setDirty();
+        }
+
+        scope.changeCredentials = function(broker) {
+            var type = broker.credentials.type;
+            broker.credentials = {};
+            broker.credentials.type = type;
+        }
+
+        scope.changeConverterType = function(map) {
+            if(map.converterType == "custom"){
+                map.converter = "";
+            }
+            if(map.converterType == "json") {
+                map.converter = {attributes:[],timeseries:[]};
+            }
+        }
+
+        scope.changeNameExpression = function(converter) {
+            if(converter.nameExp == "deviceNameJsonExpression") {
+                if(converter.deviceNameTopicExpression) {
+                    delete converter.deviceNameTopicExpression;
+                }
+            }
+            if(converter.nameExp == "deviceNameTopicExpression") {
+                if(converter.deviceNameJsonExpression) {
+                    delete converter.deviceNameJsonExpression;
+                }
+            }
+        }
+
+        scope.changeTypeExpression = function(converter) {
+            if(converter.typeExp == "deviceTypeJsonExpression") {
+                if(converter.deviceTypeTopicExpression) {
+                    delete converter.deviceTypeTopicExpression;
+                }
+            }
+            if(converter.typeExp == "deviceTypeTopicExpression") {
+                if(converter.deviceTypeJsonExpression) {
+                    delete converter.deviceTypeJsonExpression;
+                }
+            }
+        }
+
+        scope.validateCustomConverter = function(model, editorName) {
+            if(model && model.length) {
+                try {
+                    angular.fromJson(model);
+                    scope.theForm[editorName].$setValidity('converterJSON', true);
+                } catch(e) {
+                    scope.theForm[editorName].$setValidity('converterJSON', false);
+                }
+            }
+        }
+
+        scope.fileAdded = function($file, broker, fileType) {
+            var reader = new FileReader();
+            reader.onload = function(event) {
+                scope.$apply(function() {
+                    if(event.target.result) {
+                        scope.theForm.$setDirty();
+                        var addedFile = event.target.result;
+                        if (addedFile && addedFile.length > 0) {
+                            if(fileType == "caCert") {
+                                broker.credentials.caCertFileName = $file.name;
+                                broker.credentials.caCert = addedFile.replace(/^data.*base64,/, "");
+                            }
+                            if(fileType == "privateKey") {
+                                broker.credentials.privateKeyFileName = $file.name;
+                                broker.credentials.privateKey = addedFile.replace(/^data.*base64,/, "");
+                            }
+                            if(fileType == "Cert") {
+                                broker.credentials.certFileName = $file.name;
+                                broker.credentials.cert = addedFile.replace(/^data.*base64,/, "");
+                            }
+                        }
+                    }
+                });
+            };
+            reader.readAsDataURL($file.file);
+        }
+
+        scope.clearFile = function(broker, fileType) {
+            scope.theForm.$setDirty();
+            if(fileType == "caCert") {
+                broker.credentials.caCertFileName = null;
+                broker.credentials.caCert = null;
+            }
+            if(fileType == "privateKey") {
+                broker.credentials.privateKeyFileName = null;
+                broker.credentials.privateKey = null;
+            }
+            if(fileType == "Cert") {
+                broker.credentials.certFileName = null;
+                broker.credentials.cert = null;
+            }
+        }
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "A",
+        link: linker,
+        scope: {
+            config: "=",
+            isAdd: "="
+        }
+    }
+}
\ No newline at end of file
diff --git a/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html
index e55ad6c..94ae789 100644
--- a/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html
+++ b/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html
@@ -15,4 +15,439 @@
     limitations under the License.
 
 -->
-<div>MQTT</div>
\ No newline at end of file
+<md-card class="extension-form extension-mqtt">
+    <md-card-title name="testValid">
+        <md-card-title-text>
+            <span translate class="md-headline">extension.configuration</span>
+        </md-card-title-text>
+    </md-card-title>
+    <md-card-content>
+        <v-accordion id="mqtt-brokers-accordion" class="vAccordion--default">
+            <v-pane id="mqtt-brokers-pane" expanded="isAdd">
+                <v-pane-header>
+                    {{ 'extension.brokers' | translate }}
+                </v-pane-header>
+                <v-pane-content>
+                    <div ng-if="brokers.length === 0">
+                        <span translate layout-align="center center" class="tb-prompt">extension.add-broker-prompt</span>
+                    </div>
+                    <div ng-if="brokers.length > 0">
+                        <ol class="list-group">
+                            <li class="list-group-item" ng-repeat="(brokerIndex,broker) in brokers">
+                                <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeBroker(broker)">
+                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                    <md-tooltip md-direction="top">
+                                        {{ 'action.remove' | translate }}
+                                    </md-tooltip>
+                                </md-button>
+                                <md-card>
+                                    <md-card-content>
+                                        <section flex layout="row">
+                                            <md-input-container flex="40" class="md-block">
+                                                <label translate>extension.port</label>
+                                                <input required type="number" min="1" max="65535" name="mqttPort_{{brokerIndex}}" ng-model="broker.port">
+                                                <div ng-messages="theForm['mqttPort_' + brokerIndex].$error">
+                                                    <div translate ng-message="required">extension.port-required</div>
+                                                    <div translate ng-message="min">extension.port-range</div>
+                                                    <div translate ng-message="max">extension.port-range</div>
+                                                </div>
+                                            </md-input-container>
+                                            <md-input-container flex="60" class="md-block">
+                                                <label translate>extension.host</label>
+                                                <input required name="mqttHost_{{brokerIndex}}" ng-model="broker.host">
+                                                <div ng-messages="theForm['mqttHost_' + brokerIndex].$error">
+                                                    <div translate ng-message="required">extension.host-required</div>
+                                                </div>
+                                            </md-input-container>
+                                        </section>
+                                        <section flex layout="row">
+                                            <md-input-container flex="40" class="md-block">
+                                                <label translate>extension.retry-interval</label>
+                                                <input required type="number" name="mqttRetryInterval_{{brokerIndex}}" ng-model="broker.retryInterval">
+                                                <div ng-messages="theForm['mqttRetryInterval_' + brokerIndex].$error">
+                                                    <div translate ng-message="required">extension.retry-interval-required</div>
+                                                </div>
+                                            </md-input-container>
+                                            <md-input-container flex="50" class="md-block">
+                                                <label translate>extension.credentials</label>
+                                                <md-select required name="mqttCredentials_{{brokerIndex}}" ng-model="broker.credentials.type" ng-change="changeCredentials(broker)">
+                                                    <md-option ng-repeat="(credentialsType, credentialsValue) in types.mqttCredentialTypes" ng-value="credentialsValue.value">
+                                                        {{credentialsValue.name | translate}}
+                                                    </md-option>
+                                                </md-select>
+                                            </md-input-container>
+                                            <md-input-container flex="10" class="md-block t-right">
+                                                <md-checkbox flex aria-label="{{ 'extension.ssl' | translate }}"
+                                                             ng-model="broker.ssl">{{ 'extension.ssl' | translate }}
+                                                </md-checkbox>
+                                            </md-input-container>
+                                        </section>
+                                        <section flex layout="row" ng-if='broker.credentials.type == "basic"'>
+                                            <md-input-container flex="40" class="md-block">
+                                                <label translate>extension.username</label>
+                                                <input required name="mqttUsername_{{brokerIndex}}" ng-model="broker.credentials.username">
+                                                <div ng-messages="theForm['mqttUsername_' + brokerIndex].$error">
+                                                    <div translate ng-message="required">extension.username-required</div>
+                                                </div>
+                                            </md-input-container>
+                                            <md-input-container flex="60" class="md-block">
+                                                <label translate>extension.password</label>
+                                                <input required name="mqttPassword_{{brokerIndex}}" ng-model="broker.credentials.password">
+                                                <div ng-messages="theForm['mqttPassword_' + brokerIndex].$error">
+                                                    <div translate ng-message="required">extension.password-required</div>
+                                                </div>
+                                            </md-input-container>
+                                        </section>
+                                        <section flex layout="column" ng-if='broker.credentials.type == "cert.PEM"'>
+                                            <div class="tb-container">
+                                                <label class="tb-label" translate>extension.ca-cert</label>
+                                                <div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, broker, "caCert")' class="tb-file-select-container">
+                                                    <div class="tb-file-clear-container">
+                                                        <md-button ng-click='clearFile(broker, "caCert")' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
+                                                            <md-tooltip md-direction="top">
+                                                                {{ 'action.remove' | translate }}
+                                                            </md-tooltip>
+                                                            <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
+                                                        </md-button>
+                                                    </div>
+                                                    <div class="alert tb-flow-drop" flow-drop>
+                                                        <label for="caCertSelect_{{brokerIndex}}" translate>extension.drop-file</label>
+                                                        <input class="file-input" flow-btn flow-attrs="{accept:'.pem'}" id="caCertSelect_{{brokerIndex}}">
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div class="dropdown-messages">
+                                                <div ng-if="!broker.credentials.caCertFileName" class="tb-error-message" translate>extension.no-file</div>
+                                                <div ng-if="broker.credentials.caCertFileName">{{broker.credentials.caCertFileName}}</div>
+                                            </div>
+                                            <div class="tb-container">
+                                                <label class="tb-label" translate>extension.private-key</label>
+                                                <div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, broker, "privateKey")' class="tb-file-select-container">
+                                                    <div class="tb-file-clear-container">
+                                                        <md-button ng-click='clearFile(broker, "privateKey")' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
+                                                            <md-tooltip md-direction="top">
+                                                                {{ 'action.remove' | translate }}
+                                                            </md-tooltip>
+                                                            <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
+                                                        </md-button>
+                                                    </div>
+                                                    <div class="alert tb-flow-drop" flow-drop>
+                                                        <label for="privateKeySelect_{{brokerIndex}}" translate>extension.drop-file</label>
+                                                        <input class="file-input" flow-btn flow-attrs="{accept:'.pem'}" id="privateKeySelect_{{brokerIndex}}">
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div class="dropdown-messages">
+                                                <div ng-if="!broker.credentials.privateKeyFileName" class="tb-error-message" translate>extension.no-file</div>
+                                                <div ng-if="broker.credentials.privateKeyFileName">{{broker.credentials.privateKeyFileName}}</div>
+                                            </div>
+                                            <div class="tb-container" ng-class="broker.credentials.certFileName ? 'ng-valid' : 'ng-invalid'">
+                                                <label class="tb-label" translate>extension.cert</label>
+                                                <div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, broker, "Cert")' class="tb-file-select-container">
+                                                    <div class="tb-file-clear-container">
+                                                        <md-button ng-click='clearFile(broker, "Cert")' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
+                                                            <md-tooltip md-direction="top">
+                                                                {{ 'action.remove' | translate }}
+                                                            </md-tooltip>
+                                                            <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
+                                                        </md-button>
+                                                    </div>
+                                                    <div class="alert tb-flow-drop" flow-drop>
+                                                        <label for="CertSelect_{{brokerIndex}}" translate>extension.drop-file</label>
+                                                        <input class="file-input" flow-btn flow-attrs="{accept:'.pem'}" id="CertSelect_{{brokerIndex}}">
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div class="dropdown-messages">
+                                                <div ng-if="!broker.credentials.certFileName" class="tb-error-message" translate>extension.no-file</div>
+                                                <div ng-if="broker.credentials.certFileName">{{broker.credentials.certFileName}}</div>
+                                            </div>
+                                        </section>
+
+                                        <v-accordion id="mqtt-mapping-accordion" class="vAccordion--default">
+                                            <v-pane id="mqtt-mapping-pane">
+                                                <v-pane-header>
+                                                    {{ 'extension.mapping' | translate }}
+                                                </v-pane-header>
+                                                <v-pane-content>
+                                                    <div ng-if="broker.mapping.length > 0">
+                                                        <ol class="list-group">
+                                                            <li class="list-group-item" ng-repeat="(mapIndex,map) in broker.mapping">
+                                                                <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeMap(map, broker.mapping)">
+                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                    <md-tooltip md-direction="top">
+                                                                        {{ 'action.remove' | translate }}
+                                                                    </md-tooltip>
+                                                                </md-button>
+                                                                <md-card>
+                                                                    <md-card-content>
+                                                                        <section flex layout="row">
+                                                                            <md-input-container flex="40" class="md-block">
+                                                                                <label translate>extension.converter-type</label>
+                                                                                <md-select required name="mqttConverterType_{{brokerIndex}}{{mapIndex}}" ng-model="map.converterType" ng-change="changeConverterType(map)">
+                                                                                    <md-option ng-repeat="(converterType, value) in types.mqttConverterTypes" ng-value="converterType">
+                                                                                        {{value | translate}}
+                                                                                    </md-option>
+                                                                                </md-select>
+                                                                                <div ng-messages="theForm['mqttConverterType_' + brokerIndex + mapIndex].$error">
+                                                                                    <div translate ng-message="required">extension.converter-type-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                            <md-input-container flex="60" class="md-block">
+                                                                                <label translate>extension.topic-filter</label>
+                                                                                <input required name="mqttTopicFilter_{{brokerIndex}}{{mapIndex}}" ng-model="map.topicFilter">
+                                                                                <div ng-messages="theForm['mqttTopicFilter_' + brokerIndex + mapIndex].$error">
+                                                                                    <div translate ng-message="required">extension.topic-filter-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                        </section>
+
+                                                                        <div ng-if='map.converterType =="json"' ng-init="map.converter.type = 'json'">
+                                                                            <section flex layout="row">
+                                                                                <md-input-container flex="40" class="md-block">
+                                                                                    <label translate>extension.name-expression</label>
+                                                                                    <md-select required name="mqttDeviceNameExpression_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.nameExp" ng-change="changeNameExpression(map.converter)">
+                                                                                        <md-option ng-repeat="(key, value) in nameExpressions" ng-value='key'>
+                                                                                            {{value | translate}}
+                                                                                        </md-option>
+                                                                                    </md-select>
+                                                                                </md-input-container>
+                                                                                <md-input-container ng-if="map.converter.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block">
+                                                                                    <label translate>extension.json-name-expression</label>
+                                                                                    <input required name="mqttJsonNameExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceNameJsonExpression">
+                                                                                    <div ng-messages="theForm['mqttJsonNameExp_' + brokerIndex + mapIndex].$error">
+                                                                                        <div translate ng-message="required">extension.json-name-expression-required</div>
+                                                                                    </div>
+                                                                                </md-input-container>
+                                                                                <md-input-container ng-if="map.converter.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block">
+                                                                                    <label translate>extension.topic-name-expression</label>
+                                                                                    <input required name="mqttTopicNameExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceNameTopicExpression">
+                                                                                    <div ng-messages="theForm['mqttTopicNameExp_' + brokerIndex + mapIndex].$error">
+                                                                                        <div translate ng-message="required">extension.topic-name-expression-required</div>
+                                                                                    </div>
+                                                                                </md-input-container>
+                                                                            </section>
+                                                                            <section flex layout="row">
+                                                                                <md-input-container flex="40" class="md-block">
+                                                                                    <label translate>extension.type-expression</label>
+                                                                                    <md-select required name="mqttDeviceTypeExpression_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.typeExp" ng-change="changeTypeExpression(map.converter)">
+                                                                                        <md-option ng-repeat="(key, value) in typeExpressions" ng-value='key'>
+                                                                                            {{value | translate}}
+                                                                                        </md-option>
+                                                                                    </md-select>
+                                                                                </md-input-container>
+                                                                                <md-input-container ng-if="map.converter.typeExp == 'deviceTypeJsonExpression'" flex="60" class="md-block">
+                                                                                    <label translate>extension.json-type-expression</label>
+                                                                                    <input required name="mqttJsonTypeExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceTypeJsonExpression">
+                                                                                    <div ng-messages="theForm['mqttJsonTypeExp_' + brokerIndex + mapIndex].$error">
+                                                                                        <div translate ng-message="required">extension.json-type-expression-required</div>
+                                                                                    </div>
+                                                                                </md-input-container>
+                                                                                <md-input-container ng-if="map.converter.typeExp == 'deviceTypeTopicExpression'" flex="60" class="md-block">
+                                                                                    <label translate>extension.topic-type-expression</label>
+                                                                                    <input required name="mqttTopicTypeExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceTypeTopicExpression">
+                                                                                    <div ng-messages="theForm['mqttTopicTypeExp_' + brokerIndex + mapIndex].$error">
+                                                                                        <div translate ng-message="required">extension.topic-type-expression-required</div>
+                                                                                    </div>
+                                                                                </md-input-container>
+                                                                            </section>
+                                                                            <section flex layout="row">
+                                                                                <md-input-container flex="40" class="md-block">
+                                                                                    <label translate>extension.timeout</label>
+                                                                                    <input type="number" name="mqttTimeout_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.timeout" parse-to-null>
+                                                                                </md-input-container>
+                                                                                <md-input-container flex="60" class="md-block">
+                                                                                    <label translate>extension.filter-expression</label>
+                                                                                    <input required name="mqttFilterExpression{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.filterExpression">
+                                                                                    <div ng-messages="theForm['mqttFilterExpression' + brokerIndex + mapIndex].$error">
+                                                                                        <div translate ng-message="required">extension.filter-expression-required</div>
+                                                                                    </div>
+                                                                                </md-input-container>
+                                                                            </section>
+                                                                        </div>
+
+                                                                        <div ng-if='map.converterType == "custom"'>
+                                                                            <div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
+                                                                            <div flex class="tb-extension-custom-transformer-panel">
+                                                                                <div flex class="tb-extension-custom-transformer"
+                                                                                     ui-ace="extensionCustomConverterOptions"
+                                                                                     ng-model="map.converter"
+                                                                                     name="mqttCustomConverter_{{brokerIndex}}{{mapIndex}}"
+                                                                                     ng-change='validateCustomConverter(map.converter, "mqttCustomConverter_" + brokerIndex + mapIndex)'
+                                                                                     required>
+                                                                                </div>
+                                                                            </div>
+                                                                            <div class="tb-error-messages" ng-messages="theForm['mqttCustomConverter_' + brokerIndex + mapIndex].$error" role="alert">
+                                                                                <div ng-message="required" class="tb-error-message" translate>extension.converter-json-required</div>
+                                                                                <div ng-message="converterJSON" class="tb-error-message" translate>extension.converter-json-parse</div>
+                                                                            </div>
+                                                                        </div>
+
+                                                                        <v-accordion ng-if='map.converterType =="json"' id="mqtt-attributes-accordion" class="vAccordion--default">
+                                                                            <v-pane id="mqtt-attributes-pane">
+                                                                                <v-pane-header>
+                                                                                    {{ 'extension.attributes' | translate }}
+                                                                                </v-pane-header>
+                                                                                <v-pane-content>
+                                                                                    <div ng-if="map.converter.attributes.length > 0">
+                                                                                        <ol class="list-group">
+                                                                                            <li class="list-group-item" ng-repeat="(attributeIndex, attribute) in map.converter.attributes">
+                                                                                                <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attribute, map.converter.attributes)">
+                                                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                                                    <md-tooltip md-direction="top">
+                                                                                                        {{ 'action.remove' | translate }}
+                                                                                                    </md-tooltip>
+                                                                                                </md-button>
+                                                                                                <md-card>
+                                                                                                    <md-card-content>
+                                                                                                        <section flex layout="row">
+                                                                                                            <md-input-container flex="60" class="md-block">
+                                                                                                                <label translate>extension.key</label>
+                                                                                                                <input required name="mqttAttributeKey_{{brokerIndex}}{{mapIndex}}{{attributeIndex}}" ng-model="attribute.key">
+                                                                                                                <div ng-messages="theForm['mqttAttributeKey_' + brokerIndex + mapIndex + attributeIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.required-key</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                            <md-input-container flex="40" class="md-block">
+                                                                                                                <label translate>extension.type</label>
+                                                                                                                <md-select required name="mqttAttributeType_{{brokerIndex}}{{mapIndex}}{{attributeIndex}}" ng-model="attribute.type">
+                                                                                                                    <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
+                                                                                                                        {{attrTypeValue | translate}}
+                                                                                                                    </md-option>
+                                                                                                                </md-select>
+                                                                                                                <div ng-messages="theForm['mqttAttributeType_' + brokerIndex + mapIndex + attributeIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.required-type</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                        </section>
+                                                                                                        <md-input-container class="md-block">
+                                                                                                            <label translate>extension.value</label>
+                                                                                                            <input required name="mqttAttributeValue_{{brokerIndex}}{{mapIndex}}{{attributeIndex}}" ng-model="attribute.value">
+                                                                                                            <div ng-messages="theForm['mqttAttributeValue_' + brokerIndex + mapIndex + attributeIndex].$error">
+                                                                                                                <div translate ng-message="required">extension.required-value</div>
+                                                                                                            </div>
+                                                                                                        </md-input-container>
+                                                                                                    </md-card-content>
+                                                                                                </md-card>
+                                                                                            </li>
+                                                                                        </ol>
+                                                                                    </div>
+                                                                                    <div flex layout="row" layout-align="start center">
+                                                                                        <md-button class="md-primary md-raised"
+                                                                                                   ng-click="addAttribute(map.converter.attributes)" aria-label="{{ 'action.add' | translate }}">
+                                                                                            <md-tooltip md-direction="top">
+                                                                                                {{ 'extension.add-attribute' | translate }}
+                                                                                            </md-tooltip>
+                                                                                            <md-icon class="material-icons">add</md-icon>
+                                                                                            <span translate>action.add</span>
+                                                                                        </md-button>
+                                                                                    </div>
+                                                                                </v-pane-content>
+                                                                            </v-pane>
+                                                                        </v-accordion>
+
+                                                                        <v-accordion ng-if='map.converterType =="json"' id="mqtt-timeseries-accordion" class="vAccordion--default">
+                                                                            <v-pane id="mqtt-timeseries-pane">
+                                                                                <v-pane-header>
+                                                                                    {{ 'extension.timeseries' | translate }}
+                                                                                </v-pane-header>
+                                                                                <v-pane-content>
+                                                                                    <div ng-if="map.converter.timeseries.length > 0">
+                                                                                        <ol class="list-group">
+                                                                                            <li class="list-group-item" ng-repeat="(timeseriesIndex, timeseries) in map.converter.timeseries">
+                                                                                                <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(timeseries, map.converter.timeseries)">
+                                                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                                                    <md-tooltip md-direction="top">
+                                                                                                        {{ 'action.remove' | translate }}
+                                                                                                    </md-tooltip>
+                                                                                                </md-button>
+                                                                                                <md-card>
+                                                                                                    <md-card-content>
+                                                                                                        <section flex layout="row">
+                                                                                                            <md-input-container flex="60" class="md-block">
+                                                                                                                <label translate>extension.key</label>
+                                                                                                                <input required name="mqttTimeseriesKey_{{brokerIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.key">
+                                                                                                                <div ng-messages="theForm['mqtTtimeseriesKey_' + brokerIndex + mapIndex + timeseriesIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.required-key</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                            <md-input-container flex="40" class="md-block">
+                                                                                                                <label translate>extension.type</label>
+                                                                                                                <md-select required name="mqttTimeseriesType_{{brokerIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.type">
+                                                                                                                    <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
+                                                                                                                        {{attrTypeValue | translate}}
+                                                                                                                    </md-option>
+                                                                                                                </md-select>
+                                                                                                                <div ng-messages="theForm['mqttTimeseriesType_' + brokerIndex + mapIndex + timeseriesIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.required-type</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                        </section>
+                                                                                                        <md-input-container class="md-block">
+                                                                                                            <label translate>extension.value</label>
+                                                                                                            <input required name="mqttTimeseriesValue_{{brokerIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.value">
+                                                                                                            <div ng-messages="theForm['mqttTimeseriesValue_' + brokerIndex + mapIndex + timeseriesIndex].$error">
+                                                                                                                <div translate ng-message="required">extension.required-value</div>
+                                                                                                            </div>
+                                                                                                        </md-input-container>
+                                                                                                    </md-card-content>
+                                                                                                </md-card>
+                                                                                            </li>
+                                                                                        </ol>
+                                                                                    </div>
+                                                                                    <div flex layout="row" layout-align="start center">
+                                                                                        <md-button class="md-primary md-raised"
+                                                                                                   ng-click="addAttribute(map.converter.timeseries)" aria-label="{{ 'action.add' | translate }}">
+                                                                                            <md-tooltip md-direction="top">
+                                                                                                {{ 'extension.add-timeseries' | translate }}
+                                                                                            </md-tooltip>
+                                                                                            <md-icon class="material-icons">add</md-icon>
+                                                                                            <span translate>action.add</span>
+                                                                                        </md-button>
+                                                                                    </div>
+                                                                                </v-pane-content>
+                                                                            </v-pane>
+                                                                        </v-accordion>
+
+                                                                    </md-card-content>
+                                                                </md-card>
+                                                            </li>
+                                                        </ol>
+                                                    </div>
+                                                    <div flex layout="row" layout-align="start center">
+                                                        <md-button class="md-primary md-raised"
+                                                                   ng-click="addMap(broker.mapping)" aria-label="{{ 'action.add' | translate }}">
+                                                            <md-tooltip md-direction="top">
+                                                                {{ 'extension.add-map' | translate }}
+                                                            </md-tooltip>
+                                                            <md-icon class="material-icons">add</md-icon>
+                                                            <span translate>action.add</span>
+                                                        </md-button>
+                                                    </div>
+                                                </v-pane-content>
+                                            </v-pane>
+                                        </v-accordion>
+                                    </md-card-content>
+                                </md-card>
+                            </li>
+                        </ol>
+                    </div>
+
+                    <div flex layout="row" layout-align="start center">
+                        <md-button class="md-primary md-raised"
+                                   ng-click="addBroker()" aria-label="{{ 'action.add' | translate }}">
+                            <md-tooltip md-direction="top">
+                                {{ 'extension.add-broker' | translate }}
+                            </md-tooltip>
+                            <md-icon class="material-icons">add</md-icon>
+                            <span translate>action.add</span>
+                        </md-button>
+                    </div>
+                </v-pane-content>
+            </v-pane>
+        </v-accordion>
+<pre>
+{{config | json}}
+</pre>
+    </md-card-content>
+</md-card>
diff --git a/ui/src/app/extension/extension-table.scss b/ui/src/app/extension/extension-table.scss
index 3d4c54a..c6c19a5 100644
--- a/ui/src/app/extension/extension-table.scss
+++ b/ui/src/app/extension/extension-table.scss
@@ -1,4 +1,4 @@
-/*
+/**
  * Copyright © 2016-2017 The Thingsboard Authors
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/ui/src/app/extension/index.js b/ui/src/app/extension/index.js
index 78fd56d..619374f 100644
--- a/ui/src/app/extension/index.js
+++ b/ui/src/app/extension/index.js
@@ -16,10 +16,12 @@
 
 import ExtensionTableDirective from './extension-table.directive';
 import ExtensionFormHttpDirective from './extensions-forms/extension-form-http.directive';
+import ExtensionFormMqttDirective from './extensions-forms/extension-form-mqtt.directive'
 import {ParseToNull} from './extension-dialog.controller';
 
 export default angular.module('thingsboard.extension', [])
     .directive('tbExtensionTable', ExtensionTableDirective)
     .directive('tbExtensionFormHttp', ExtensionFormHttpDirective)
+    .directive('tbExtensionFormMqtt', ExtensionFormMqttDirective)
     .directive('parseToNull', ParseToNull)
     .name;
\ No newline at end of file
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index d8b861f..3ac0ebc 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -738,7 +738,7 @@ export default angular.module('thingsboard.locale', [])
                     "id": "Id",
                     "extension-id": "Extension id",
                     "extension-type": "Extension type",
-                    "transformer-json": "JSON*",
+                    "transformer-json": "JSON *",
                     "id-required": "Extension id is required.",
                     "unique-id-required": "Current extension id already exists.",
                     "type-required": "Extension type is required.",
@@ -775,6 +775,54 @@ export default angular.module('thingsboard.locale', [])
                     "add-attribute": "Add attribute",
                     "timeseries": "Timeseries",
                     "add-timeseries": "Add timeseries",
+
+                    "brokers": "Brokers",
+                    "add-broker": "Add broker",
+                    "add-broker-prompt": "Please add broker",
+                    "host": "Host",
+                    "host-required": "Host is required.",
+                    "port": "Port",
+                    "port-required": "Port is required.",
+                    "port-range": "Port should be in a range from 1 to 65535.",
+                    "ssl": "Ssl",
+                    "credentials": "Credentials",
+                    "username": "Username",
+                    "username-required": "Username is required.",
+                    "password": "Password",
+                    "password-required": "Password is required.",
+                    "retry-interval": "Retry interval",
+                    "retry-interval-required": "Retry interval is required.",
+                    "anonymous": "Anonymous",
+                    "basic": "Basic",
+                    "pem": "PEM",
+                    "ca-cert": "CA certificate file *",
+                    "private-key": "Private key file *",
+                    "cert": "Certificate file *",
+                    "no-file": "No file selected.",
+                    "drop-file": "Drop a file or click to select a file to upload.",
+                    "mapping": "Mapping",
+                    "add-map": "Add map",
+                    "topic-filter": "Topic filter",
+                    "topic-filter-required": "Topic filter is required.",
+                    "converter-type": "Converter type",
+                    "converter-type-required": "Converter type is required.",
+                    "converter-json": "Json",
+                    "name-expression": "Name expression",
+                    "type-expression": "Type expression",
+                    "json-name-expression": "Json name expression",
+                    "json-name-expression-required": "Json name expression is required.",
+                    "topic-name-expression": "Topic name expression",
+                    "topic-name-expression-required": "Topic name expression is required.",
+                    "json-type-expression": "Json type expression",
+                    "json-type-expression-required": "Json type expression is required.",
+                    "topic-type-expression": "Topic type expression",
+                    "topic-type-expression-required": "Topic type expression is required.",
+                    "topic": "Topic",
+                    "timeout": "Timeout",
+                    "converter-json-required": "Converter json is required.",
+                    "converter-json-parse": "Unable to parse converter json.",
+                    "filter-expression": "Filter expression",
+                    "filter-expression-required": "Filter expression is required."
                 },
                 "fullscreen": {
                     "expand": "Expand to fullscreen",
@@ -898,7 +946,6 @@ export default angular.module('thingsboard.locale', [])
                     "invalid-plugin-file-error": "Unable to import plugin: Invalid plugin data structure.",
                     "copyId": "Copy plugin Id",
                     "idCopiedMessage": "Plugin Id has been copied to clipboard"
-
                 },
                 "position": {
                     "top": "Top",