thingsboard-developers

Merge branch 'feature/TB-74' of github.com:thingsboard/thingsboard

11/27/2017 2:37:44 PM

Details

diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index 7bac29d..52c08d2 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -317,6 +317,35 @@ export default angular.module('thingsboard.types', [])
                     name: "event.type-stats"
                 }
             },
+            extensionType: {
+                http: "HTTP",
+                mqtt: "MQTT",
+                opc: "OPC UA"
+            },
+            extensionValueType: {
+                string: 'value.string',
+                long: 'value.long',
+                double: 'value.double',
+                boolean: 'value.boolean'
+            },
+            extensionTransformerType: {
+                toDouble: 'extension.to-double',
+                custom: 'extension.custom'
+            },
+            extensionOpcSecurityTypes: {
+                Basic128Rsa15: "Basic128Rsa15",
+                Basic256: "Basic256",
+                Basic256Sha256: "Basic256Sha256",
+                None: "None"
+            },
+            extensionIdentityType: {
+                anonymous: "anonymous",
+                username: "username"
+            },
+            extensionKeystoreType: {
+                PKCS12: "PKCS12",
+                JKS: "JKS"
+            },
             latestTelemetry: {
                 value: "LATEST_TELEMETRY",
                 name: "attribute.scope-latest-telemetry",
diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html
index b8394c7..e0760c3 100644
--- a/ui/src/app/device/devices.tpl.html
+++ b/ui/src/app/device/devices.tpl.html
@@ -67,4 +67,11 @@
                                entity-type="{{vm.types.entityType.device}}">
             </tb-relation-table>
         </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.operatingItem().additionalInfo.gateway" md-on-select="vm.grid.triggerResize()" label="{{ 'extension.extensions' | translate }}">
+            <tb-extension-table flex
+                                entity-id="vm.grid.operatingItem().id.id"
+                                entity-type="{{vm.types.entityType.device}}">
+
+            </tb-extension-table>
+        </md-tab>
 </tb-grid>
diff --git a/ui/src/app/extension/extension-dialog.controller.js b/ui/src/app/extension/extension-dialog.controller.js
new file mode 100644
index 0000000..cfec9cf
--- /dev/null
+++ b/ui/src/app/extension/extension-dialog.controller.js
@@ -0,0 +1,201 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import beautify from 'js-beautify';
+
+const js_beautify =  beautify.js;
+
+/*@ngInject*/
+export default function ExtensionDialogController($scope, $mdDialog, $translate, isAdd, allExtensions, entityId, entityType, extension, types, attributeService) {
+
+    var vm = this;
+
+    vm.types = types;
+    vm.isAdd = isAdd;
+    vm.entityType = entityType;
+    vm.entityId = entityId;
+    vm.allExtensions = allExtensions;
+
+
+    if (extension) { // Editing
+        //vm.configuration = vm.extension.configuration;
+        vm.extension = angular.copy(extension);
+        editTransformers(vm.extension);
+    } else { // Add new
+        vm.extension = {};
+    }
+
+
+    vm.extensionTypeChange = function () {
+
+        if (vm.extension.type === "HTTP") {
+            vm.extension.configuration = {
+                "converterConfigurations": []
+            };
+        }
+        if (vm.extension.type === "MQTT") {
+            vm.extension.configuration = {
+                "brokers": []
+            };
+        }
+        if (vm.extension.type === "OPC UA") {
+            vm.extension.configuration = {
+                "servers": []
+            };
+        }
+    };
+
+    vm.cancel = cancel;
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    vm.save = save;
+    function save() {
+        saveTransformers();
+
+        let $errorElement = angular.element('[name=theForm]').find('.ng-invalid');
+
+        if ($errorElement.length) {
+
+            let $mdDialogScroll = angular.element('md-dialog-content').scrollTop();
+            let $mdDialogTop = angular.element('md-dialog-content').offset().top;
+            let $errorElementTop = angular.element('[name=theForm]').find('.ng-invalid').eq(0).offset().top;
+
+
+            if ($errorElementTop !== $mdDialogTop) {
+                angular.element('md-dialog-content').animate({
+                    scrollTop: $mdDialogScroll + ($errorElementTop - $mdDialogTop) - 20
+                }, 500);
+                $errorElement.eq(0).focus();
+            }
+
+        } else {
+
+            if(vm.isAdd) {
+                vm.allExtensions.push(vm.extension);
+            } else {
+                var index = vm.allExtensions.indexOf(extension);
+                if(index > -1) {
+                    vm.allExtensions[index] = vm.extension;
+                }
+            }
+
+            var editedValue = angular.toJson(vm.allExtensions);
+
+            attributeService
+                .saveEntityAttributes(
+                    vm.entityType,
+                    vm.entityId,
+                    types.attributesScope.shared.value,
+                    [{key:"configuration", value:editedValue}]
+                )
+                .then(function success() {
+                    $scope.theForm.$setPristine();
+                    $mdDialog.hide();
+                });
+
+        }
+    }
+    
+    vm.validateId = function() {
+        var coincidenceArray = vm.allExtensions.filter(function(ext) {
+            return ext.id == vm.extension.id;
+        });
+        if(coincidenceArray.length) {
+            if(!vm.isAdd) {
+                if(coincidenceArray[0].id == extension.id) {
+                    $scope.theForm.extensionId.$setValidity('uniqueIdValidation', true);
+                } else {
+                    $scope.theForm.extensionId.$setValidity('uniqueIdValidation', false);
+                }
+            } else {
+                $scope.theForm.extensionId.$setValidity('uniqueIdValidation', false);
+            }
+        } else {
+            $scope.theForm.extensionId.$setValidity('uniqueIdValidation', true);
+        }
+    };
+
+    function saveTransformers() {
+        var config = vm.extension.configuration.converterConfigurations;
+        if(vm.extension.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"};
+                        }
+                        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;
+                    }
+                }
+            }
+        }
+    }
+
+    function editTransformers(extension) {
+        var config = extension.configuration.converterConfigurations;
+        if(extension.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].transformer){
+                            if(config[i].converters[j].attributes[k].transformer.type == "intToDouble"){
+                                config[i].converters[j].attributes[k].transformerType = "toDouble";
+                            } else {
+                                config[i].converters[j].attributes[k].transformerType = "custom";
+                                config[i].converters[j].attributes[k].transformer = js_beautify(config[i].converters[j].attributes[k].transformer, {indent_size: 4});
+                            }
+                        }
+                    }
+                    for(let l=0;l<config[i].converters[j].timeseries.length;l++) {
+                        if(config[i].converters[j].timeseries[l].transformer){
+                            if(config[i].converters[j].timeseries[l].transformer.type == "intToDouble"){
+                                config[i].converters[j].timeseries[l].transformerType = "toDouble";
+                            } else {
+                                config[i].converters[j].timeseries[l].transformerType = "custom";
+                                config[i].converters[j].timeseries[l].transformer = js_beautify(config[i].converters[j].timeseries[l].transformer, {indent_size: 4});
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+/*@ngInject*/
+export function ParseToNull() {
+    var linker = function (scope, elem, attrs, ngModel) {
+        ngModel.$parsers.push(function(value) {
+            if(value === "") {
+                return null;
+            }
+            return value;
+        })
+    };
+    return {
+        restrict: "A",
+        link: linker,
+        require: "ngModel"
+    }
+}
\ No newline at end of file
diff --git a/ui/src/app/extension/extension-dialog.tpl.html b/ui/src/app/extension/extension-dialog.tpl.html
new file mode 100644
index 0000000..2336a60
--- /dev/null
+++ b/ui/src/app/extension/extension-dialog.tpl.html
@@ -0,0 +1,84 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<md-dialog class="extensionDialog" aria-label="{{ (vm.isAdd ? 'extension.add' : 'extension.edit' ) | translate }}">
+    <form name="theForm" ng-submit="vm.save()" novalidate>
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>{{ vm.isAdd ? 'extension.add' : 'extension.edit'}}</h2>
+                <span flex></span>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+
+        <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+
+        <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <md-content class="md-padding" layout="column">
+                    <fieldset ng-disabled="loading">
+                        <section flex layout="row">
+                            <md-input-container flex="60" class="md-block">
+                                <label translate>extension.extension-id</label>
+                                <input required name="extensionId" ng-model="vm.extension.id" ng-change="vm.validateId()">
+                                <div ng-messages="theForm.extensionId.$error">
+                                    <div translate ng-message="required">extension.id-required</div>
+                                    <div translate ng-message="uniqueIdValidation">extension.unique-id-required</div>
+                                </div>
+                            </md-input-container>
+
+                            <md-input-container flex="40" class="md-block">
+                                <label translate>extension.extension-type</label>
+
+                                <md-select ng-disabled="!vm.isAdd" required name="extensionType" ng-change="vm.extensionTypeChange()" ng-model="vm.extension.type">
+                                    <md-option ng-repeat="(key,value) in vm.types.extensionType" ng-value="value">
+                                        {{value}}
+                                    </md-option>
+                                </md-select>
+
+                                <div ng-messages="theForm.extensionType.$error">
+                                    <div translate ng-message="required">extension.type-required</div>
+                                </div>
+                            </md-input-container>
+                        </section>
+
+                        <div tb-extension-form-http        config="vm.extension.configuration" is-add="vm.isAdd" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.http"></div>
+
+                        <div tb-extension-form-opc  configuration="vm.extension.configuration"                   ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.opc"></div>
+                    </fieldset>
+                    <!--<div>{{vm.extension}}</div>-->
+                </md-content>
+            </div>
+        </md-dialog-content>
+
+        <md-dialog-actions layout="row">
+            <md-button type="submit"
+                       class="md-raised md-primary"
+            >
+                {{ (vm.isAdd  ? 'action.add' : 'action.save') | translate }}
+            </md-button>
+
+            <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
+
diff --git a/ui/src/app/extension/extensions-forms/extension-form.scss b/ui/src/app/extension/extensions-forms/extension-form.scss
new file mode 100644
index 0000000..142516c
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form.scss
@@ -0,0 +1,56 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.extension-form {
+  li > .md-button {
+    color: rgba(0, 0, 0, 0.7);
+    margin: 0;
+  }
+  .vAccordion--default {
+    margin-top: 0;
+    padding-left: 3px;
+  }
+}
+
+.tb-extension-custom-transformer-panel {
+  margin-left: 15px;
+  border: 1px solid #C0C0C0;
+  height: 100%;
+  .tb-extension-custom-transformer {
+    min-width: 600px;
+    min-height: 200px;
+    width: 100%;
+    height: 100%;
+  }
+  .ace_text-input {
+    position:absolute!important
+  }
+}
+
+.extensionDialog {
+  min-width: 1000px;
+}
+
+.tb-container-for-select {
+  height: 58px;
+}
+
+.tb-drop-file-input-hide {
+  height: 200%;
+  display: block;
+  position: absolute;
+  bottom: 0;
+  width: 100%;
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..4c09078
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form-http.directive.js
@@ -0,0 +1,163 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'brace/ext/language_tools';
+import 'brace/mode/json';
+import 'brace/theme/github';
+
+import './extension-form.scss';
+
+/* eslint-disable angular/log */
+
+import extensionFormHttpTemplate from './extension-form-http.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function ExtensionFormHttpDirective($compile, $templateCache, $translate, types) {
+
+    var linker = function(scope, element) {
+
+        var template = $templateCache.get(extensionFormHttpTemplate);
+        element.html(template);
+
+        scope.types = types;
+        scope.theForm = scope.$parent.theForm;
+
+        scope.extensionCustomTransformerOptions = {
+            useWrapMode: false,
+            mode: 'json',
+            showGutter: true,
+            showPrintMargin: true,
+            theme: 'github',
+            advanced: {
+                enableSnippets: true,
+                enableBasicAutocompletion: true,
+                enableLiveAutocompletion: true
+            },
+            onLoad: function(_ace) {
+                _ace.$blockScrolling = 1;
+            }
+        };
+
+
+        scope.addConverterConfig = function() {
+            var newConverterConfig = {converterId:"", converters:[]};
+            scope.converterConfigs.push(newConverterConfig);
+
+            scope.converterConfigs[scope.converterConfigs.length - 1].converters = [];
+            scope.addConverter(scope.converterConfigs[scope.converterConfigs.length - 1].converters);
+        };
+
+        scope.removeConverterConfig = function(config) {
+            var index = scope.converterConfigs.indexOf(config);
+            if (index > -1) {
+                scope.converterConfigs.splice(index, 1);
+            }
+            scope.theForm.$setDirty();
+        };
+
+        scope.addConverter = function(converters) {
+            var newConverter = {
+                deviceNameJsonExpression:"",
+                deviceTypeJsonExpression:"",
+                attributes:[],
+                timeseries:[]
+            };
+            converters.push(newConverter);
+        };
+
+        scope.removeConverter = function(converter, converters) {
+            var index = converters.indexOf(converter);
+            if (index > -1) {
+                converters.splice(index, 1);
+            }
+            scope.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();
+        };
+
+
+
+
+
+        if(scope.isAdd) {
+            scope.converterConfigs = scope.config.converterConfigurations;
+            scope.addConverterConfig();
+        } else {
+            scope.converterConfigs = scope.config.converterConfigurations;
+        }
+
+
+
+        scope.updateValidity = function() {
+            let valid = scope.converterConfigs && scope.converterConfigs.length > 0;
+            scope.theForm.$setValidity('converterConfigs', valid);
+            if(scope.converterConfigs.length) {
+                for(let i=0; i<scope.converterConfigs.length; i++) {
+                    if(!scope.converterConfigs[i].converters.length) {
+                        scope.theForm.$setValidity('converters', false);
+                        break;
+                    } else {
+                        scope.theForm.$setValidity('converters', true);
+                    }
+                }
+            }
+        };
+
+        scope.$watch('converterConfigs', function() {
+            scope.updateValidity();
+        }, true);
+
+
+        scope.transformerTypeChange = function(attribute) {
+            attribute.transformer = "";
+        };
+
+        scope.validateTransformer = function (model, editorName) {
+            if(model && model.length) {
+                try {
+                    angular.fromJson(model);
+                    scope.theForm[editorName].$setValidity('transformerJSON', true);
+                } catch(e) {
+                    scope.theForm[editorName].$setValidity('transformerJSON', false);
+                }
+            }
+        };
+        
+        $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-http.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html
new file mode 100644
index 0000000..6416656
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html
@@ -0,0 +1,329 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<md-card class="extension-form extension-http">
+    <md-card-title>
+        <md-card-title-text>
+            <span translate class="md-headline">extension.configuration</span>
+        </md-card-title-text>
+    </md-card-title>
+    <md-card-content>
+        <v-accordion id="http-converter-configs-accordion" class="vAccordion--default">
+            <v-pane id="http-converters-pane" expanded="isAdd">
+                <v-pane-header>
+                    {{ 'extension.converter-configurations' | translate }}
+                </v-pane-header>
+                <v-pane-content>
+                    <div ng-if="converterConfigs.length === 0">
+                        <span translate layout-align="center center" class="tb-prompt">extension.add-config-prompt</span>
+                    </div>
+                    <div ng-if="converterConfigs.length > 0">
+                        <ol class="list-group">
+                            <li class="list-group-item" ng-repeat="(configIndex, config) in converterConfigs">
+                                <md-button aria-label="{{ 'action.remove' | translate }}"
+                                           class="md-icon-button"
+                                           ng-click="removeConverterConfig(config)"
+                                           ng-hide="converterConfigs.length < 2"
+                                >
+                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                    <md-tooltip md-direction="top">
+                                        {{ 'action.remove' | translate }}
+                                    </md-tooltip>
+                                </md-button>
+                                <md-card>
+                                    <md-card-content>
+
+                                        <md-input-container class="md-block">
+                                            <label translate>extension.converter-id</label>
+                                            <input required name="httpConverterId_{{configIndex}}" ng-model="config.converterId">
+                                            <div ng-messages="theForm['httpConverterId_' + configIndex].$error">
+                                                <div translate ng-message="required">extension.converter-id-required</div>
+                                            </div>
+                                        </md-input-container>
+                                        <md-input-container class="md-block">
+                                            <label translate>extension.token</label>
+                                            <input name="httpToken" ng-model="config.token" parse-to-null>
+                                        </md-input-container>
+                                        <v-accordion id="http-converters-accordion" class="vAccordion--default">
+                                            <v-pane id="http-converters-pane">
+                                                <v-pane-header>
+                                                    {{ 'extension.converters' | translate }}
+                                                </v-pane-header>
+                                                <v-pane-content>
+                                                    <div ng-if="config.converters.length === 0">
+                                                        <span translate layout-align="center center" class="tb-prompt">extension.add-converter-prompt</span>
+                                                    </div>
+                                                    <div ng-if="config.converters.length > 0">
+                                                        <ol class="list-group">
+                                                            <li class="list-group-item"
+                                                                ng-repeat="(converterIndex,converter) in config.converters"
+                                                            >
+                                                                <md-button aria-label="{{ 'action.remove' | translate }}"
+                                                                           class="md-icon-button"
+                                                                           ng-click="removeConverter(converter, config.converters)"
+                                                                           ng-hide="config.converters.length < 2"
+                                                                >
+                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                    <md-tooltip md-direction="top">
+                                                                        {{ 'action.remove' | translate }}
+                                                                    </md-tooltip>
+                                                                </md-button>
+                                                                <md-card>
+                                                                    <md-card-content>
+                                                                        <md-input-container class="md-block">
+                                                                            <label translate>extension.device-name-expression</label>
+                                                                            <input required name="httpDeviceNameExp_{{configIndex}}{{converterIndex}}" ng-model="converter.deviceNameJsonExpression">
+                                                                            <div ng-messages="theForm['httpDeviceNameExp_' + configIndex + converterIndex].$error">
+                                                                                <div translate ng-message="required">extension.device-name-expression-required</div>
+                                                                            </div>
+                                                                        </md-input-container>
+                                                                        <md-input-container class="md-block">
+                                                                            <label translate>extension.device-type-expression</label>
+                                                                            <input required name="httpDeviceTypeExp_{{configIndex}}{{converterIndex}}" ng-model="converter.deviceTypeJsonExpression">
+                                                                            <div ng-messages="theForm['httpDeviceTypeExp_' + configIndex + converterIndex].$error">
+                                                                                <div translate ng-message="required">extension.device-type-expression-required</div>
+                                                                            </div>
+                                                                        </md-input-container>
+
+                                                                        <v-accordion id="http-attributes-accordion" class="vAccordion--default">
+                                                                            <v-pane id="http-attributes-pane">
+                                                                                <v-pane-header>
+                                                                                    {{ 'extension.attributes' | translate }}
+                                                                                </v-pane-header>
+                                                                                <v-pane-content>
+                                                                                    <div ng-if="converter.attributes.length > 0">
+                                                                                        <ol class="list-group">
+                                                                                            <li class="list-group-item" ng-repeat="(attributeIndex, attribute) in converter.attributes">
+                                                                                                <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attribute, converter.attributes)">
+                                                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                                                    <md-tooltip md-direction="top">
+                                                                                                        {{ 'action.remove' | translate }}
+                                                                                                    </md-tooltip>
+                                                                                                </md-button>
+                                                                                                <md-card>
+                                                                                                    <md-card-content>
+                                                                                                        <section flex layout="row">
+                                                                                                            <md-input-container flex="60" class="md-block">
+                                                                                                                <label translate>extension.key</label>
+                                                                                                                <input required name="httpAttributeKey_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.key">
+                                                                                                                <div ng-messages="theForm['httpAttributeKey_' + configIndex + converterIndex + attributeIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.required-key</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                            <md-input-container flex="40" class="md-block">
+                                                                                                                <label translate>extension.type</label>
+                                                                                                                <md-select required name="httpAttributeType_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.type">
+                                                                                                                    <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
+                                                                                                                        {{attrTypeValue | translate}}
+                                                                                                                    </md-option>
+                                                                                                                </md-select>
+                                                                                                                <div ng-messages="theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.required-type</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                        </section>
+                                                                                                        <section flex layout="row">
+                                                                                                            <md-input-container flex="60" class="md-block">
+                                                                                                                <label translate>extension.value</label>
+                                                                                                                <input required name="httpAttributeValue_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.value">
+                                                                                                                <div ng-messages="theForm['httpAttributeValue_' + configIndex + converterIndex + attributeIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.required-value</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+
+
+                                                                                                            <md-input-container flex="40" class="md-block">
+                                                                                                                <label translate>extension.transformer</label>
+                                                                                                                <md-select name="httpAttributeTransformer" ng-model="attribute.transformerType" ng-change="transformerTypeChange(attribute)">
+                                                                                                                    <md-option ng-repeat="(transformerType, value) in types.extensionTransformerType" ng-value="transformerType">
+                                                                                                                        {{value | translate}}
+                                                                                                                    </md-option>
+                                                                                                                </md-select>
+                                                                                                            </md-input-container>
+                                                                                                        </section>
+
+                                                                                                        <div ng-if='attribute.transformerType == "custom"'>
+                                                                                                            <div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
+                                                                                                            <div flex class="tb-extension-custom-transformer-panel">
+                                                                                                                <div flex class="tb-extension-custom-transformer"
+                                                                                                                     ui-ace="extensionCustomTransformerOptions"
+                                                                                                                     ng-model="attribute.transformer"
+                                                                                                                     name="attributeCustomTransformer_{{configIndex}}{{converterIndex}}{{attributeIndex}}"
+                                                                                                                     ng-change='validateTransformer(attribute.transformer,"attributeCustomTransformer_" + configIndex + converterIndex + attributeIndex)'
+                                                                                                                     required>
+                                                                                                                </div>
+                                                                                                            </div>
+                                                                                                            <div class="tb-error-messages" ng-messages="theForm['attributeCustomTransformer_' + configIndex + converterIndex + attributeIndex].$error" role="alert">
+                                                                                                                <div ng-message="required" class="tb-error-message" translate>extension.json-required</div>
+                                                                                                                <div ng-message="transformerJSON" class="tb-error-message" translate>extension.json-parse</div>
+                                                                                                            </div>
+                                                                                                        </div>
+
+
+                                                                                                    </md-card-content>
+                                                                                                </md-card>
+                                                                                            </li>
+                                                                                        </ol>
+                                                                                    </div>
+                                                                                    <div flex layout="row" layout-align="start center">
+                                                                                        <md-button class="md-primary md-raised"
+                                                                                                   ng-click="addAttribute(converter.attributes)" aria-label="{{ 'action.add' | translate }}">
+                                                                                            <md-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 id="http-timeseries-accordion" class="vAccordion--default">
+                                                                            <v-pane id="http-timeseries-pane">
+                                                                                <v-pane-header>
+                                                                                    {{ 'extension.timeseries' | translate }}
+                                                                                </v-pane-header>
+                                                                                <v-pane-content>
+                                                                                    <div ng-if="converter.timeseries.length > 0">
+                                                                                        <ol class="list-group">
+                                                                                            <li class="list-group-item" ng-repeat="(timeseriesIndex, timeseries) in converter.timeseries">
+                                                                                                <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(timeseries, converter.timeseries)">
+                                                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                                                    <md-tooltip md-direction="top">
+                                                                                                        {{ 'action.remove' | translate }}
+                                                                                                    </md-tooltip>
+                                                                                                </md-button>
+                                                                                                <md-card>
+                                                                                                    <md-card-content>
+                                                                                                        <section flex layout="row">
+                                                                                                            <md-input-container flex="60" class="md-block">
+                                                                                                                <label translate>extension.key</label>
+                                                                                                                <input required name="httpTimeseriesKey_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.key">
+                                                                                                                <div ng-messages="theForm['httpTimeseriesKey_' + configIndex + converterIndex + timeseriesIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.required-key</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                            <md-input-container flex="40" class="md-block">
+                                                                                                                <label translate>extension.type</label>
+                                                                                                                <md-select required name="httpTimeseriesType_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.type">
+                                                                                                                    <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
+                                                                                                                        {{attrTypeValue | translate}}
+                                                                                                                    </md-option>
+                                                                                                                </md-select>
+                                                                                                                <div ng-messages="theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.required-type</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                        </section>
+                                                                                                        <section flex layout="row">
+                                                                                                            <md-input-container flex="60" class="md-block">
+                                                                                                                <label translate>extension.value</label>
+                                                                                                                <input required name="httpTimeseriesValue_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.value">
+                                                                                                                <div ng-messages="theForm['httpTimeseriesValue_' + configIndex + converterIndex + timeseriesIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.required-value</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+
+
+                                                                                                            <md-input-container flex="40" class="md-block">
+                                                                                                                <label translate>extension.transformer</label>
+                                                                                                                <md-select name="httpTimeseriesTransformer" ng-model="timeseries.transformerType" ng-change="transformerTypeChange(timeseries)">
+                                                                                                                    <md-option ng-repeat="(transformerType, value) in types.extensionTransformerType" ng-value="transformerType">
+                                                                                                                        {{value | translate}}
+                                                                                                                    </md-option>
+                                                                                                                </md-select>
+                                                                                                            </md-input-container>
+                                                                                                        </section>
+
+                                                                                                        <div ng-if='timeseries.transformerType == "custom"'>
+                                                                                                            <div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
+                                                                                                            <div flex class="tb-extension-custom-transformer-panel">
+                                                                                                                <div flex class="tb-extension-custom-transformer"
+                                                                                                                     ui-ace="extensionCustomTransformerOptions"
+                                                                                                                     ng-model="timeseries.transformer"
+                                                                                                                     name="timeseriesCustomTransformer_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}"
+                                                                                                                     ng-change='validateTransformer(timeseries.transformer,"timeseriesCustomTransformer_" + configIndex + converterIndex + timeseriesIndex)'
+                                                                                                                     required>
+                                                                                                                </div>
+                                                                                                            </div>
+                                                                                                            <div class="tb-error-messages" ng-messages="theForm['timeseriesCustomTransformer_' + configIndex + converterIndex + timeseriesIndex].$error" role="alert">
+                                                                                                                <div ng-message="required" class="tb-error-message" translate>extension.json-required</div>
+                                                                                                                <div ng-message="transformerJSON" class="tb-error-message" translate>extension.json-parse</div>
+                                                                                                            </div>
+                                                                                                        </div>
+
+
+                                                                                                    </md-card-content>
+                                                                                                </md-card>
+                                                                                            </li>
+                                                                                        </ol>
+                                                                                    </div>
+                                                                                    <div flex layout="row" layout-align="start center">
+                                                                                        <md-button class="md-primary md-raised"
+                                                                                                   ng-click="addAttribute(converter.timeseries)" aria-label="{{ 'action.add' | translate }}">
+                                                                                            <md-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="addConverter(config.converters)" aria-label="{{ 'action.add' | translate }}">
+                                                            <md-tooltip md-direction="top">
+                                                                {{ 'extension.add-converter' | 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="addConverterConfig()" aria-label="{{ 'action.add' | translate }}">
+                            <md-tooltip md-direction="top">
+                                {{ 'extension.add-config' | 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>
+        <!--{{config}}-->
+    </md-card-content>
+</md-card>
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
new file mode 100644
index 0000000..e55ad6c
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html
@@ -0,0 +1,18 @@
+<!--
+
+    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.
+
+-->
+<div>MQTT</div>
\ No newline at end of file
diff --git a/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js b/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js
new file mode 100644
index 0000000..7eeeb29
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js
@@ -0,0 +1,161 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'brace/ext/language_tools';
+import 'brace/mode/json';
+import 'brace/theme/github';
+
+import './extension-form.scss';
+
+/* eslint-disable angular/log */
+
+import extensionFormOpcTemplate from './extension-form-opc.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function ExtensionFormOpcDirective($compile, $templateCache, $translate, types) {
+
+
+    var linker = function(scope, element) {
+
+
+        function Server() {
+            this.applicationName = "Thingsboard OPC-UA client";
+            this.applicationUri = "";
+            this.host = "localhost";
+            this.port = 49320;
+            this.scanPeriodInSeconds = 10;
+            this.timeoutInMillis = 5000;
+            this.security = "Basic128Rsa15";
+            this.identity = {
+                "type": "anonymous"
+            };
+            this.keystore = {
+                "type": "PKCS12",
+                "location": "example.pfx",
+                "password": "secret",
+                "alias": "gateway",
+                "keyPassword": "secret"
+            };
+            this.mapping = []
+        }
+
+        function Map() {
+            this.deviceNodePattern = "Channel1\\.Device\\d+$";
+            this.deviceNamePattern = "Device ${_System._DeviceId}";
+            this.attributes = [];
+            this.timeseries = [];
+        }
+
+        function Attribute() {
+            this.key = "Tag1";
+            this.type = "string";
+            this.value = "${Tag1}";
+        }
+
+        function Timeseries() {
+            this.key = "Tag2";
+            this.type = "long";
+            this.value = "${Tag2}";
+        }
+
+
+        var template = $templateCache.get(extensionFormOpcTemplate);
+        element.html(template);
+
+        scope.types = types;
+        scope.theForm = scope.$parent.theForm;
+
+
+        if (!scope.configuration.servers.length) {
+            scope.configuration.servers.push(new Server());
+        }
+
+        scope.addServer = function(serversList) {
+            serversList.push(new Server());
+            // scope.addMap(serversList[serversList.length-1].mapping);
+
+            scope.theForm.$setDirty();
+        };
+
+        scope.addMap = function(mappingList) {
+            mappingList.push(new Map());
+            scope.theForm.$setDirty();
+        };
+
+        scope.addNewAttribute = function(attributesList) {
+            attributesList.push(new Attribute());
+            scope.theForm.$setDirty();
+        };
+
+        scope.addNewTimeseries = function(timeseriesList) {
+            timeseriesList.push(new Timeseries());
+            scope.theForm.$setDirty();
+        };
+
+
+        scope.removeItem = (item, itemList) => {
+            var index = itemList.indexOf(item);
+            if (index > -1) {
+                itemList.splice(index, 1);
+            }
+            scope.theForm.$setDirty();
+        };
+
+
+        $compile(element.contents())(scope);
+
+
+        scope.fileAdded = function($file, model, options) {
+            let reader = new FileReader();
+            reader.onload = function(event) {
+                scope.$apply(function() {
+                    if(event.target.result) {
+                        scope.theForm.$setDirty();
+                        let addedFile = event.target.result;
+
+                        if (addedFile && addedFile.length > 0) {
+                            model[options.fileName] = $file.name;
+                            model[options.file] = addedFile.replace(/^data.*base64,/, "");
+
+                        }
+                    }
+                });
+            };
+            reader.readAsDataURL($file.file);
+
+        };
+
+        scope.clearFile = function(model, options) {
+            scope.theForm.$setDirty();
+
+            model[options.fileName] = null;
+            model[options.file] = null;
+
+        };
+
+    };
+
+    return {
+        restrict: "A",
+        link: linker,
+        scope: {
+            configuration: "=",
+            isAdd: "="
+        }
+    }
+}
\ No newline at end of file
diff --git a/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
new file mode 100644
index 0000000..8c959db
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
@@ -0,0 +1,566 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<md-card class="extension-form extension-opc">
+    <md-card-title>
+        <md-card-title-text>
+            <span translate class="md-headline">extension.configuration</span>
+        </md-card-title-text>
+    </md-card-title>
+
+    <md-card-content>
+        <v-accordion id="http-server-configs-accordion" class="vAccordion--default">
+            <v-pane id="http-servers-pane" expanded="true">
+                <v-pane-header>
+                    {{ 'extension.opc-server' | translate }}
+                </v-pane-header>
+
+                <v-pane-content>
+                    <div ng-if="configuration.servers.length === 0">
+                        <span translate layout-align="center center" class="tb-prompt">extension.opc-add-server-prompt</span>
+                    </div>
+
+                    <div ng-if="configuration.servers.length > 0">
+                        <ol class="list-group">
+                            <li class="list-group-item" ng-repeat="(serverIndex, server) in configuration.servers">
+                                <md-button aria-label="{{ 'action.remove' | translate }}"
+                                           class="md-icon-button"
+                                           ng-click="removeItem(server, configuration.servers)"
+                                           ng-hide="configuration.servers.length < 2"
+                                >
+                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                    <md-tooltip md-direction="top">
+                                        {{ 'action.remove' | translate }}
+                                    </md-tooltip>
+                                </md-button>
+
+                                <md-card>
+                                    <md-card-content>
+
+                                        <div layout="row">
+                                            <md-input-container flex="50" class="md-block">
+                                                <label translate>extension.opc-application-name</label>
+                                                <input required name="applicationName_{{serverIndex}}" ng-model="server.applicationName">
+                                                <div ng-messages="theForm['applicationName_' + serverIndex].$error">
+                                                    <div translate ng-message="required">extension.opc-field-required</div>
+                                                </div>
+                                            </md-input-container>
+
+
+                                            <md-input-container flex="50" class="md-block">
+                                                <label translate>extension.opc-application-uri</label>
+                                                <input required name="applicationUri_{{serverIndex}}" ng-model="server.applicationUri">
+                                                <div ng-messages="theForm['applicationUri_' + serverIndex].$error">
+                                                    <div translate ng-message="required">extension.opc-field-required</div>
+                                                </div>
+                                            </md-input-container>
+                                        </div>
+
+
+                                        <div layout="row">
+                                            <md-input-container flex="50" class="md-block">
+                                                <label translate>extension.opc-host</label>
+                                                <input required name="host_{{serverIndex}}" ng-model="server.host">
+                                                <div ng-messages="theForm['host_' + serverIndex].$error">
+                                                    <div translate ng-message="required">extension.opc-field-required</div>
+                                                </div>
+                                            </md-input-container>
+
+                                            <md-input-container flex="50" class="md-block">
+                                                <label translate>extension.opc-port</label>
+                                                <input type="number"
+                                                       required
+                                                       name="port_{{serverIndex}}"
+                                                       ng-model="server.port"
+                                                       min="1"
+                                                       max="65535"
+                                                >
+                                                <div ng-messages="theForm['port_' + serverIndex].$error">
+                                                    <div translate
+                                                         ng-message="required"
+                                                    >extension.opc-field-required</div>
+                                                    <div translate
+                                                         ng-message="min"
+                                                    >Port should be in a range from 1 to 65535</div>
+                                                    <div translate
+                                                         ng-message="max"
+                                                    >Port should be in a range from 1 to 65535</div>
+                                                </div>
+                                            </md-input-container>
+                                        </div>
+
+                                        <div layout="row">
+                                            <md-input-container flex="50" class="md-block">
+                                                <label translate>extension.opc-scan-period-in-seconds</label>
+                                                <input type="number"
+                                                       required
+                                                       name="scanPeriodInSeconds_{{serverIndex}}"
+                                                       ng-model="server.scanPeriodInSeconds">
+                                                <div ng-messages="theForm['scanPeriodInSeconds_' + serverIndex].$error">
+                                                    <div translate
+                                                         ng-message="required"
+                                                    >extension.opc-field-required</div>
+                                                </div>
+                                            </md-input-container>
+
+                                            <md-input-container flex="50" class="md-block">
+                                                <label translate>extension.opc-timeout-in-millis</label>
+                                                <input type="number"
+                                                       required name="timeoutInMillis_{{serverIndex}}"
+                                                       ng-model="server.timeoutInMillis"
+                                                >
+                                                <div ng-messages="theForm['timeoutInMillis_' + serverIndex].$error">
+                                                    <div translate
+                                                         ng-message="required"
+                                                    >extension.opc-field-required</div>
+                                                </div>
+                                            </md-input-container>
+                                        </div>
+
+                                        <div layout="row">
+
+                                            <md-input-container flex="50" class="md-block tb-container-for-select">
+                                                <label translate>extension.opc-security</label>
+                                                <md-select required
+                                                           name="securityType_{{serverIndex}}"
+                                                           ng-model="server.security">
+                                                    <md-option ng-value="securityType"
+                                                               ng-repeat="(securityType, securityValue) in types.extensionOpcSecurityTypes"
+                                                    ><span ng-bind="::securityValue"></span></md-option>
+                                                </md-select>
+                                                <div ng-messages="theForm['securityType_' + serverIndex].$error">
+                                                    <div translate
+                                                         ng-message="required"
+                                                    >extension.opc-field-required</div>
+                                                </div>
+                                            </md-input-container>
+
+                                            <md-input-container flex="50" class="md-block tb-container-for-select">
+                                                <label translate>extension.opc-identity</label>
+                                                <md-select required
+                                                           name="identityType_{{serverIndex}}"
+                                                           ng-model="server.identity.type"
+                                                >
+                                                    <md-option ng-value="identityType"
+                                                               ng-repeat="(identityType, identityValue) in types.extensionIdentityType"
+                                                    ><span ng-bind="::identityValue"></span></md-option>
+                                                </md-select>
+                                                <div ng-messages="theForm['identityType_' + serverIndex].$error">
+                                                    <div translate
+                                                         ng-message="required"
+                                                    >extension.opc-field-required</div>
+                                                </div>
+                                            </md-input-container>
+                                        </div>
+
+
+                                        <div ng-if="server.identity.type != 'username'">
+                                            <span class=""
+                                                  ng-init="server.identity = {'type':'anonymous'}"></span>
+                                        </div>
+                                        <div layout="row" ng-if="server.identity.type == 'username'">
+                                            <md-input-container flex="50" class="md-block">
+                                                <label translate>extension.opc-username</label>
+                                                <input required
+                                                       name="identityUsername_{{serverIndex}}"
+                                                       ng-model="server.identity.username"
+                                                >
+                                                <div ng-messages="theForm['identityUsername_' + serverIndex].$error">
+                                                    <div translate
+                                                         ng-message="required"
+                                                    >extension.opc-field-required</div>
+                                                </div>
+                                            </md-input-container>
+
+                                            <md-input-container flex="50" class="md-block">
+                                                <label translate>extension.opc-password</label>
+                                                <input required
+                                                       name="identityPassword_{{serverIndex}}" ng-model="server.identity.password">
+                                                <div ng-messages="theForm['identityPassword_' + serverIndex].$error">
+                                                    <div translate
+                                                         ng-message="required"
+                                                    >extension.opc-field-required</div>
+                                                </div>
+                                            </md-input-container>
+                                        </div>
+
+
+                                        <v-accordion id="opc-attributes-accordion" class="vAccordion--default">
+                                            <v-pane id="opc-attributes-pane">
+                                                <v-pane-header>
+                                                    {{ 'extension.opc-keystore' | translate }}
+                                                </v-pane-header>
+                                                <v-pane-content>
+
+                                                    <md-input-container class="md-block tb-container-for-select">
+                                                        <label translate>extension.opc-keystore-type</label>
+                                                        <md-select required name="keystoreType_{{serverIndex}}" ng-model="server.keystore.type">
+                                                            <md-option ng-value="keystoreType" ng-repeat="(keystoreType, keystoreValue) in types.extensionKeystoreType"><span ng-bind="::keystoreValue"></span></md-option>
+                                                        </md-select>
+                                                        <div ng-messages="theForm['keystoreType_'+serverIndex].$error">
+                                                            <div translate ng-message="required">extension.opc-field-required</div>
+                                                        </div>
+                                                    </md-input-container>
+
+                                                    <div class="tb-container" ng-class="{'ng-invalid':!server.keystore.file}">
+                                                        <span ng-init='fieldsToFill = {"fileName":"fileName", "file":"file"}'></span>
+                                                        <label class="tb-label" translate>extension.opc-keystore-location</label>
+                                                        <div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, server.keystore, fieldsToFill)' class="tb-file-select-container">
+                                                            <div class="tb-file-clear-container">
+                                                                <md-button ng-click='clearFile(server.keystore, fieldsToFill)' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
+                                                                    <md-tooltip md-direction="top">
+                                                                        {{ 'action.remove' | translate }}
+                                                                    </md-tooltip>
+                                                                    <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
+                                                                </md-button>
+                                                            </div>
+                                                            <div class="alert tb-flow-drop" flow-drop>
+                                                                <label for="dropFileKeystore_{{serverIndex}}" translate>extension.drop-file</label>
+                                                                <input flow-attrs="{accept:'.pfx,.p12'}"
+                                                                       type="file"
+                                                                       class="file-input"
+                                                                       flow-btn id="dropFileKeystore_{{serverIndex}}"
+                                                                       name="keystoreFile"
+                                                                       ng-model="server.keystore.file"
+                                                                >
+                                                            </div>
+                                                        </div>
+                                                    </div>
+                                                    <div>
+                                                        <div ng-if="!server.keystore[fieldsToFill.fileName]" class="tb-error-message" translate>extension.no-file</div>
+                                                        <div ng-if="server.keystore[fieldsToFill.fileName]">{{server.keystore[fieldsToFill.fileName]}}</div>
+                                                    </div>
+
+
+                                                    <div flex layout="row">
+                                                        <md-input-container flex="50" class="md-block">
+                                                            <label translate>extension.opc-keystore-password</label>
+                                                            <input required name="keystorePassword_{{serverIndex}}" ng-model="server.keystore.password">
+                                                            <div ng-messages="theForm['keystorePassword_' + serverIndex].$error">
+                                                                <div translate ng-message="required">extension.opc-field-required</div>
+                                                            </div>
+                                                        </md-input-container>
+
+                                                        <md-input-container flex="50" class="md-block">
+                                                            <label translate>extension.opc-keystore-alias</label>
+                                                            <input required name="keystoreAlias_{{serverIndex}}" ng-model="server.keystore.alias">
+                                                            <div ng-messages="theForm['keystoreAlias_' + serverIndex].$error">
+                                                                <div translate ng-message="required">extension.opc-field-required</div>
+                                                            </div>
+                                                        </md-input-container>
+                                                    </div>
+
+                                                    <md-input-container class="md-block">
+                                                        <label translate>extension.opc-keystore-key-password</label>
+                                                        <input required name="keystoreKeyPassword_{{serverIndex}}" ng-model="server.keystore.keyPassword">
+                                                        <div ng-messages="theForm['keystoreKeyPassword_' + serverIndex].$error">
+                                                            <div translate ng-message="required">extension.opc-field-required</div>
+                                                        </div>
+                                                    </md-input-container>
+
+                                                </v-pane-content>
+                                            </v-pane>
+                                        </v-accordion>
+
+
+                                        <v-accordion id="opc-attributes-accordion"
+                                                     class="vAccordion--default"
+                                        >
+                                            <v-pane id="opc-attributes-pane">
+                                                <v-pane-header>
+                                                    {{ 'extension.opc-mapping' | translate }}
+                                                </v-pane-header>
+                                                <v-pane-content>
+                                                    <div ng-if="server.mapping.length > 0">
+                                                        <ol class="list-group">
+                                                            <li class="list-group-item"
+                                                                ng-repeat="(mapIndex, map) in server.mapping"
+                                                            >
+                                                                <md-button aria-label="{{ 'action.remove' | translate }}"
+                                                                           class="md-icon-button"
+                                                                           ng-click="removeItem(map, server.mapping)"
+                                                                >
+                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                    <md-tooltip md-direction="top">
+                                                                        {{ 'action.remove' | translate }}
+                                                                    </md-tooltip>
+                                                                </md-button>
+
+                                                                <md-card>
+                                                                    <md-card-content>
+                                                                        <div flex layout="row">
+                                                                            <md-input-container flex="50" class="md-block">
+                                                                                <label translate>extension.opc-device-node-pattern</label>
+                                                                                <input required
+                                                                                       name="deviceNodePattern_{{serverIndex}}{{mapIndex}}"
+                                                                                       ng-model="map.deviceNodePattern"
+                                                                                >
+                                                                                <div ng-messages="theForm['deviceNodePattern_' + serverIndex + mapIndex].$error">
+                                                                                    <div translate
+                                                                                         ng-message="required"
+                                                                                    >extension.opc-field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+
+                                                                            <md-input-container flex="50" class="md-block">
+                                                                                <label translate>extension.opc-device-name-pattern</label>
+                                                                                <input required
+                                                                                       name="deviceNamePattern_{{serverIndex}}{{mapIndex}}"
+                                                                                       ng-model="map.deviceNamePattern"
+                                                                                >
+                                                                                <div ng-messages="theForm['deviceNamePattern_' + serverIndex + mapIndex].$error">
+                                                                                    <div translate
+                                                                                         ng-message="required"
+                                                                                    >extension.opc-field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                        </div>
+
+
+                                                                        <v-accordion id="opc-attributes-accordion"
+                                                                                     class="vAccordion--default"
+                                                                        >
+                                                                            <v-pane id="opc-attributes-pane">
+                                                                                <v-pane-header>
+                                                                                    {{ 'extension.opc-mapping-attributes' | translate }}
+                                                                                </v-pane-header>
+                                                                                <v-pane-content>
+                                                                                    <div ng-show="map.attributes.length > 0">
+                                                                                        <ol class="list-group">
+                                                                                            <li class="list-group-item"
+                                                                                                ng-repeat="(attributeIndex, attribute) in map.attributes"
+                                                                                            >
+                                                                                                <md-button aria-label="{{ 'action.remove' | translate }}"
+                                                                                                           class="md-icon-button"
+                                                                                                           ng-click="removeItem(attribute, map.attributes)">
+                                                                                                    <ng-md-icon icon="close"
+                                                                                                                aria-label="{{ 'action.remove' | translate }}"
+                                                                                                    ></ng-md-icon>
+                                                                                                    <md-tooltip md-direction="top">
+                                                                                                        {{ 'action.remove' | translate }}
+                                                                                                    </md-tooltip>
+                                                                                                </md-button>
+                                                                                                <md-card>
+                                                                                                    <md-card-content>
+
+                                                                                                        <section flex
+                                                                                                                 layout="row"
+                                                                                                        >
+                                                                                                            <md-input-container flex="60" class="md-block">
+                                                                                                                <label translate>extension.key</label>
+                                                                                                                <input required
+                                                                                                                       name="opcAttributeKey_{{serverIndex}}{{mapIndex}}{{attributeIndex}}"
+                                                                                                                       ng-model="attribute.key"
+                                                                                                                >
+                                                                                                                <div ng-messages="theForm['opcAttributeKey_' + serverIndex + mapIndex + attributeIndex].$error">
+                                                                                                                    <div translate
+                                                                                                                         ng-message="required"
+                                                                                                                    >extension.opc-field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                            <md-input-container flex="40" class="md-block tb-container-for-select">
+                                                                                                                <label translate>extension.type</label>
+                                                                                                                <md-select required name="opcAttributeType_{{serverIndex}}{{mapIndex}}{{attributeIndex}}"
+                                                                                                                           ng-model="attribute.type"
+                                                                                                                >
+                                                                                                                    <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType"
+                                                                                                                               ng-value="attrType"
+                                                                                                                    >
+                                                                                                                        {{attrTypeValue | translate}}
+                                                                                                                    </md-option>
+                                                                                                                </md-select>
+                                                                                                                <div ng-messages="theForm['opcAttributeType_' + serverIndex + mapIndex + attributeIndex].$error">
+                                                                                                                    <div translate
+                                                                                                                         ng-message="required"
+                                                                                                                    >extension.required-type</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                        </section>
+
+                                                                                                        <section flex layout="row">
+                                                                                                            <md-input-container flex="100" class="md-block">
+                                                                                                                <label translate>extension.value</label>
+                                                                                                                <input required name="opcAttributeValue_{{serverIndex}}{{mapIndex}}{{attributeIndex}}"
+                                                                                                                       ng-model="attribute.value"
+                                                                                                                >
+                                                                                                                <div ng-messages="theForm['opcAttributeValue_' + serverIndex + mapIndex + attributeIndex].$error">
+                                                                                                                    <div translate
+                                                                                                                         ng-message="required"
+                                                                                                                    >extension.opc-field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+
+                                                                                                        </section>
+
+
+                                                                                                    </md-card-content>
+                                                                                                </md-card>
+                                                                                            </li>
+                                                                                        </ol>
+                                                                                    </div>
+                                                                                    <div flex layout="row" layout-align="start center">
+                                                                                        <md-button class="md-primary md-raised"
+                                                                                                   ng-click="addNewAttribute(map.attributes)"
+                                                                                                   aria-label="{{ 'action.add' | translate }}"
+                                                                                        >
+                                                                                            <md-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>
+
+                                                                        <v-accordion id="opc-timeseries-accordion" class="vAccordion--default">
+                                                                            <v-pane id="opc-timeseries-pane">
+                                                                                <v-pane-header>
+                                                                                    {{ 'extension.opc-timeseries' | translate }}
+                                                                                </v-pane-header>
+                                                                                <v-pane-content>
+                                                                                    <div ng-show="map.timeseries.length > 0">
+                                                                                        <ol class="list-group">
+                                                                                            <li class="list-group-item"
+                                                                                                ng-repeat="(timeseriesIndex, timeseries) in map.timeseries"
+                                                                                            >
+                                                                                                <md-button aria-label="{{ 'action.remove' | translate }}"
+                                                                                                           class="md-icon-button"
+                                                                                                           ng-click="removeItem(timeseries, map.timeseries)"
+                                                                                                >
+                                                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                                                    <md-tooltip md-direction="top">
+                                                                                                        {{ 'action.remove' | translate }}
+                                                                                                    </md-tooltip>
+                                                                                                </md-button>
+                                                                                                <md-card>
+                                                                                                    <md-card-content>
+                                                                                                        <section flex layout="row">
+                                                                                                            <md-input-container flex="60" class="md-block">
+                                                                                                                <label translate>extension.key</label>
+                                                                                                                <input required
+                                                                                                                       name="opcTimeseriesKey_{{serverIndex}}{{mapIndex}}{{timeseriesIndex}}"
+                                                                                                                       ng-model="timeseries.key"
+                                                                                                                >
+                                                                                                                <div ng-messages="theForm['opcTimeseriesKey_' + serverIndex + mapIndex + timeseriesIndex].$error">
+                                                                                                                    <div translate
+                                                                                                                         ng-message="required"
+                                                                                                                    >extension.opc-field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                            <md-input-container flex="40"
+                                                                                                                                class="md-block tb-container-for-select"
+                                                                                                            >
+                                                                                                                <label translate>extension.type</label>
+                                                                                                                <md-select required
+                                                                                                                           name="opcTimeseriesType_{{serverIndex}}{{mapIndex}}{{timeseriesIndex}}"
+                                                                                                                           ng-model="timeseries.type"
+                                                                                                                >
+                                                                                                                    <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType"
+                                                                                                                               ng-value="attrType"
+                                                                                                                    >
+                                                                                                                        {{attrTypeValue | translate}}
+                                                                                                                    </md-option>
+                                                                                                                </md-select>
+                                                                                                                <div ng-messages="theForm['opcTimeseriesType_' + serverIndex + mapIndex + timeseriesIndex].$error">
+                                                                                                                    <div translate
+                                                                                                                         ng-message="required"
+                                                                                                                    >extension.opc-field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                        </section>
+                                                                                                        <section flex layout="row">
+                                                                                                            <md-input-container flex="100" class="md-block">
+                                                                                                                <label translate>extension.value</label>
+                                                                                                                <input required name="opcTimeseriesValue_{{serverIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.value">
+                                                                                                                <div ng-messages="theForm['opcTimeseriesValue_' + serverIndex + mapIndex + timeseriesIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.required-value</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                        </section>
+                                                                                                    </md-card-content>
+                                                                                                </md-card>
+                                                                                            </li>
+                                                                                        </ol>
+                                                                                    </div>
+                                                                                    <div flex layout="row" layout-align="start center">
+                                                                                        <md-button class="md-primary md-raised"
+                                                                                                   ng-click="addNewAttribute(map.timeseries)"
+                                                                                                   aria-label="{{ 'action.add' | translate }}"
+                                                                                        >
+                                                                                            <md-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(server.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 flex
+                             layout="row"
+                             layout-align="start center"
+                        >
+                            <md-button class="md-primary md-raised"
+                                       ng-click="addServer(configuration.servers)"
+                                       aria-label="{{ 'action.add' | translate }}"
+                            >
+                                <md-icon class="material-icons">add</md-icon>
+                                <span translate>extension.opc-add-another-server</span>
+                            </md-button>
+                        </div>
+
+                    </div>
+                </v-pane-content>
+            </v-pane>
+        </v-accordion>
+        <!--{{config}}-->
+    </md-card-content>
+</md-card>
\ No newline at end of file
diff --git a/ui/src/app/extension/extension-table.directive.js b/ui/src/app/extension/extension-table.directive.js
new file mode 100644
index 0000000..21d716b
--- /dev/null
+++ b/ui/src/app/extension/extension-table.directive.js
@@ -0,0 +1,242 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'angular-material-data-table/dist/md-data-table.min.css';
+import './extension-table.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import extensionTableTemplate from './extension-table.tpl.html';
+import extensionDialogTemplate from './extension-dialog.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import ExtensionDialogController from './extension-dialog.controller'
+
+/*@ngInject*/
+export default function ExtensionTableDirective() {
+    return {
+        restrict: "E",
+        scope: true,
+        bindToController: {
+            entityId: '=',
+            entityType: '@'
+        },
+        controller: ExtensionTableController,
+        controllerAs: 'vm',
+        templateUrl: extensionTableTemplate
+    };
+}
+
+/*@ngInject*/
+function ExtensionTableController($scope, $filter, $document, $translate, types, $mdDialog, attributeService) {
+
+    let vm = this;
+
+    vm.extensions = [];
+    vm.allExtensions = [];
+    vm.selectedExtensions = [];
+    vm.extensionsCount = 0;
+
+    vm.query = {
+        order: 'id',
+        limit: 5,
+        page: 1,
+        search: null
+    };
+
+    vm.enterFilterMode = enterFilterMode;
+    vm.exitFilterMode = exitFilterMode;
+    vm.onReorder = onReorder;
+    vm.onPaginate = onPaginate;
+    vm.addExtension = addExtension;
+    vm.editExtension = editExtension;
+    vm.deleteExtension = deleteExtension;
+    vm.deleteExtensions = deleteExtensions;
+    vm.reloadExtensions = reloadExtensions;
+    vm.updateExtensions = updateExtensions;
+
+
+    $scope.$watch("vm.entityId", function(newVal) {
+        if (newVal) {
+            reloadExtensions();
+        }
+    });
+
+    $scope.$watch("vm.query.search", function(newVal, prevVal) {
+        if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
+            updateExtensions();
+        }
+    });
+
+    function enterFilterMode() {
+        vm.query.search = '';
+    }
+
+    function exitFilterMode() {
+        vm.query.search = null;
+        updateExtensions();
+    }
+
+    function onReorder() {
+        updateExtensions();
+    }
+
+    function onPaginate() {
+        updateExtensions();
+    }
+
+    function addExtension($event) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        openExtensionDialog($event);
+    }
+
+    function editExtension($event, extension) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        openExtensionDialog($event, extension);
+    }
+
+    function openExtensionDialog($event, extension) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        var isAdd = false;
+        if(!extension) {
+            isAdd = true;
+        }
+        $mdDialog.show({
+            controller: ExtensionDialogController,
+            controllerAs: 'vm',
+            templateUrl: extensionDialogTemplate,
+            parent: angular.element($document[0].body),
+            locals: {
+                isAdd: isAdd,
+                allExtensions: vm.allExtensions,
+                entityId: vm.entityId,
+                entityType: vm.entityType,
+                extension: extension
+            },
+            bindToController: true,
+            targetEvent: $event,
+            fullscreen: true,
+            skipHide: true
+        }).then(function() {
+            reloadExtensions();
+        }, function () {
+        });
+    }
+
+    function deleteExtension($event, extension) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        if(extension) {
+            var title = $translate.instant('extension.delete-extension-title', {extensionId: extension.id});
+            var content = $translate.instant('extension.delete-extension-text');
+
+            var confirm = $mdDialog.confirm()
+                .targetEvent($event)
+                .title(title)
+                .htmlContent(content)
+                .ariaLabel(title)
+                .cancel($translate.instant('action.no'))
+                .ok($translate.instant('action.yes'));
+            $mdDialog.show(confirm).then(function() {
+                var editedExtensions = vm.allExtensions.filter(function(ext) {
+                    return ext.id !== extension.id;
+                });
+                var editedValue = angular.toJson(editedExtensions);
+                attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then(
+                    function success() {
+                        reloadExtensions();
+                    }
+                );
+            });
+        }
+    }
+
+    function deleteExtensions($event) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        if (vm.selectedExtensions && vm.selectedExtensions.length > 0) {
+            var title = $translate.instant('extension.delete-extensions-title', {count: vm.selectedExtensions.length}, 'messageformat');
+            var content = $translate.instant('extension.delete-extensions-text');
+
+            var confirm = $mdDialog.confirm()
+                .targetEvent($event)
+                .title(title)
+                .htmlContent(content)
+                .ariaLabel(title)
+                .cancel($translate.instant('action.no'))
+                .ok($translate.instant('action.yes'));
+            $mdDialog.show(confirm).then(function () {
+                var editedExtensions = angular.copy(vm.allExtensions);
+                for (var i = 0; i < vm.selectedExtensions.length; i++) {
+                    editedExtensions = editedExtensions.filter(function (ext) {
+                        return ext.id !== vm.selectedExtensions[i].id;
+                    });
+                }
+                var editedValue = angular.toJson(editedExtensions);
+                attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then(
+                    function success() {
+                        reloadExtensions();
+                    }
+                );
+            });
+        }
+    }
+
+    function reloadExtensions() {
+        vm.allExtensions.length = 0;
+        vm.extensions.length = 0;
+        vm.extensionsPromise = attributeService.getEntityAttributesValues(vm.entityType, vm.entityId, types.attributesScope.shared.value, ["configuration"]);
+        vm.extensionsPromise.then(
+            function success(data) {
+                vm.allExtensions = angular.fromJson(data[0].value);
+                vm.selectedExtensions = [];
+                updateExtensions();
+                vm.extensionsPromise = null;
+            },
+            function fail() {
+                vm.extensions = [];
+                vm.selectedExtensions = [];
+                updateExtensions();
+                vm.extensionsPromise = null;
+            }
+        );
+    }
+
+    function updateExtensions() {
+        vm.selectedExtensions = [];
+        var result = $filter('orderBy')(vm.allExtensions, vm.query.order);
+        if (vm.query.search != null) {
+            result = $filter('filter')(result, function(extension) {
+                if(!vm.query.search || (extension.id.indexOf(vm.query.search) != -1) || (extension.type.indexOf(vm.query.search) != -1)) {
+                    return true;
+                }
+                return false;
+            });
+        }
+        vm.extensionsCount = result.length;
+        var startIndex = vm.query.limit * (vm.query.page - 1);
+        vm.extensions = result.slice(startIndex, startIndex + vm.query.limit);
+    }
+}
\ No newline at end of file
diff --git a/ui/src/app/extension/extension-table.scss b/ui/src/app/extension/extension-table.scss
new file mode 100644
index 0000000..c6c19a5
--- /dev/null
+++ b/ui/src/app/extension/extension-table.scss
@@ -0,0 +1,16 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import '../../scss/constants';
\ No newline at end of file
diff --git a/ui/src/app/extension/extension-table.tpl.html b/ui/src/app/extension/extension-table.tpl.html
new file mode 100644
index 0000000..558b9c2
--- /dev/null
+++ b/ui/src/app/extension/extension-table.tpl.html
@@ -0,0 +1,118 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+
+<md-content flex class="md-padding tb-absolute-fill tb-data-table" layout="column">
+    <div layout="column" class="md-whiteframe-z1">
+        <md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedExtensions.length
+                                                                 && vm.query.search === null">
+            <div class="md-toolbar-tools">
+                <span translate>{{ 'extension.extensions' }}</span>
+                <span flex></span>
+                <md-button class="md-icon-button" ng-click="vm.addExtension($event)">
+                    <md-icon>add</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.add' | translate }}
+                    </md-tooltip>
+                </md-button>
+                <md-button class="md-icon-button" ng-click="vm.enterFilterMode()">
+                    <md-icon>search</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.search' | translate }}
+                    </md-tooltip>
+                </md-button>
+                <md-button class="md-icon-button" ng-click="vm.reloadExtensions()">
+                    <md-icon>refresh</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.refresh' | translate }}
+                    </md-tooltip>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedExtensions.length
+                                                                 && vm.query.search != null"">
+            <div class="md-toolbar-tools">
+                <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
+                    <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.search' | translate }}
+                    </md-tooltip>
+                </md-button>
+                <md-input-container flex>
+                    <label>&nbsp;</label>
+                    <input ng-model="vm.query.search" placeholder="{{ 'common.enter-search' | translate }}"/>
+                </md-input-container>
+                <md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="vm.exitFilterMode()">
+                    <md-icon aria-label="{{ 'action.close' | translate }}" class="material-icons">close</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.close' | translate }}
+                    </md-tooltip>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-toolbar class="md-table-toolbar alternate" ng-show="vm.selectedExtensions.length">
+            <div class="md-toolbar-tools">
+                <span translate
+                      translate-values="{count: vm.selectedExtensions.length}"
+                      translate-interpolation="messageformat">extension.selected-extensions</span>
+                <span flex></span>
+                <md-button class="md-icon-button" ng-click="vm.deleteExtensions($event)">
+                    <md-icon>delete</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.delete' | translate }}
+                    </md-tooltip>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-table-container>
+            <table md-table md-row-select multiple="" ng-model="vm.selectedExtensions" md-progress="vm.extensionsDeferred.promise">
+                <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
+                    <tr md-row>
+                        <th md-column md-order-by="id"><span translate>extension.id</span></th>
+                        <th md-column md-order-by="type"><span translate>extension.type</span></th>
+                        <th md-column><span>&nbsp</span></th>
+                    </tr>
+                </thead>
+                <tbody md-body>
+                <tr md-row md-select="extension" md-select-id="extension" md-auto-select ng-repeat="extension in vm.extensions">
+                    <td md-cell>{{ extension.id }}</td>
+                    <td md-cell>{{ extension.type }}</td>
+                    <td md-cell class="tb-action-cell">
+                        <md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}" ng-click="vm.editExtension($event, extension)">
+                            <md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">edit</md-icon>
+                            <md-tooltip md-direction="top">
+                                {{ 'extension.edit' | translate }}
+                            </md-tooltip>
+                        </md-button>
+                        <md-button class="md-icon-button" aria-label="{{ 'action.delete' | translate }}" ng-click="vm.deleteExtension($event, extension)"> <!-- add click-function -->
+                            <md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">delete</md-icon>
+                            <md-tooltip md-direction="top">
+                                {{ 'extension.delete' | translate }}
+                            </md-tooltip>
+                        </md-button>
+                    </td>
+                </tr>
+                </tbody>
+            </table>
+        </md-table-container>
+        <md-table-pagination md-limit="vm.query.limit" md-limit-options="[5, 10, 15]"
+                             md-page="vm.query.page" md-total="{{vm.extensionsCount}}"
+                             md-on-paginate="vm.onPaginate" md-page-select>
+        </md-table-pagination>
+    </div>
+    <div></div> <!-- div for testing values -->
+</md-content>
\ No newline at end of file
diff --git a/ui/src/app/extension/index.js b/ui/src/app/extension/index.js
new file mode 100644
index 0000000..4c9f812
--- /dev/null
+++ b/ui/src/app/extension/index.js
@@ -0,0 +1,27 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import ExtensionTableDirective from './extension-table.directive';
+import ExtensionFormHttpDirective from './extensions-forms/extension-form-http.directive';
+import ExtensionFormOpcDirective from './extensions-forms/extension-form-opc.directive';
+import {ParseToNull} from './extension-dialog.controller';
+
+export default angular.module('thingsboard.extension', [])
+    .directive('tbExtensionTable', ExtensionTableDirective)
+    .directive('tbExtensionFormHttp', ExtensionFormHttpDirective)
+    .directive('tbExtensionFormOpc', ExtensionFormOpcDirective)
+    .directive('parseToNull', ParseToNull)
+    .name;
\ No newline at end of file
diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js
index 9d3e1d1..c23b008 100644
--- a/ui/src/app/layout/index.js
+++ b/ui/src/app/layout/index.js
@@ -35,6 +35,7 @@ import thingsboardUserMenu from './user-menu.directive';
 import thingsboardEntity from '../entity';
 import thingsboardEvent from '../event';
 import thingsboardAlarm from '../alarm';
+import thingsboardExtension from '../extension';
 import thingsboardTenant from '../tenant';
 import thingsboardCustomer from '../customer';
 import thingsboardUser from '../user';
@@ -66,6 +67,7 @@ export default angular.module('thingsboard.home', [
     thingsboardEntity,
     thingsboardEvent,
     thingsboardAlarm,
+    thingsboardExtension,
     thingsboardTenant,
     thingsboardCustomer,
     thingsboardUser,
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index bd1b9e9..6f626b5 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -729,11 +729,88 @@ export default angular.module('thingsboard.locale', [])
                     "messages-processed": "Messages processed",
                     "errors-occurred": "Errors occurred"
                 },
+                "extension": {
+                    "extensions": "Extensions",
+                    "selected-extensions": "{ count, select, 1 {1 extension} other {# extensions} } selected",
+                    "type": "Type",
+                    "key": "Key",
+                    "value": "Value",
+                    "id": "Id",
+                    "extension-id": "Extension id",
+                    "extension-type": "Extension type",
+                    "transformer-json": "JSON*",
+                    "id-required": "Extension id is required.",
+                    "unique-id-required": "Current extension id already exists.",
+                    "type-required": "Extension type is required.",
+                    "required-type": "Type is required.",
+                    "required-key": "Key is required.",
+                    "required-value": "Value is required.",
+                    "delete": "Delete extension",
+                    "add": "Add extension",
+                    "edit": "Edit extension",
+                    "delete-extension-title": "Are you sure you want to delete the extension '{{extensionId}}'?",
+                    "delete-extension-text": "Be careful, after the confirmation the extension and all related data will become unrecoverable.",
+                    "delete-extensions-title": "Are you sure you want to delete { count, select, 1 {1 extension} other {# extensions} }?",
+                    "delete-extensions-text": "Be careful, after the confirmation all selected extensions will be removed.",
+                    "converters": "Converters",
+                    "converter-id": "Converter id",
+                    "converter-id-required": "Converter id is required.",
+                    "configuration": "Configuration",
+                    "converter-configurations": "Converter configurations",
+                    "token": "Security token",
+                    "add-converter": "Add converter",
+                    "add-converter-prompt": "Please add converter",
+                    "add-config": "Add converter configuration",
+                    "add-config-prompt": "Please add converter configuration",
+                    "device-name-expression": "Device name expression",
+                    "device-name-expression-required": "Device name expression is required.",
+                    "device-type-expression": "Device type expression",
+                    "device-type-expression-required": "Device type expression is required.",
+                    "custom": "Custom",
+                    "to-double": "To Double",
+                    "transformer": "Transformer",
+                    "json-required": "Transformer json is required.",
+                    "json-parse": "Unable to parse transformer json.",
+                    "attributes": "Attributes",
+                    "add-attribute": "Add attribute",
+                    "add-map": "Add mapping element",
+                    "timeseries": "Timeseries",
+                    "add-timeseries": "Add timeseries",
+
+                    "opc-field-required": "Field is required",
+                    "opc-server": "Servers",
+                    "opc-add-server-hint": "Add server",
+                    "opc-add-server-prompt": "Please add server",
+                    "opc-server-id": "Server id",
+                    "opc-timeseries": "Timeseries",
+                    "opc-application-name": "Application name",
+                    "opc-application-uri": "Application uri",
+                    "opc-host": "Host",
+                    "opc-port": "Port",
+                    "opc-scan-period-in-seconds": "Scan period in seconds",
+                    "opc-timeout-in-millis": "Timeout in milliseconds",
+                    "opc-security": "Security",
+                    "opc-identity": "Identity",
+                    "opc-keystore": "Keystore",
+                    "opc-type": "Type",
+                    "opc-keystore-type":"Type",
+                    "opc-keystore-location":"Location *",
+                    "opc-keystore-password":"Password",
+                    "opc-keystore-alias":"Alias",
+                    "opc-keystore-key-password":"Key password",
+                    "opc-mapping":"Mapping",
+                    "opc-device-node-pattern":"Device node pattern",
+                    "opc-device-name-pattern":"Device name pattern",
+                    "opc-mapping-attributes":"Mapping attributes",
+                    "opc-username":"Username",
+                    "opc-password":"Password",
+                    "opc-add-another-server":"Add another server",
+                },
                 "fullscreen": {
                     "expand": "Expand to fullscreen",
                     "exit": "Exit fullscreen",
                     "toggle": "Toggle fullscreen mode",
-                    "fullscreen": "Fullscreen"
+                    "fullscreen": "Fullscreen",
                 },
                 "function": {
                     "function": "Function"
@@ -1071,7 +1148,8 @@ export default angular.module('thingsboard.locale', [])
                     "boolean": "Boolean",
                     "boolean-value": "Boolean value",
                     "false": "False",
-                    "true": "True"
+                    "true": "True",
+                    "long": "Long"
                 },
                 "widget": {
                     "widget-library": "Widgets Library",