thingsboard-aplcache

Import/Export. Bug fixes.

1/6/2017 3:49:00 PM

Details

ui/package.json 1(+1 -0)

diff --git a/ui/package.json b/ui/package.json
index c1101d2..b39ae99 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -54,6 +54,7 @@
     "json-schema-defaults": "^0.2.0",
     "justgage": "^1.2.2",
     "material-ui": "^0.16.1",
+    "material-ui-number-input": "^5.0.16",
     "md-color-picker": "^0.2.6",
     "mdPickers": "git://github.com/alenaksu/mdPickers.git#0.7.5",
     "moment": "^2.15.0",
diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js
index 27cf605..1a1d714 100644
--- a/ui/src/app/api/device.service.js
+++ b/ui/src/app/api/device.service.js
@@ -88,10 +88,10 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
         return deferred.promise;
     }
 
-    function getDevice(deviceId) {
+    function getDevice(deviceId, ignoreErrors) {
         var deferred = $q.defer();
         var url = '/api/device/' + deviceId;
-        $http.get(url, null).then(function success(response) {
+        $http.get(url, { ignoreErrors: ignoreErrors }).then(function success(response) {
             deferred.resolve(response.data);
         }, function fail(response) {
             deferred.reject(response.data);
diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js
index 7c5094a..c55995e 100644
--- a/ui/src/app/components/dashboard.directive.js
+++ b/ui/src/app/components/dashboard.directive.js
@@ -58,8 +58,10 @@ function Dashboard() {
             isMobile: '=',
             isMobileDisabled: '=?',
             isEditActionEnabled: '=',
+            isExportActionEnabled: '=',
             isRemoveActionEnabled: '=',
             onEditWidget: '&?',
+            onExportWidget: '&?',
             onRemoveWidget: '&?',
             onWidgetMouseDown: '&?',
             onWidgetClicked: '&?',
@@ -139,6 +141,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
     vm.showWidgetTitle = showWidgetTitle;
     vm.hasTimewindow = hasTimewindow;
     vm.editWidget = editWidget;
+    vm.exportWidget = exportWidget;
     vm.removeWidget = removeWidget;
     vm.loading = loading;
 
@@ -413,6 +416,16 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
         }
     }
 
+    function exportWidget ($event, widget) {
+        resetWidgetClick();
+        if ($event) {
+            $event.stopPropagation();
+        }
+        if (vm.isExportActionEnabled && vm.onExportWidget) {
+            vm.onExportWidget({event: $event, widget: widget});
+        }
+    }
+
     function removeWidget($event, widget) {
         resetWidgetClick();
         if ($event) {
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
index c43caac..0ad43eb 100644
--- a/ui/src/app/components/dashboard.tpl.html
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -62,6 +62,18 @@
 											edit
 										</md-icon>
 									</md-button>
+									<md-button ng-show="vm.isExportActionEnabled && !vm.isWidgetExpanded"
+											   ng-disabled="vm.loading()"
+											   class="md-icon-button md-primary"
+											   ng-click="vm.exportWidget($event, widget)"
+											   aria-label="{{ 'widget.export' | translate }}">
+										<md-tooltip md-direction="top">
+											{{ 'widget.export' | translate }}
+										</md-tooltip>
+										<md-icon class="material-icons">
+											file_download
+										</md-icon>
+									</md-button>
 									<md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
 										ng-disabled="vm.loading()"
 										class="md-icon-button md-primary"
diff --git a/ui/src/app/components/grid.directive.js b/ui/src/app/components/grid.directive.js
index ba8cdc1..67f29ed 100644
--- a/ui/src/app/components/grid.directive.js
+++ b/ui/src/app/components/grid.directive.js
@@ -327,6 +327,10 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
                 icon: "add"
             };
 
+        vm.addItemActionsOpen = false;
+
+        vm.addItemActions = vm.config.addItemActions || [];
+
         vm.onGridInited = vm.config.onGridInited || function () {
             };
 
diff --git a/ui/src/app/components/grid.tpl.html b/ui/src/app/components/grid.tpl.html
index 16d38f3..df3e54c 100644
--- a/ui/src/app/components/grid.tpl.html
+++ b/ui/src/app/components/grid.tpl.html
@@ -79,7 +79,7 @@
     </tb-details-sidenav>
 </section>
 
-<section layout="row" layout-wrap class="tb-footer-buttons md-fab ">
+<section layout="row" layout-wrap class="tb-footer-buttons md-fab " layout-align="start end">
     <md-button ng-disabled="loading" ng-show="vm.items.selectedCount > 0" class="tb-btn-footer md-accent md-hue-2 md-fab" ng-repeat="groupAction in vm.groupActionsList"
                ng-click="groupAction.onAction($event, vm.items)" aria-label="{{ groupAction.name() }}">
         <md-tooltip md-direction="top">
@@ -93,10 +93,29 @@
         </md-tooltip>
         <ng-md-icon icon="arrow_drop_up"></ng-md-icon>
     </md-button>
-    <md-button ng-disabled="loading" ng-if="vm.addItemAction.name()" class="tb-btn-footer md-accent md-hue-2 md-fab" ng-click="vm.addItemAction.onAction($event)" aria-label="{{ vm.addItemAction.name() }}" >
+    <md-button ng-disabled="loading" ng-if="vm.addItemAction.name() && vm.addItemActions.length == 0" class="tb-btn-footer md-accent md-hue-2 md-fab" ng-click="vm.addItemAction.onAction($event)" aria-label="{{ vm.addItemAction.name() }}" >
         <md-tooltip md-direction="top">
             {{ vm.addItemAction.details() }}
         </md-tooltip>
         <ng-md-icon icon="{{ vm.addItemAction.icon }}"></ng-md-icon>
     </md-button>
+    <md-fab-speed-dial ng-disabled="loading" ng-if="vm.addItemAction.name() && vm.addItemActions.length > 0" md-open="vm.addItemActionsOpen" class="md-scale" md-direction="up" ng-if="vm.addItemAction.name()">
+        <md-fab-trigger>
+            <md-button ng-disabled="loading" class="tb-btn-footer md-accent md-hue-2 md-fab" aria-label="{{ vm.addItemAction.name() }}" >
+                <md-tooltip md-direction="top">
+                    {{ vm.addItemAction.details() }}
+                </md-tooltip>
+                <ng-md-icon icon="{{ vm.addItemAction.icon }}"></ng-md-icon>
+            </md-button>
+        </md-fab-trigger>
+        <md-fab-actions>
+            <md-button ng-disabled="loading" class="md-accent md-hue-2 md-fab" ng-repeat="addItemAction in vm.addItemActions"
+                       ng-click="addItemAction.onAction($event)" aria-label="{{ addItemAction.name() }}" >
+                <md-tooltip md-direction="top">
+                    {{ addItemAction.details() }}
+                </md-tooltip>
+                <ng-md-icon icon="{{addItemAction.icon}}"></ng-md-icon>
+            </md-button>
+        </md-fab-actions>
+    </md-fab-speed-dial>
 </section>
\ No newline at end of file
diff --git a/ui/src/app/components/react/json-form-number.jsx b/ui/src/app/components/react/json-form-number.jsx
index 1922ca2..8d9296b 100644
--- a/ui/src/app/components/react/json-form-number.jsx
+++ b/ui/src/app/components/react/json-form-number.jsx
@@ -15,7 +15,7 @@
  */
 import React from 'react';
 import ThingsboardBaseComponent from './json-form-base-component.jsx';
-import TextField from 'material-ui/TextField';
+import NumberInput from 'material-ui-number-input';
 
 class ThingsboardNumber extends React.Component {
 
@@ -63,16 +63,18 @@ class ThingsboardNumber extends React.Component {
         if (this.state.focused) {
             fieldClass += " tb-focused";
         }
+        var value = this.state.lastSuccessfulValue;
+        value = Number(value);
 
         return (
-            <TextField
+            <NumberInput
                 className={fieldClass}
-                type={this.props.form.type}
+                strategy="allow"
                 floatingLabelText={this.props.form.title}
                 hintText={this.props.form.placeholder}
                 errorText={this.props.error}
                 onChange={this.preValidationCheck}
-                defaultValue={this.state.lastSuccessfulValue}
+                defaultValue={value}
                 ref="numberField"
                 disabled={this.props.form.readonly}
                 onFocus={this.onFocus}
diff --git a/ui/src/app/dashboard/add-widget.controller.js b/ui/src/app/dashboard/add-widget.controller.js
index ec77404..92a7235 100644
--- a/ui/src/app/dashboard/add-widget.controller.js
+++ b/ui/src/app/dashboard/add-widget.controller.js
@@ -102,10 +102,12 @@ export default function AddWidgetController($scope, widgetService, deviceService
             controllerAs: 'vm',
             templateUrl: deviceAliasesTemplate,
             locals: {
-                deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
-                aliasToWidgetsMap: null,
-                isSingleDevice: true,
-                singleDeviceAlias: singleDeviceAlias
+                config: {
+                    deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
+                    widgets: null,
+                    isSingleDevice: true,
+                    singleDeviceAlias: singleDeviceAlias
+                }
             },
             parent: angular.element($document[0].body),
             fullscreen: true,
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index 43225f5..8e85e74 100644
--- a/ui/src/app/dashboard/dashboard.controller.js
+++ b/ui/src/app/dashboard/dashboard.controller.js
@@ -23,7 +23,7 @@ import addWidgetTemplate from './add-widget.tpl.html';
 
 /*@ngInject*/
 export default function DashboardController(types, widgetService, userService,
-                                            dashboardService, itembuffer, hotkeys, $window, $rootScope,
+                                            dashboardService, itembuffer, importExport, hotkeys, $window, $rootScope,
                                             $scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) {
 
     var user = userService.getCurrentUser();
@@ -53,6 +53,8 @@ export default function DashboardController(types, widgetService, userService,
     vm.prepareDashboardContextMenu = prepareDashboardContextMenu;
     vm.prepareWidgetContextMenu = prepareWidgetContextMenu;
     vm.editWidget = editWidget;
+    vm.exportWidget = exportWidget;
+    vm.importWidget = importWidget;
     vm.isTenantAdmin = isTenantAdmin;
     vm.loadDashboard = loadDashboard;
     vm.noData = noData;
@@ -210,44 +212,17 @@ export default function DashboardController(types, widgetService, userService,
     }
 
     function openDeviceAliases($event) {
-        var aliasToWidgetsMap = {};
-        var widgetsTitleList;
-        for (var w in vm.widgets) {
-            var widget = vm.widgets[w];
-            if (widget.type === types.widgetType.rpc.value) {
-                if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
-                    var targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];
-                    widgetsTitleList = aliasToWidgetsMap[targetDeviceAliasId];
-                    if (!widgetsTitleList) {
-                        widgetsTitleList = [];
-                        aliasToWidgetsMap[targetDeviceAliasId] = widgetsTitleList;
-                    }
-                    widgetsTitleList.push(widget.config.title);
-                }
-            } else {
-                for (var i in widget.config.datasources) {
-                    var datasource = widget.config.datasources[i];
-                    if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
-                        widgetsTitleList = aliasToWidgetsMap[datasource.deviceAliasId];
-                        if (!widgetsTitleList) {
-                            widgetsTitleList = [];
-                            aliasToWidgetsMap[datasource.deviceAliasId] = widgetsTitleList;
-                        }
-                        widgetsTitleList.push(widget.config.title);
-                    }
-                }
-            }
-        }
-
         $mdDialog.show({
             controller: 'DeviceAliasesController',
             controllerAs: 'vm',
             templateUrl: deviceAliasesTemplate,
             locals: {
-                deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
-                aliasToWidgetsMap: aliasToWidgetsMap,
-                isSingleDevice: false,
-                singleDeviceAlias: null
+                config: {
+                    deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
+                    widgets: vm.widgets,
+                    isSingleDevice: false,
+                    singleDeviceAlias: null
+                }
             },
             parent: angular.element($document[0].body),
             skipHide: true,
@@ -300,6 +275,16 @@ export default function DashboardController(types, widgetService, userService,
         }
     }
 
+    function exportWidget($event, widget) {
+        $event.stopPropagation();
+        importExport.exportWidget(vm.dashboard, widget);
+    }
+
+    function importWidget($event) {
+        $event.stopPropagation();
+        importExport.importWidget($event, vm.dashboard);
+    }
+
     function widgetMouseDown($event, widget) {
         if (vm.isEdit && !vm.isEditingWidget) {
             vm.dashboardContainer.selectWidget(widget, 0);
@@ -438,48 +423,7 @@ export default function DashboardController(types, widgetService, userService,
     }
 
     function copyWidget($event, widget) {
-        var aliasesInfo = {
-            datasourceAliases: {},
-            targetDeviceAliases: {}
-        };
-        var originalColumns = 24;
-        if (vm.dashboard.configuration.gridSettings &&
-            vm.dashboard.configuration.gridSettings.columns) {
-            originalColumns = vm.dashboard.configuration.gridSettings.columns;
-        }
-        if (widget.config && vm.dashboard.configuration
-            && vm.dashboard.configuration.deviceAliases) {
-            var deviceAlias;
-            if (widget.config.datasources) {
-                for (var i=0;i<widget.config.datasources.length;i++) {
-                    var datasource = widget.config.datasources[i];
-                    if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
-                        deviceAlias = vm.dashboard.configuration.deviceAliases[datasource.deviceAliasId];
-                        if (deviceAlias) {
-                            aliasesInfo.datasourceAliases[i] = {
-                                aliasName: deviceAlias.alias,
-                                deviceId: deviceAlias.deviceId
-                            }
-                        }
-                    }
-                }
-            }
-            if (widget.config.targetDeviceAliasIds) {
-                for (i=0;i<widget.config.targetDeviceAliasIds.length;i++) {
-                    var targetDeviceAliasId = widget.config.targetDeviceAliasIds[i];
-                    if (targetDeviceAliasId) {
-                        deviceAlias = vm.dashboard.configuration.deviceAliases[targetDeviceAliasId];
-                        if (deviceAlias) {
-                            aliasesInfo.targetDeviceAliases[i] = {
-                                aliasName: deviceAlias.alias,
-                                deviceId: deviceAlias.deviceId
-                            }
-                        }
-                    }
-                }
-            }
-        }
-        itembuffer.copyWidget(widget, aliasesInfo, originalColumns);
+        itembuffer.copyWidget(vm.dashboard, widget);
     }
 
     function helpLinkIdForWidgetType() {
diff --git a/ui/src/app/dashboard/dashboard.directive.js b/ui/src/app/dashboard/dashboard.directive.js
index cd9243f..5d00cce 100644
--- a/ui/src/app/dashboard/dashboard.directive.js
+++ b/ui/src/app/dashboard/dashboard.directive.js
@@ -36,6 +36,7 @@ export default function DashboardDirective($compile, $templateCache) {
             theForm: '=',
             onAssignToCustomer: '&',
             onUnassignFromCustomer: '&',
+            onExportDashboard: '&',
             onDeleteDashboard: '&'
         }
     };
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index 4d9c21e..16a6c16 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -80,8 +80,10 @@
                 is-mobile="vm.forceDashboardMobileMode"
                 is-mobile-disabled="vm.widgetEditMode"
                 is-edit-action-enabled="vm.isEdit || vm.widgetEditMode"
+                is-export-action-enabled="vm.isEdit && !vm.widgetEditMode"
                 is-remove-action-enabled="vm.isEdit && !vm.widgetEditMode"
                 on-edit-widget="vm.editWidget(event, widget)"
+                on-export-widget="vm.exportWidget(event, widget)"
                 on-widget-mouse-down="vm.widgetMouseDown(event, widget)"
                 on-widget-clicked="vm.widgetClicked(event, widget)"
                 on-widget-context-menu="vm.widgetContextMenu(event, widget)"
@@ -180,15 +182,38 @@
         </div>
     </tb-details-sidenav>
     <!-- </section> -->
-    <section layout="row" layout-wrap class="tb-footer-buttons md-fab">
-        <md-button ng-disabled="loading" ng-show="!vm.isAddingWidget && vm.isEdit && !vm.widgetEditMode"
-                   class="tb-btn-footer md-accent md-hue-2 md-fab" ng-click="vm.addWidget($event)"
-                   aria-label="{{ 'dashboard.add-widget' | translate }}">
-            <md-tooltip md-direction="top">
-                {{ 'dashboard.add-widget' | translate }}
-            </md-tooltip>
-            <ng-md-icon icon="add"></ng-md-icon>
-        </md-button>
+    <section layout="row" layout-wrap class="tb-footer-buttons md-fab" layout-align="start end">
+        <md-fab-speed-dial ng-disabled="loading" ng-show="!vm.isAddingWidget && vm.isEdit && !vm.widgetEditMode"
+                           md-open="vm.addItemActionsOpen" class="md-scale" md-direction="up">
+            <md-fab-trigger>
+                <md-button ng-disabled="loading"
+                           class="tb-btn-footer md-accent md-hue-2 md-fab"
+                           aria-label="{{ 'dashboard.add-widget' | translate }}">
+                    <md-tooltip md-direction="top">
+                        {{ 'dashboard.add-widget' | translate }}
+                    </md-tooltip>
+                    <ng-md-icon icon="add"></ng-md-icon>
+                </md-button>
+            </md-fab-trigger>
+            <md-fab-actions>
+                <md-button ng-disabled="loading"
+                           class="tmd-accent md-hue-2 md-fab" ng-click="vm.addWidget($event)"
+                           aria-label="{{ 'action.create' | translate }}">
+                    <md-tooltip md-direction="top">
+                        {{ 'dashboard.create-new-widget' | translate }}
+                    </md-tooltip>
+                    <ng-md-icon icon="insert_drive_file"></ng-md-icon>
+                </md-button>
+                <md-button ng-disabled="loading"
+                           class="tmd-accent md-hue-2 md-fab" ng-click="vm.importWidget($event)"
+                           aria-label="{{ 'action.import' | translate }}">
+                    <md-tooltip md-direction="top">
+                        {{ 'dashboard.import-widget' | translate }}
+                    </md-tooltip>
+                    <ng-md-icon icon="file_upload"></ng-md-icon>
+                </md-button>
+            </md-fab-actions>
+        </md-fab-speed-dial>
         <md-button ng-if="vm.isTenantAdmin()" ng-show="vm.isEdit && !vm.isAddingWidget && !loading && !vm.widgetEditMode" ng-disabled="loading"
                    class="tb-btn-footer md-accent md-hue-2 md-fab"
                    aria-label="{{ 'action.apply' | translate }}"
diff --git a/ui/src/app/dashboard/dashboard-fieldset.tpl.html b/ui/src/app/dashboard/dashboard-fieldset.tpl.html
index 46e15b4..053a919 100644
--- a/ui/src/app/dashboard/dashboard-fieldset.tpl.html
+++ b/ui/src/app/dashboard/dashboard-fieldset.tpl.html
@@ -17,6 +17,7 @@
 -->
 <md-button ng-click="onAssignToCustomer({event: $event})" ng-show="!isEdit && dashboardScope === 'tenant'" class="md-raised md-primary">{{ 'dashboard.assign-to-customer' | translate }}</md-button>
 <md-button ng-click="onUnassignFromCustomer({event: $event})" ng-show="!isEdit && dashboardScope === 'customer'" class="md-raised md-primary">{{ 'dashboard.unassign-from-customer' | translate }}</md-button>
+<md-button ng-click="onExportDashboard({event: $event})" ng-show="!isEdit && dashboardScope === 'tenant'" class="md-raised md-primary">{{ 'dashboard.export' | translate }}</md-button>
 <md-button ng-click="onDeleteDashboard({event: $event})" ng-show="!isEdit && dashboardScope === 'tenant'" class="md-raised md-primary">{{ 'dashboard.delete' | translate }}</md-button>
 
 <md-content class="md-padding" layout="column">
diff --git a/ui/src/app/dashboard/dashboards.controller.js b/ui/src/app/dashboard/dashboards.controller.js
index 5a93ad3..8c84607 100644
--- a/ui/src/app/dashboard/dashboards.controller.js
+++ b/ui/src/app/dashboard/dashboards.controller.js
@@ -23,7 +23,7 @@ import addDashboardsToCustomerTemplate from './add-dashboards-to-customer.tpl.ht
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function DashboardsController(userService, dashboardService, customerService, $scope, $controller, $state, $stateParams, $mdDialog, $document, $q, $translate) {
+export default function DashboardsController(userService, dashboardService, customerService, importExport, $scope, $controller, $state, $stateParams, $mdDialog, $document, $q, $translate) {
 
     var customerId = $stateParams.customerId;
 
@@ -86,6 +86,7 @@ export default function DashboardsController(userService, dashboardService, cust
 
     vm.assignToCustomer = assignToCustomer;
     vm.unassignFromCustomer = unassignFromCustomer;
+    vm.exportDashboard = exportDashboard;
 
     initController();
 
@@ -115,6 +116,14 @@ export default function DashboardsController(userService, dashboardService, cust
             dashboardActionsList.push(
                 {
                     onAction: function ($event, item) {
+                        exportDashboard($event, item);
+                    },
+                    name: function() { $translate.instant('action.export') },
+                    details: function() { return $translate.instant('dashboard.export') },
+                    icon: "file_download"
+                },
+                {
+                    onAction: function ($event, item) {
                         assignToCustomer($event, [ item.id.id ]);
                     },
                     name: function() { return $translate.instant('action.assign') },
@@ -158,7 +167,27 @@ export default function DashboardsController(userService, dashboardService, cust
                 }
             );
 
-
+            vm.dashboardGridConfig.addItemActions = [];
+            vm.dashboardGridConfig.addItemActions.push({
+                onAction: function ($event) {
+                    vm.grid.addItem($event);
+                },
+                name: function() { return $translate.instant('action.create') },
+                details: function() { return $translate.instant('dashboard.create-new-dashboard') },
+                icon: "insert_drive_file"
+            });
+            vm.dashboardGridConfig.addItemActions.push({
+                onAction: function ($event) {
+                    importExport.importDashboard($event).then(
+                        function() {
+                            vm.grid.refreshList();
+                        }
+                    );
+                },
+                name: function() { return $translate.instant('action.import') },
+                details: function() { return $translate.instant('dashboard.import') },
+                icon: "file_upload"
+            });
         } else if (vm.dashboardsScope === 'customer' || vm.dashboardsScope === 'customer_user') {
             fetchDashboardsFunction = function (pageLink) {
                 return dashboardService.getCustomerDashboards(customerId, pageLink);
@@ -344,6 +373,11 @@ export default function DashboardsController(userService, dashboardService, cust
         });
     }
 
+    function exportDashboard($event, dashboard) {
+        $event.stopPropagation();
+        importExport.exportDashboard(dashboard.id.id);
+    }
+
     function unassignDashboardsFromCustomer($event, items) {
         var confirm = $mdDialog.confirm()
             .targetEvent($event)
diff --git a/ui/src/app/dashboard/dashboards.tpl.html b/ui/src/app/dashboard/dashboards.tpl.html
index ac8ccb2..c36c249 100644
--- a/ui/src/app/dashboard/dashboards.tpl.html
+++ b/ui/src/app/dashboard/dashboards.tpl.html
@@ -25,5 +25,6 @@
 						  the-form="vm.grid.detailsForm"
 						  on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
 						  on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem)"
+						  on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)"
 						  on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details>
 </tb-grid>
diff --git a/ui/src/app/dashboard/device-aliases.controller.js b/ui/src/app/dashboard/device-aliases.controller.js
index ab2b6f4..89fe6bf 100644
--- a/ui/src/app/dashboard/device-aliases.controller.js
+++ b/ui/src/app/dashboard/device-aliases.controller.js
@@ -17,16 +17,18 @@ import './device-aliases.scss';
 
 /*@ngInject*/
 export default function DeviceAliasesController(deviceService, toast, $scope, $mdDialog, $document, $q, $translate,
-                                                deviceAliases, aliasToWidgetsMap, isSingleDevice, singleDeviceAlias) {
+                                                types, config) {
 
     var vm = this;
 
-    vm.isSingleDevice = isSingleDevice;
-    vm.singleDeviceAlias = singleDeviceAlias;
+    vm.isSingleDevice = config.isSingleDevice;
+    vm.singleDeviceAlias = config.singleDeviceAlias;
     vm.deviceAliases = [];
-    vm.aliasToWidgetsMap = aliasToWidgetsMap;
     vm.singleDevice = null;
     vm.singleDeviceSearchText = '';
+    vm.title = config.customTitle ? config.customTitle : 'device.aliases';
+    vm.disableAdd = config.disableAdd;
+    vm.aliasToWidgetsMap = {};
 
     vm.addAlias = addAlias;
     vm.cancel = cancel;
@@ -39,9 +41,48 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
     initController();
 
     function initController() {
-        for (var aliasId in deviceAliases) {
-            var alias = deviceAliases[aliasId].alias;
-            var deviceId = deviceAliases[aliasId].deviceId;
+        var aliasId;
+        if (config.widgets) {
+            var widgetsTitleList, widget;
+            if (config.isSingleWidget && config.widgets.length == 1) {
+                widget = config.widgets[0];
+                widgetsTitleList = [widget.config.title];
+                for (aliasId in config.deviceAliases) {
+                    vm.aliasToWidgetsMap[aliasId] = widgetsTitleList;
+                }
+            } else {
+                for (var w in config.widgets) {
+                    widget = config.widgets[w];
+                    if (widget.type === types.widgetType.rpc.value) {
+                        if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
+                            var targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];
+                            widgetsTitleList = vm.aliasToWidgetsMap[targetDeviceAliasId];
+                            if (!widgetsTitleList) {
+                                widgetsTitleList = [];
+                                vm.aliasToWidgetsMap[targetDeviceAliasId] = widgetsTitleList;
+                            }
+                            widgetsTitleList.push(widget.config.title);
+                        }
+                    } else {
+                        for (var i in widget.config.datasources) {
+                            var datasource = widget.config.datasources[i];
+                            if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
+                                widgetsTitleList = vm.aliasToWidgetsMap[datasource.deviceAliasId];
+                                if (!widgetsTitleList) {
+                                    widgetsTitleList = [];
+                                    vm.aliasToWidgetsMap[datasource.deviceAliasId] = widgetsTitleList;
+                                }
+                                widgetsTitleList.push(widget.config.title);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        for (aliasId in config.deviceAliases) {
+            var alias = config.deviceAliases[aliasId].alias;
+            var deviceId = config.deviceAliases[aliasId].deviceId;
             var deviceAlias = {id: aliasId, alias: alias, device: null, changed: false, searchText: ''};
             if (deviceId) {
                 fetchAliasDevice(deviceAlias, deviceId);
diff --git a/ui/src/app/dashboard/device-aliases.tpl.html b/ui/src/app/dashboard/device-aliases.tpl.html
index bf5bb61..22421a3 100644
--- a/ui/src/app/dashboard/device-aliases.tpl.html
+++ b/ui/src/app/dashboard/device-aliases.tpl.html
@@ -15,11 +15,11 @@
     limitations under the License.
 
 -->
-<md-dialog style="width: 700px;" aria-label="{{ 'device.aliases' | translate }}">
+<md-dialog style="width: 700px;" aria-label="{{ vm.title | translate }}">
 	<form name="theForm" ng-submit="vm.save()">
 	    <md-toolbar>
 	      <div class="md-toolbar-tools">
-	        <h2>{{ vm.isSingleDevice ? ('device.select-device-for-alias' | translate:vm.singleDeviceAlias ) : ('device.aliases' | translate) }}</h2>
+	        <h2>{{ vm.isSingleDevice ? ('device.select-device-for-alias' | translate:vm.singleDeviceAlias ) : (vm.title | translate) }}</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>
@@ -109,7 +109,7 @@
 				          </div>						
 					</div>					
 				</div>
-				<div ng-show="!vm.isSingleDevice" style="padding-bottom: 10px;">
+				<div ng-show="!vm.isSingleDevice && !vm.disableAdd" style="padding-bottom: 10px;">
 			         <md-button ng-disabled="loading" class="md-primary md-raised" ng-click="vm.addAlias($event)" aria-label="{{ 'action.add' | translate }}">
 			         		<md-tooltip md-direction="top">
 			      						{{ 'device.add-alias' | translate }}
diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js
index 34b11f7..ecdf7ed 100644
--- a/ui/src/app/dashboard/edit-widget.directive.js
+++ b/ui/src/app/dashboard/edit-widget.directive.js
@@ -76,10 +76,12 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
                 controllerAs: 'vm',
                 templateUrl: deviceAliasesTemplate,
                 locals: {
-                    deviceAliases: angular.copy(scope.dashboard.configuration.deviceAliases),
-                    aliasToWidgetsMap: null,
-                    isSingleDevice: true,
-                    singleDeviceAlias: singleDeviceAlias
+                    config: {
+                        deviceAliases: angular.copy(scope.dashboard.configuration.deviceAliases),
+                        widgets: null,
+                        isSingleDevice: true,
+                        singleDeviceAlias: singleDeviceAlias
+                    }
                 },
                 parent: angular.element($document[0].body),
                 fullscreen: true,
diff --git a/ui/src/app/dashboard/index.js b/ui/src/app/dashboard/index.js
index 461f56e..7dc9eff 100644
--- a/ui/src/app/dashboard/index.js
+++ b/ui/src/app/dashboard/index.js
@@ -30,6 +30,7 @@ import thingsboardExpandFullscreen from '../components/expand-fullscreen.directi
 import thingsboardWidgetsBundleSelect from '../components/widgets-bundle-select.directive';
 import thingsboardTypes from '../common/types.constant';
 import thingsboardItemBuffer from '../services/item-buffer.service';
+import thingsboardImportExport from '../import-export';
 
 import DashboardRoutes from './dashboard.routes';
 import DashboardsController from './dashboards.controller';
@@ -47,6 +48,7 @@ export default angular.module('thingsboard.dashboard', [
     gridster.name,
     thingsboardTypes,
     thingsboardItemBuffer,
+    thingsboardImportExport,
     thingsboardGrid,
     thingsboardApiWidget,
     thingsboardApiUser,
diff --git a/ui/src/app/global-interceptor.service.js b/ui/src/app/global-interceptor.service.js
index 7061152..06798ea 100644
--- a/ui/src/app/global-interceptor.service.js
+++ b/ui/src/app/global-interceptor.service.js
@@ -148,6 +148,7 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
             $rootScope.loading = false;
         }
         var unhandled = false;
+        var ignoreErrors = rejection.config.ignoreErrors;
         if (rejection.refreshTokenPending || rejection.status === 401) {
             var errorCode = rejectionErrorCode(rejection);
             if (rejection.refreshTokenPending || (errorCode && errorCode === getTypes().serverErrorCode.jwtTokenExpired)) {
@@ -156,13 +157,17 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
                 unhandled = true;
             }
         } else if (rejection.status === 403) {
-            $rootScope.$broadcast('forbidden');
+            if (!ignoreErrors) {
+                $rootScope.$broadcast('forbidden');
+            }
         } else if (rejection.status === 0 || rejection.status === -1) {
             getToast().showError(getTranslate().instant('error.unable-to-connect'));
         } else if (!rejection.config.url.startsWith('/api/plugins/rpc')) {
             if (rejection.status === 404) {
-                getToast().showError(rejection.config.method + ": " + rejection.config.url + "<br/>" +
-                    rejection.status + ": " + rejection.statusText);
+                if (!ignoreErrors) {
+                    getToast().showError(rejection.config.method + ": " + rejection.config.url + "<br/>" +
+                        rejection.status + ": " + rejection.statusText);
+                }
             } else {
                 unhandled = true;
             }
diff --git a/ui/src/app/import-export/import-dialog.controller.js b/ui/src/app/import-export/import-dialog.controller.js
new file mode 100644
index 0000000..8a43fc7
--- /dev/null
+++ b/ui/src/app/import-export/import-dialog.controller.js
@@ -0,0 +1,71 @@
+/*
+ * Copyright © 2016 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 './import-dialog.scss';
+
+/*@ngInject*/
+export default function ImportDialogController($scope, $mdDialog, toast, importTitle, importFileLabel) {
+
+    var vm = this;
+
+    vm.cancel = cancel;
+    vm.importFromJson = importFromJson;
+    vm.fileAdded = fileAdded;
+    vm.clearFile = clearFile;
+
+    vm.importTitle = importTitle;
+    vm.importFileLabel = importFileLabel;
+
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function fileAdded($file) {
+        if ($file.getExtension() === 'json') {
+            var reader = new FileReader();
+            reader.onload = function(event) {
+                $scope.$apply(function() {
+                    if (event.target.result) {
+                        $scope.theForm.$setDirty();
+                        var importJson = event.target.result;
+                        if (importJson && importJson.length > 0) {
+                            try {
+                                vm.importData = angular.fromJson(importJson);
+                                vm.fileName = $file.name;
+                            } catch (err) {
+                                vm.fileName = null;
+                                toast.showError(err.message);
+                            }
+                        }
+                    }
+                });
+            };
+            reader.readAsText($file.file);
+        }
+    }
+
+    function clearFile() {
+        $scope.theForm.$setDirty();
+        vm.fileName = null;
+        vm.importData = null;
+    }
+
+    function importFromJson() {
+        $scope.theForm.$setPristine();
+        $mdDialog.hide(vm.importData);
+    }
+}
diff --git a/ui/src/app/import-export/import-dialog.scss b/ui/src/app/import-export/import-dialog.scss
new file mode 100644
index 0000000..712126f
--- /dev/null
+++ b/ui/src/app/import-export/import-dialog.scss
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016 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.
+ */
+$previewSize: 100px;
+
+.file-input {
+  display: none;
+}
+
+.tb-container {
+  position: relative;
+  margin-top: 32px;
+  padding: 10px 0;
+}
+
+.tb-file-select-container {
+  position: relative;
+  height: $previewSize;
+  width: 100%;
+}
+
+.tb-file-preview {
+  max-width: $previewSize;
+  max-height: $previewSize;
+  width: auto;
+  height: auto;
+}
+
+.tb-flow-drop {
+  position: relative;
+  border: dashed 2px;
+  height: $previewSize;
+  vertical-align: top;
+  padding: 0 8px;
+  overflow: hidden;
+  min-width: 300px;
+  label {
+    width: 100%;
+    font-size: 24px;
+    text-align: center;
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%,-50%);
+  }
+}
+
+.tb-file-clear-container {
+  width: 48px;
+  height: $previewSize;
+  position: relative;
+  float: right;
+}
+.tb-file-clear-btn {
+  position: absolute !important;
+  top: 50%;
+  transform: translate(0%,-50%) !important;
+}
diff --git a/ui/src/app/import-export/import-dialog.tpl.html b/ui/src/app/import-export/import-dialog.tpl.html
new file mode 100644
index 0000000..2a8f316
--- /dev/null
+++ b/ui/src/app/import-export/import-dialog.tpl.html
@@ -0,0 +1,72 @@
+<!--
+
+    Copyright © 2016 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 aria-label="{{ vm.importTitle | translate }}">
+    <form name="theForm" ng-submit="vm.importFromJson()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>{{ vm.importTitle }}</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-show="loading"></md-progress-linear>
+        <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <fieldset ng-disabled="loading">
+                    <div layout="column" layout-padding>
+                        <div class="tb-container">
+                            <label class="tb-label" translate>{{ vm.importFileLabel }}</label>
+                            <div flow-init="{singleFile:true}"
+                                 flow-file-added="vm.fileAdded( $file )" class="tb-file-select-container">
+                                <div class="tb-file-clear-container">
+                                    <md-button ng-click="vm.clearFile()"
+                                               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="select" translate>import.drop-file</label>
+                                    <input class="file-input" flow-btn flow-attrs="{accept:'.json,application/json'}" id="select">
+                                </div>
+                            </div>
+                        </div>
+                        <div>
+                            <div ng-show="!vm.fileName" translate>import.no-file</div>
+                            <div ng-show="vm.fileName">{{ vm.fileName }}</div>
+                        </div>
+                    </div>
+                </fieldset>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="loading || !theForm.$dirty || !theForm.$valid || !vm.importData" type="submit" class="md-raised md-primary">
+                {{ 'action.import' | 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/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js
new file mode 100644
index 0000000..d409498
--- /dev/null
+++ b/ui/src/app/import-export/import-export.service.js
@@ -0,0 +1,373 @@
+/*
+ * Copyright © 2016 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.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import importDialogTemplate from './import-dialog.tpl.html';
+import deviceAliasesTemplate from '../dashboard/device-aliases.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+
+/* eslint-disable no-undef, angular/window-service, angular/document-service */
+
+/*@ngInject*/
+export default function ImportExport($log, $translate, $q, $mdDialog, $document, itembuffer, deviceService, dashboardService, toast) {
+
+
+    var service = {
+        exportDashboard: exportDashboard,
+        importDashboard: importDashboard,
+        exportWidget: exportWidget,
+        importWidget: importWidget
+    }
+
+    return service;
+
+    // Widget functions
+
+    function exportWidget(dashboard, widget) {
+        var widgetItem = itembuffer.prepareWidgetItem(dashboard, widget);
+        var name = widgetItem.widget.config.title;
+        name = name.toLowerCase().replace(/\W/g,"_");
+        exportToPc(prepareExport(widgetItem), name + '.json');
+    }
+
+    function importWidget($event, dashboard) {
+        openImportDialog($event, 'dashboard.import-widget', 'dashboard.widget-file').then(
+            function success(widgetItem) {
+                if (!validateImportedWidget(widgetItem)) {
+                    toast.showError($translate.instant('dashboard.invalid-widget-file-error'));
+                } else {
+                    var widget = widgetItem.widget;
+                    var aliasesInfo = widgetItem.aliasesInfo;
+                    var originalColumns = widgetItem.originalColumns;
+
+                    var datasourceAliases = aliasesInfo.datasourceAliases;
+                    var targetDeviceAliases = aliasesInfo.targetDeviceAliases;
+                    if (datasourceAliases || targetDeviceAliases) {
+                        var deviceAliases = {};
+                        var datasourceAliasesMap = {};
+                        var targetDeviceAliasesMap = {};
+                        var aliasId = 1;
+                        var datasourceIndex;
+                        if (datasourceAliases) {
+                            for (datasourceIndex in datasourceAliases) {
+                                datasourceAliasesMap[aliasId] = datasourceIndex;
+                                deviceAliases[aliasId] = {
+                                    alias: datasourceAliases[datasourceIndex].aliasName,
+                                    deviceId: datasourceAliases[datasourceIndex].deviceId
+                                };
+                                aliasId++;
+                            }
+                        }
+                        if (targetDeviceAliases) {
+                            for (datasourceIndex in targetDeviceAliases) {
+                                targetDeviceAliasesMap[aliasId] = datasourceIndex;
+                                deviceAliases[aliasId] = {
+                                    alias: targetDeviceAliases[datasourceIndex].aliasName,
+                                    deviceId: targetDeviceAliases[datasourceIndex].deviceId
+                                };
+                                aliasId++;
+                            }
+                        }
+
+                        var aliasIds = Object.keys(deviceAliases);
+                        if (aliasIds.length > 0) {
+                            processDeviceAliases(deviceAliases, aliasIds).then(
+                                function(missingDeviceAliases) {
+                                    if (Object.keys(missingDeviceAliases).length > 0) {
+                                        editMissingAliases($event, [ widget ],
+                                              true, 'dashboard.widget-import-missing-aliases-title', missingDeviceAliases).then(
+                                            function success(updatedDeviceAliases) {
+                                                for (var aliasId in updatedDeviceAliases) {
+                                                    var deviceAlias = updatedDeviceAliases[aliasId];
+                                                    var datasourceIndex;
+                                                    if (datasourceAliasesMap[aliasId]) {
+                                                        datasourceIndex = datasourceAliasesMap[aliasId];
+                                                        datasourceAliases[datasourceIndex].deviceId = deviceAlias.deviceId;
+                                                    } else if (targetDeviceAliasesMap[aliasId]) {
+                                                        datasourceIndex = targetDeviceAliasesMap[aliasId];
+                                                        targetDeviceAliases[datasourceIndex].deviceId = deviceAlias.deviceId;
+                                                    }
+                                                }
+                                                addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
+                                            },
+                                            function fail() {}
+                                        );
+                                    } else {
+                                        addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
+                                    }
+                                }
+                            );
+                        } else {
+                            addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
+                        }
+                    } else {
+                        addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
+                    }
+                }
+            },
+            function fail() {}
+        );
+    }
+
+    function validateImportedWidget(widgetItem) {
+        if (angular.isUndefined(widgetItem.widget)
+            || angular.isUndefined(widgetItem.aliasesInfo)
+            || angular.isUndefined(widgetItem.originalColumns)) {
+            return false;
+        }
+        var widget = widgetItem.widget;
+        if (angular.isUndefined(widget.isSystemType) ||
+            angular.isUndefined(widget.bundleAlias) ||
+            angular.isUndefined(widget.typeAlias) ||
+            angular.isUndefined(widget.type)) {
+            return false;
+        }
+        return true;
+    }
+
+    function addImportedWidget(dashboard, widget, aliasesInfo, originalColumns) {
+        itembuffer.addWidgetToDashboard(dashboard, widget, aliasesInfo, originalColumns, -1, -1);
+    }
+
+    // Dashboard functions
+
+    function exportDashboard(dashboardId) {
+        dashboardService.getDashboard(dashboardId).then(
+            function success(dashboard) {
+                var name = dashboard.title;
+                name = name.toLowerCase().replace(/\W/g,"_");
+                exportToPc(prepareExport(dashboard), name + '.json');
+            },
+            function fail(rejection) {
+                var message = rejection;
+                if (!message) {
+                    message = $translate.instant('error.unknown-error');
+                }
+                toast.showError($translate.instant('dashboard.export-failed-error', {error: message}));
+            }
+        );
+    }
+
+    function importDashboard($event) {
+        var deferred = $q.defer();
+        openImportDialog($event, 'dashboard.import', 'dashboard.dashboard-file').then(
+            function success(dashboard) {
+                if (!validateImportedDashboard(dashboard)) {
+                    toast.showError($translate.instant('dashboard.invalid-dashboard-file-error'));
+                    deferred.reject();
+                } else {
+                    var deviceAliases = dashboard.configuration.deviceAliases;
+                    if (deviceAliases) {
+                        var aliasIds = Object.keys( deviceAliases );
+                        if (aliasIds.length > 0) {
+                            processDeviceAliases(deviceAliases, aliasIds).then(
+                                function(missingDeviceAliases) {
+                                    if (Object.keys( missingDeviceAliases ).length > 0) {
+                                        editMissingAliases($event, dashboard.configuration.widgets,
+                                                false, 'dashboard.dashboard-import-missing-aliases-title', missingDeviceAliases).then(
+                                            function success(updatedDeviceAliases) {
+                                                for (var aliasId in updatedDeviceAliases) {
+                                                    deviceAliases[aliasId] = updatedDeviceAliases[aliasId];
+                                                }
+                                                saveImportedDashboard(dashboard, deferred);
+                                            },
+                                            function fail() {
+                                                deferred.reject();
+                                            }
+                                        );
+                                    } else {
+                                        saveImportedDashboard(dashboard, deferred);
+                                    }
+                                }
+                            )
+                        } else {
+                            saveImportedDashboard(dashboard, deferred);
+                        }
+                    } else {
+                        saveImportedDashboard(dashboard, deferred);
+                    }
+                }
+            },
+            function fail() {
+                deferred.reject();
+            }
+        );
+        return deferred.promise;
+    }
+
+    function saveImportedDashboard(dashboard, deferred) {
+        dashboardService.saveDashboard(dashboard).then(
+            function success() {
+                deferred.resolve();
+            },
+            function fail() {
+                deferred.reject();
+            }
+        )
+    }
+
+    function validateImportedDashboard(dashboard) {
+        if (angular.isUndefined(dashboard.title) || angular.isUndefined(dashboard.configuration)) {
+            return false;
+        }
+        return true;
+    }
+
+    function processDeviceAliases(deviceAliases, aliasIds) {
+        var deferred = $q.defer();
+        var missingDeviceAliases = {};
+        var index = -1;
+        checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
+        return deferred.promise;
+    }
+
+    function checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred) {
+        index++;
+        if (index == aliasIds.length) {
+            deferred.resolve(missingDeviceAliases);
+        } else {
+            checkDeviceAlias(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
+        }
+    }
+
+    function checkDeviceAlias(index, aliasIds, deviceAliases, missingDeviceAliases, deferred) {
+        var aliasId = aliasIds[index];
+        var deviceAlias = deviceAliases[aliasId];
+        if (deviceAlias.deviceId) {
+            deviceService.getDevice(deviceAlias.deviceId, true).then(
+                function success() {
+                    checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
+                },
+                function fail() {
+                    var missingDeviceAlias = angular.copy(deviceAlias);
+                    missingDeviceAlias.deviceId = null;
+                    missingDeviceAliases[aliasId] = missingDeviceAlias;
+                    checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
+                }
+            );
+        }
+    }
+
+    function editMissingAliases($event, widgets, isSingleWidget, customTitle, missingDeviceAliases) {
+        var deferred = $q.defer();
+        $mdDialog.show({
+            controller: 'DeviceAliasesController',
+            controllerAs: 'vm',
+            templateUrl: deviceAliasesTemplate,
+            locals: {
+                config: {
+                    deviceAliases: missingDeviceAliases,
+                    widgets: widgets,
+                    isSingleWidget: isSingleWidget,
+                    isSingleDevice: false,
+                    singleDeviceAlias: null,
+                    customTitle: customTitle,
+                    disableAdd: true
+                }
+            },
+            parent: angular.element($document[0].body),
+            skipHide: true,
+            fullscreen: true,
+            targetEvent: $event
+        }).then(function (updatedDeviceAliases) {
+            deferred.resolve(updatedDeviceAliases);
+        }, function () {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    // Common functions
+
+    function prepareExport(data) {
+        var exportedData = angular.copy(data);
+        if (angular.isDefined(exportedData.id)) {
+            delete exportedData.id;
+        }
+        if (angular.isDefined(exportedData.createdTime)) {
+            delete exportedData.createdTime;
+        }
+        if (angular.isDefined(exportedData.tenantId)) {
+            delete exportedData.tenantId;
+        }
+        if (angular.isDefined(exportedData.customerId)) {
+            delete exportedData.customerId;
+        }
+        return exportedData;
+    }
+
+    function exportToPc(data, filename) {
+        if (!data) {
+            $log.error('No data');
+            return;
+        }
+
+        if (!filename) {
+            filename = 'download.json';
+        }
+
+        if (angular.isObject(data)) {
+            data = angular.toJson(data, 2);
+        }
+
+        var blob = new Blob([data], {type: 'text/json'});
+
+        // FOR IE:
+
+        if (window.navigator && window.navigator.msSaveOrOpenBlob) {
+            window.navigator.msSaveOrOpenBlob(blob, filename);
+        }
+        else{
+            var e = document.createEvent('MouseEvents'),
+                a = document.createElement('a');
+
+            a.download = filename;
+            a.href = window.URL.createObjectURL(blob);
+            a.dataset.downloadurl = ['text/json', a.download, a.href].join(':');
+            e.initEvent('click', true, false, window,
+                0, 0, 0, 0, 0, false, false, false, false, 0, null);
+            a.dispatchEvent(e);
+        }
+    }
+
+    function openImportDialog($event, importTitle, importFileLabel) {
+        var deferred = $q.defer();
+        $mdDialog.show({
+            controller: 'ImportDialogController',
+            controllerAs: 'vm',
+            templateUrl: importDialogTemplate,
+            locals: {
+                importTitle: importTitle,
+                importFileLabel: importFileLabel
+            },
+            parent: angular.element($document[0].body),
+            skipHide: true,
+            fullscreen: true,
+            targetEvent: $event
+        }).then(function (importData) {
+            deferred.resolve(importData);
+        }, function () {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+}
+
+/* eslint-enable no-undef, angular/window-service, angular/document-service */
diff --git a/ui/src/app/import-export/index.js b/ui/src/app/import-export/index.js
new file mode 100644
index 0000000..6d76229
--- /dev/null
+++ b/ui/src/app/import-export/index.js
@@ -0,0 +1,24 @@
+/*
+ * Copyright © 2016 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 ImportExport from './import-export.service';
+import ImportDialogController from './import-dialog.controller';
+
+
+export default angular.module('thingsboard.importexport', [])
+    .factory('importExport', ImportExport)
+    .controller('ImportDialogController', ImportDialogController)
+    .name;
diff --git a/ui/src/app/services/item-buffer.service.js b/ui/src/app/services/item-buffer.service.js
index 56d8d6f..2e59bb6 100644
--- a/ui/src/app/services/item-buffer.service.js
+++ b/ui/src/app/services/item-buffer.service.js
@@ -25,11 +25,12 @@ export default angular.module('thingsboard.itembuffer', [angularStorage])
     .name;
 
 /*@ngInject*/
-function ItemBuffer(bufferStore) {
+function ItemBuffer(bufferStore, types) {
 
     const WIDGET_ITEM = "widget_item";
 
     var service = {
+        prepareWidgetItem: prepareWidgetItem,
         copyWidget: copyWidget,
         hasWidget: hasWidget,
         pasteWidget: pasteWidget,
@@ -56,12 +57,57 @@ function ItemBuffer(bufferStore) {
      }
     **/
 
-    function copyWidget(widget, aliasesInfo, originalColumns) {
-        var widgetItem = {
+    function prepareWidgetItem(dashboard, widget) {
+        var aliasesInfo = {
+            datasourceAliases: {},
+            targetDeviceAliases: {}
+        };
+        var originalColumns = 24;
+        if (dashboard.configuration.gridSettings &&
+            dashboard.configuration.gridSettings.columns) {
+            originalColumns = dashboard.configuration.gridSettings.columns;
+        }
+        if (widget.config && dashboard.configuration
+            && dashboard.configuration.deviceAliases) {
+            var deviceAlias;
+            if (widget.config.datasources) {
+                for (var i=0;i<widget.config.datasources.length;i++) {
+                    var datasource = widget.config.datasources[i];
+                    if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
+                        deviceAlias = dashboard.configuration.deviceAliases[datasource.deviceAliasId];
+                        if (deviceAlias) {
+                            aliasesInfo.datasourceAliases[i] = {
+                                aliasName: deviceAlias.alias,
+                                deviceId: deviceAlias.deviceId
+                            }
+                        }
+                    }
+                }
+            }
+            if (widget.config.targetDeviceAliasIds) {
+                for (i=0;i<widget.config.targetDeviceAliasIds.length;i++) {
+                    var targetDeviceAliasId = widget.config.targetDeviceAliasIds[i];
+                    if (targetDeviceAliasId) {
+                        deviceAlias = dashboard.configuration.deviceAliases[targetDeviceAliasId];
+                        if (deviceAlias) {
+                            aliasesInfo.targetDeviceAliases[i] = {
+                                aliasName: deviceAlias.alias,
+                                deviceId: deviceAlias.deviceId
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return {
             widget: widget,
             aliasesInfo: aliasesInfo,
             originalColumns: originalColumns
         }
+    }
+
+    function copyWidget(dashboard, widget) {
+        var widgetItem = prepareWidgetItem(dashboard, widget);
         bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem));
     }
 
@@ -69,7 +115,7 @@ function ItemBuffer(bufferStore) {
         return bufferStore.get(WIDGET_ITEM);
     }
 
-    function pasteWidget(targetDasgboard, position) {
+    function pasteWidget(targetDashboard, position) {
         var widgetItemJson = bufferStore.get(WIDGET_ITEM);
         if (widgetItemJson) {
             var widgetItem = angular.fromJson(widgetItemJson);
@@ -82,7 +128,7 @@ function ItemBuffer(bufferStore) {
                 targetRow = position.row;
                 targetColumn = position.column;
             }
-            addWidgetToDashboard(targetDasgboard, widget, aliasesInfo, originalColumns, targetRow, targetColumn);
+            addWidgetToDashboard(targetDashboard, widget, aliasesInfo, originalColumns, targetRow, targetColumn);
         }
     }
 
diff --git a/ui/src/app/widget/lib/analogue-linear-gauge.js b/ui/src/app/widget/lib/analogue-linear-gauge.js
index 72b5d6e..54ddeb9 100644
--- a/ui/src/app/widget/lib/analogue-linear-gauge.js
+++ b/ui/src/app/widget/lib/analogue-linear-gauge.js
@@ -34,7 +34,13 @@ export default class TbAnalogueLinearGauge {
         var majorTicksCount = settings.majorTicksCount || 10;
         var total = maxValue-minValue;
         var step = (total/majorTicksCount);
-        step = parseFloat(parseFloat(step).toPrecision(12));
+
+        var valueInt = settings.valueInt || 3;
+
+        var valueDec = (angular.isDefined(settings.valueDec) && settings.valueDec !== null)
+            ? settings.valueDec : 2;
+
+        step = parseFloat(parseFloat(step).toFixed(valueDec));
 
         var majorTicks = [];
         var highlights = [];
@@ -44,7 +50,7 @@ export default class TbAnalogueLinearGauge {
             var majorTick = tick + minValue;
             majorTicks.push(majorTick);
             var nextTick = tick+step;
-            nextTick = parseFloat(parseFloat(nextTick).toPrecision(12));
+            nextTick = parseFloat(parseFloat(nextTick).toFixed(valueDec));
             if (tick<total) {
                 var highlightColor = tinycolor(keyColor);
                 var percent = tick/total;
@@ -89,9 +95,8 @@ export default class TbAnalogueLinearGauge {
             // borders
 
             // number formats
-            valueInt: settings.valueInt || 3,
-            valueDec: (angular.isDefined(settings.valueDec) && settings.valueDec !== null)
-                ? settings.valueDec : 2,
+            valueInt: valueInt,
+            valueDec: valueDec,
             majorTicksInt: 1,
             majorTicksDec: 0,
 
diff --git a/ui/src/app/widget/lib/analogue-radial-gauge.js b/ui/src/app/widget/lib/analogue-radial-gauge.js
index c970b9d..1ed8aa4 100644
--- a/ui/src/app/widget/lib/analogue-radial-gauge.js
+++ b/ui/src/app/widget/lib/analogue-radial-gauge.js
@@ -35,7 +35,13 @@ export default class TbAnalogueRadialGauge {
         var majorTicksCount = settings.majorTicksCount || 10;
         var total = maxValue-minValue;
         var step = (total/majorTicksCount);
-        step = parseFloat(parseFloat(step).toPrecision(12));
+
+        var valueInt = settings.valueInt || 3;
+
+        var valueDec = (angular.isDefined(settings.valueDec) && settings.valueDec !== null)
+            ? settings.valueDec : 2;
+
+        step = parseFloat(parseFloat(step).toFixed(valueDec));
 
         var majorTicks = [];
         var highlights = [];
@@ -44,7 +50,7 @@ export default class TbAnalogueRadialGauge {
         while(tick<=maxValue) {
             majorTicks.push(tick);
             var nextTick = tick+step;
-            nextTick = parseFloat(parseFloat(nextTick).toPrecision(12));
+            nextTick = parseFloat(parseFloat(nextTick).toFixed(valueDec));
             if (tick<maxValue) {
                 var highlightColor = tinycolor(keyColor);
                 var percent = (tick-minValue)/total;
@@ -86,9 +92,8 @@ export default class TbAnalogueRadialGauge {
             //borderShadowWidth: (settings.showBorder !== false) ? 3 : 0,
 
             // number formats
-            valueInt: settings.valueInt || 3,
-            valueDec: (angular.isDefined(settings.valueDec) && settings.valueDec !== null)
-                ? settings.valueDec : 2,
+            valueInt: valueInt,
+            valueDec: valueDec,
             majorTicksInt: 1,
             majorTicksDec: 0,
 
diff --git a/ui/src/locale/en_US.json b/ui/src/locale/en_US.json
index 5de8938..ccbc3e7 100644
--- a/ui/src/locale/en_US.json
+++ b/ui/src/locale/en_US.json
@@ -40,7 +40,9 @@
     "refresh": "Refresh",
     "undo": "Undo",
     "copy": "Copy",
-    "paste": "Paste"
+    "paste": "Paste",
+    "import": "Import",
+    "export": "Export"
   },
   "admin": {
     "general": "General",
@@ -214,7 +216,19 @@
     "vertical-margin-required": "Vertical margin value is required.",
     "min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.",
     "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.",
-    "display-title": "Display dashboard title"
+    "display-title": "Display dashboard title",
+    "import": "Import dashboard",
+    "export": "Export dashboard",
+    "export-failed-error": "Unable to export dashboard: {error}",
+    "create-new-dashboard": "Create new dashboard",
+    "dashboard-file": "Dashboard file",
+    "invalid-dashboard-file-error": "Unable to import dashboard: Invalid dashboard data structure.",
+    "dashboard-import-missing-aliases-title": "Select missing devices for dashboard aliases",
+    "create-new-widget": "Create new widget",
+    "import-widget": "Import widget",
+    "widget-file": "Widget file",
+    "invalid-widget-file-error": "Unable to import widget: Invalid widget data structure.",
+    "widget-import-missing-aliases-title": "Select missing devices used by widget"
   },
   "datakey": {
     "settings": "Settings",
@@ -370,6 +384,10 @@
     "avatar": "Avatar",
     "open-user-menu": "Open user menu"
   },
+  "import": {
+    "no-file": "No file selected",
+    "drop-file": "Drop a JSON file or click to select a file to upload."
+  },
   "item": {
     "selected": "Selected"
   },
@@ -612,7 +630,8 @@
     "widget-type-load-failed-error": "Failed to load widget type!",
     "widget-template-load-failed-error": "Failed to load widget template!",
     "add": "Add Widget",
-    "undo": "Undo widget changes"
+    "undo": "Undo widget changes",
+    "export": "Export widget"
   },
   "widgets-bundle": {
     "current": "Current bundle",