thingsboard-aplcache

Details

diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html
index bc66a79..1ec0134 100644
--- a/ui/src/app/device/devices.tpl.html
+++ b/ui/src/app/device/devices.tpl.html
@@ -70,6 +70,7 @@
         <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.operatingItem().additionalInfo.gateway" md-on-select="vm.grid.triggerResize()" label="{{ 'extension.extensions' | translate }}">
             <tb-extension-table flex
                                 entity-id="vm.grid.operatingItem().id.id"
+                                entity-name="vm.grid.operatingItem().name"
                                 entity-type="{{vm.types.entityType.device}}">
             </tb-extension-table>
         </md-tab>
diff --git a/ui/src/app/extension/extension-table.directive.js b/ui/src/app/extension/extension-table.directive.js
index 8ad8275..ecc7b0f 100644
--- a/ui/src/app/extension/extension-table.directive.js
+++ b/ui/src/app/extension/extension-table.directive.js
@@ -34,7 +34,8 @@ export default function ExtensionTableDirective() {
         scope: true,
         bindToController: {
             entityId: '=',
-            entityType: '@'
+            entityType: '@',
+            entityName: '='
         },
         controller: ExtensionTableController,
         controllerAs: 'vm',
@@ -43,7 +44,7 @@ export default function ExtensionTableDirective() {
 }
 
 /*@ngInject*/
-function ExtensionTableController($scope, $filter, $document, $translate, types, $mdDialog, attributeService, telemetryWebsocketService) {
+function ExtensionTableController($scope, $filter, $document, $translate, types, $mdDialog, attributeService, telemetryWebsocketService, importExport) {
 
     let vm = this;
 
@@ -328,4 +329,21 @@ function ExtensionTableController($scope, $filter, $document, $translate, types,
             return num;
         }
     }
+
+    vm.importExtensions = function () {
+        importExport.importExtension({"entityType":vm.entityType, "entityId":vm.entityId, "successFunc":reloadExtensions});
+    };
+    vm.exportExtensions = function () {
+        importExport.exportToPc(vm.extensionsJSON,  vm.entityName + '_configuration.json');
+    };
+
+    vm.exportExtension = function ($event, extension) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        importExport.exportToPc(extension,  vm.entityName +'_'+ extension.id +'_configuration.json');
+    };
+
+
+
 }
\ No newline at end of file
diff --git a/ui/src/app/extension/extension-table.scss b/ui/src/app/extension/extension-table.scss
index ee2a570..1cedaed 100644
--- a/ui/src/app/extension/extension-table.scss
+++ b/ui/src/app/extension/extension-table.scss
@@ -16,10 +16,19 @@
 @import '../../scss/constants';
 
 
-.extension-table md-input-container .md-errors-spacer {
-  min-height: 0;
+.extension-table {
+
+  md-input-container .md-errors-spacer {
+    min-height: 0;
+  }
+
+  &.tb-data-table table.md-table tbody tr td.tb-action-cell,
+  &.tb-data-table table.md-table.md-row-select tbody tr td.tb-action-cell {
+    width: 114px;
+  }
 }
 
+
 .extension__syncStatus--black {
   color: #000000!important;
 }
diff --git a/ui/src/app/extension/extension-table.tpl.html b/ui/src/app/extension/extension-table.tpl.html
index 428a146..3cc322b 100644
--- a/ui/src/app/extension/extension-table.tpl.html
+++ b/ui/src/app/extension/extension-table.tpl.html
@@ -23,6 +23,19 @@
             <div class="md-toolbar-tools">
                 <span translate>{{ 'extension.extensions' }}</span>
                 <span flex></span>
+
+                <md-button class="md-icon-button" ng-click="vm.importExtensions($event)">
+                    <md-icon>file_upload</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'extension.sync.import-extensions-configuration' | translate }}
+                    </md-tooltip>
+                </md-button>
+                <md-button class="md-icon-button" ng-click="vm.exportExtensions()">
+                    <md-icon>file_download</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'extension.sync.export-extensions-configuration' | translate }}
+                    </md-tooltip>
+                </md-button>
                 <md-button class="md-icon-button" ng-click="vm.addExtension($event)">
                     <md-icon>add</md-icon>
                     <md-tooltip md-direction="top">
@@ -44,7 +57,7 @@
             </div>
         </md-toolbar>
         <md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedExtensions.length
-                                                                 && vm.query.search != null"">
+                                                                 && 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>
@@ -111,6 +124,12 @@
                     <td md-cell>{{ extension.id }}</td>
                     <td md-cell>{{ extension.type }}</td>
                     <td md-cell class="tb-action-cell">
+                        <md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}" ng-click="vm.exportExtension($event, extension)">
+                            <md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">file_download</md-icon>
+                            <md-tooltip md-direction="top">
+                                {{ 'extension.export-extension' | translate }}
+                            </md-tooltip>
+                        </md-button>
                         <md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}" ng-click="vm.editExtension($event, extension)">
                             <md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">edit</md-icon>
                             <md-tooltip md-direction="top">
diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js
index 86d5240..e660e82 100644
--- a/ui/src/app/import-export/import-export.service.js
+++ b/ui/src/app/import-export/import-export.service.js
@@ -24,8 +24,9 @@ import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
 /* eslint-disable no-undef, angular/window-service, angular/document-service */
 
 /*@ngInject*/
-export default function ImportExport($log, $translate, $q, $mdDialog, $document, itembuffer, utils, types, dashboardUtils,
-                                     entityService, dashboardService, pluginService, ruleService, widgetService, toast) {
+export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types,
+                                     dashboardUtils, entityService, dashboardService, pluginService, ruleService,
+                                     widgetService, toast, attributeService) {
 
 
     var service = {
@@ -40,8 +41,11 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
         exportWidgetType: exportWidgetType,
         importWidgetType: importWidgetType,
         exportWidgetsBundle: exportWidgetsBundle,
-        importWidgetsBundle: importWidgetsBundle
-    }
+        importWidgetsBundle: importWidgetsBundle,
+        exportExtension: exportExtension,
+        importExtension: importExtension,
+        exportToPc: exportToPc
+    };
 
     return service;
 
@@ -614,6 +618,89 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
         return true;
     }
 
+
+
+    function exportExtension(extensionId) {
+
+        getExtension(extensionId)
+            .then(
+                function success(extension) {
+                    var name = extension.title;
+                    name = name.toLowerCase().replace(/\W/g,"_");
+                    exportToPc(prepareExport(extension), name + '.json');
+                },
+                function fail(rejection) {
+                    var message = rejection;
+                    if (!message) {
+                        message = $translate.instant('error.unknown-error');
+                    }
+                    toast.showError($translate.instant('extension.export-failed-error', {error: message}));
+                }
+            );
+
+
+        function getExtension(extensionId) {
+            var deferred = $q.defer();
+            var url = '/api/plugins/telemetry/DEVICE/' + extensionId;
+            $http.get(url, null)
+                .then(function success(response) {
+                    deferred.resolve(response.data);
+                }, function fail() {
+                    deferred.reject();
+                });
+            return deferred.promise;
+        }
+
+    }
+
+    function importExtension(options) {
+        var deferred = $q.defer();
+        openImportDialog(options, 'extension.import-extensions', 'extension.file')
+            .then(
+                function success(extension) {
+                    if (!validateImportedExtension(extension)) {
+                        toast.showError($translate.instant('extension.invalid-file-error'));
+                        deferred.reject();
+                    } else {
+                        attributeService
+                            .saveEntityAttributes(
+                                options.entityType,
+                                options.entityId,
+                                types.attributesScope.shared.value,
+                                [{
+                                    key: "configuration",
+                                    value: angular.toJson(extension)
+                                }]
+                            )
+                            .then(function success() {
+                                options.successFunc();
+                            });
+
+                    }
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
+        return deferred.promise;
+    }
+
+
+    function validateImportedExtension(configuration) {
+        if (configuration.length) {
+            for (let i = 0; i < configuration.length; i++) {
+                if (angular.isUndefined(configuration[i].configuration) || angular.isUndefined(configuration[i].id )|| angular.isUndefined(configuration[i].type)) {
+                    return false;
+                }
+            }
+        } else {
+            return false;
+        }
+        return true;
+    }
+
+
+
     function processEntityAliases(entityAliases, aliasIds) {
         var deferred = $q.defer();
         var missingEntityAliases = {};
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 089a0f5..0021e3d 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -845,8 +845,16 @@ export default angular.module('thingsboard.locale', [])
                         "sync": "Sync",
                         "not-sync": "Not sync",
                         "last-sync-time": "Last sync time",
-                        "not-available": "Not available"
+                        "not-available": "Not available",
+                        "export-extensions-configuration":"Export extensions configuration",
+                        "import-extensions-configuration":"Import extensions configuration"
                     },
+
+                    "import-extensions": "Import extensions",
+                    "import-extension": "Import extension",
+                    "export-extension": "Export extension",
+                    "file": "Extensions file",
+                    "invalid-file-error": "Invalid extension file"
                 },
                 "fullscreen": {
                     "expand": "Expand to fullscreen",