thingsboard-aplcache

Add widgets copy/past ability

12/26/2016 1:07:47 PM

Details

diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index 3aa6cea..982f15e 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -49,6 +49,7 @@ import thingsboardDialogs from './components/datakey-config-dialog.controller';
 import thingsboardMenu from './services/menu.service';
 import thingsboardUtils from './common/utils.service';
 import thingsboardTypes from './common/types.constant';
+import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter';
 import thingsboardHelp from './help/help.directive';
 import thingsboardToast from './services/toast';
 import thingsboardHome from './layout';
@@ -95,6 +96,7 @@ angular.module('thingsboard', [
     thingsboardMenu,
     thingsboardUtils,
     thingsboardTypes,
+    thingsboardKeyboardShortcut,
     thingsboardHelp,
     thingsboardToast,
     thingsboardHome,
diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js
index f90b055..7c5094a 100644
--- a/ui/src/app/components/dashboard.directive.js
+++ b/ui/src/app/components/dashboard.directive.js
@@ -23,6 +23,7 @@ import thingsboardWidget from './widget.directive';
 import thingsboardToast from '../services/toast';
 import thingsboardTimewindow from './timewindow.directive';
 import thingsboardEvents from './tb-event-directives';
+import thingsboardMousepointMenu from './mousepoint-menu.directive';
 
 /* eslint-disable import/no-unresolved, import/default */
 
@@ -38,6 +39,7 @@ export default angular.module('thingsboard.directives.dashboard', [thingsboardTy
     thingsboardWidget,
     thingsboardTimewindow,
     thingsboardEvents,
+    thingsboardMousepointMenu,
     gridster.name])
     .directive('tbDashboard', Dashboard)
     .name;
@@ -59,7 +61,10 @@ function Dashboard() {
             isRemoveActionEnabled: '=',
             onEditWidget: '&?',
             onRemoveWidget: '&?',
+            onWidgetMouseDown: '&?',
             onWidgetClicked: '&?',
+            prepareDashboardContextMenu: '&?',
+            prepareWidgetContextMenu: '&?',
             loadWidgets: '&?',
             onInit: '&?',
             onInitFailed: '&?',
@@ -75,8 +80,9 @@ function Dashboard() {
 function DashboardController($scope, $rootScope, $element, $timeout, $log, toast, types) {
 
     var highlightedMode = false;
-    var highlightedIndex = -1;
-    var mouseDownIndex = -1;
+    var highlightedWidget = null;
+    var selectedWidget = null;
+    var mouseDownWidget = -1;
     var widgetMouseMoved = false;
 
     var gridsterParent = null;
@@ -117,6 +123,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
     vm.isWidgetExpanded = false;
     vm.isHighlighted = isHighlighted;
     vm.isNotHighlighted = isNotHighlighted;
+    vm.selectWidget = selectWidget;
+    vm.getSelectedWidget = getSelectedWidget;
     vm.highlightWidget = highlightWidget;
     vm.resetHighlight = resetHighlight;
 
@@ -134,6 +142,17 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
     vm.removeWidget = removeWidget;
     vm.loading = loading;
 
+    vm.openDashboardContextMenu = openDashboardContextMenu;
+    vm.openWidgetContextMenu = openWidgetContextMenu;
+
+    vm.getEventGridPosition = getEventGridPosition;
+
+    vm.contextMenuItems = [];
+    vm.contextMenuEvent = null;
+
+    vm.widgetContextMenuItems = [];
+    vm.widgetContextMenuEvent = null;
+
     //$element[0].onmousemove=function(){
     //    widgetMouseMove();
    // }
@@ -305,7 +324,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
     }
 
     function resetWidgetClick () {
-        mouseDownIndex = -1;
+        mouseDownWidget = -1;
         widgetMouseMoved = false;
     }
 
@@ -315,25 +334,27 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
     }
 
     function widgetMouseDown ($event, widget) {
-        mouseDownIndex = vm.widgets.indexOf(widget);
+        mouseDownWidget = widget;
         widgetMouseMoved = false;
+        if (vm.onWidgetMouseDown) {
+            vm.onWidgetMouseDown({event: $event, widget: widget});
+        }
     }
 
     function widgetMouseMove () {
-        if (mouseDownIndex > -1) {
+        if (mouseDownWidget) {
             widgetMouseMoved = true;
         }
     }
 
     function widgetMouseUp ($event, widget) {
         $timeout(function () {
-            if (!widgetMouseMoved && mouseDownIndex > -1) {
-                var index = vm.widgets.indexOf(widget);
-                if (index === mouseDownIndex) {
+            if (!widgetMouseMoved && mouseDownWidget) {
+                if (widget === mouseDownWidget) {
                     widgetClicked($event, widget);
                 }
             }
-            mouseDownIndex = -1;
+            mouseDownWidget = null;
             widgetMouseMoved = false;
         }, 0);
     }
@@ -347,6 +368,41 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
         }
     }
 
+    function openDashboardContextMenu($event, $mdOpenMousepointMenu) {
+        if (vm.prepareDashboardContextMenu) {
+            vm.contextMenuItems = vm.prepareDashboardContextMenu();
+            if (vm.contextMenuItems && vm.contextMenuItems.length > 0) {
+                vm.contextMenuEvent = $event;
+                $mdOpenMousepointMenu($event);
+            }
+        }
+    }
+
+    function openWidgetContextMenu($event, widget, $mdOpenMousepointMenu) {
+        if (vm.prepareWidgetContextMenu) {
+            vm.widgetContextMenuItems = vm.prepareWidgetContextMenu({widget: widget});
+            if (vm.widgetContextMenuItems && vm.widgetContextMenuItems.length > 0) {
+                vm.widgetContextMenuEvent = $event;
+                $mdOpenMousepointMenu($event);
+            }
+        }
+    }
+
+    function getEventGridPosition(event) {
+        var pos = {
+            row: 0,
+            column: 0
+        }
+        var offset = gridsterParent.offset();
+        var x = event.pageX - offset.left + gridsterParent.scrollLeft();
+        var y = event.pageY - offset.top + gridsterParent.scrollTop();
+        if (gridster) {
+            pos.row = gridster.pixelsToRows(y);
+            pos.column = gridster.pixelsToColumns(x);
+        }
+        return pos;
+    }
+
     function editWidget ($event, widget) {
         resetWidgetClick();
         if ($event) {
@@ -367,10 +423,10 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
         }
     }
 
-    function highlightWidget(widgetIndex, delay) {
+    function highlightWidget(widget, delay) {
         highlightedMode = true;
-        highlightedIndex = widgetIndex;
-        var item = $('.gridster-item', gridster.$element)[widgetIndex];
+        highlightedWidget = widget;
+        var item = $('.gridster-item', gridster.$element)[vm.widgets.indexOf(widget)];
         if (item) {
             var height = $(item).outerHeight(true);
             var rectHeight = gridsterParent.height();
@@ -385,17 +441,39 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
         }
     }
 
+    function selectWidget(widget, delay) {
+        selectedWidget = widget;
+        var item = $('.gridster-item', gridster.$element)[vm.widgets.indexOf(widget)];
+        if (item) {
+            var height = $(item).outerHeight(true);
+            var rectHeight = gridsterParent.height();
+            var offset = (rectHeight - height) / 2;
+            var scrollTop = item.offsetTop;
+            if (offset > 0) {
+                scrollTop -= offset;
+            }
+            gridsterParent.animate({
+                scrollTop: scrollTop
+            }, delay);
+        }
+    }
+
+    function getSelectedWidget() {
+        return selectedWidget;
+    }
+
     function resetHighlight() {
         highlightedMode = false;
-        highlightedIndex = -1;
+        highlightedWidget = null;
+        selectedWidget = null;
     }
 
     function isHighlighted(widget) {
-        return highlightedMode && vm.widgets.indexOf(widget) === highlightedIndex;
+        return (highlightedMode && highlightedWidget === widget) || (selectedWidget === widget);
     }
 
     function isNotHighlighted(widget) {
-        return highlightedMode && vm.widgets.indexOf(widget) != highlightedIndex;
+        return highlightedMode && highlightedWidget != widget;
     }
 
     function widgetColor(widget) {
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
index f0cecef..c43caac 100644
--- a/ui/src/app/components/dashboard.tpl.html
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -19,64 +19,90 @@
    ng-show="(vm.loading() || vm.dashboardLoading) && !vm.isEdit">
 	<md-progress-circular md-mode="indeterminate" class="md-warn" md-diameter="100"></md-progress-circular>
 </md-content>
-<md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap>
-	<div ng-style="vm.dashboardStyle" id="gridster-background" style="height: auto; min-height: 100%;">
-		<div id="gridster-child" gridster="vm.gridsterOpts">
-			<ul>
-	<!-- 			    			ng-click="widgetClicked($event, widget)"  -->
-				<li gridster-item="widget" ng-repeat="widget in vm.widgets">
-					<div tb-expand-fullscreen expand-button-id="expand-button" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" layout="column" class="tb-widget md-whiteframe-4dp"
-								ng-class="{'tb-highlighted': vm.isHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget)}"
-								tb-mousedown="vm.widgetMouseDown($event, widget)"
-								tb-mousemove="vm.widgetMouseMove($event, widget)"
-								tb-mouseup="vm.widgetMouseUp($event, widget)"
-						style="
-						cursor: pointer;
-						color: {{vm.widgetColor(widget)}};
-						background-color: {{vm.widgetBackgroundColor(widget)}};
-						padding: {{vm.widgetPadding(widget)}}
-						">
-						<div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
-							<span ng-show="vm.showWidgetTitle(widget)" class="md-subhead">{{widget.config.title}}</span>
-							<tb-timewindow ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
-						</div>
-						<div class="tb-widget-actions" layout="row" layout-align="start center">
-							<md-button id="expand-button"
-									   aria-label="{{ 'fullscreen.fullscreen' | translate }}"
-									   class="md-icon-button md-primary"></md-button>
-							<md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded"
-									   ng-disabled="vm.loading()"
-									   class="md-icon-button md-primary"
-									   ng-click="vm.editWidget($event, widget)"
-									   aria-label="{{ 'widget.edit' | translate }}">
-								<md-tooltip md-direction="top">
-									{{ 'widget.edit' | translate }}
-								</md-tooltip>
-								<md-icon class="material-icons">
-									edit
-								</md-icon>
-							</md-button>
-							<md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
-								ng-disabled="vm.loading()"
-								class="md-icon-button md-primary"
-								ng-click="vm.removeWidget($event, widget)"
-								aria-label="{{ 'widget.remove' | translate }}">
-								<md-tooltip md-direction="top">
-									{{ 'widget.remove' | translate }}
-								</md-tooltip>
-								<md-icon class="material-icons">
-								  close
-								</md-icon>
-							</md-button>
-						</div>
-						<div flex layout="column" class="tb-widget-content">
-							<div flex tb-widget
-								 locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isPreview: vm.isEdit }">
+<md-menu md-position-mode="target target" tb-mousepoint-menu>
+	<md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap ng-click="" tb-contextmenu="vm.openDashboardContextMenu($event, $mdOpenMousepointMenu)">
+		<div ng-style="vm.dashboardStyle" id="gridster-background" style="height: auto; min-height: 100%;">
+			<div id="gridster-child" gridster="vm.gridsterOpts">
+				<ul>
+		<!-- 			    			ng-click="widgetClicked($event, widget)"  -->
+					<li gridster-item="widget" ng-repeat="widget in vm.widgets">
+						<md-menu md-position-mode="target target" tb-mousepoint-menu>
+							<div tb-expand-fullscreen
+								        expand-button-id="expand-button" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" layout="column" class="tb-widget md-whiteframe-4dp"
+										ng-class="{'tb-highlighted': vm.isHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget)}"
+										tb-mousedown="vm.widgetMouseDown($event, widget)"
+										tb-mousemove="vm.widgetMouseMove($event, widget)"
+										tb-mouseup="vm.widgetMouseUp($event, widget)"
+										ng-click=""
+										tb-contextmenu="vm.openWidgetContextMenu($event, widget, $mdOpenMousepointMenu)"
+								style="
+								cursor: pointer;
+								color: {{vm.widgetColor(widget)}};
+								background-color: {{vm.widgetBackgroundColor(widget)}};
+								padding: {{vm.widgetPadding(widget)}}
+								">
+								<div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
+									<span ng-show="vm.showWidgetTitle(widget)" class="md-subhead">{{widget.config.title}}</span>
+									<tb-timewindow ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
+								</div>
+								<div class="tb-widget-actions" layout="row" layout-align="start center">
+									<md-button id="expand-button"
+											   ng-show="!vm.isEdit"
+											   aria-label="{{ 'fullscreen.fullscreen' | translate }}"
+											   class="md-icon-button md-primary"></md-button>
+									<md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded"
+											   ng-disabled="vm.loading()"
+											   class="md-icon-button md-primary"
+											   ng-click="vm.editWidget($event, widget)"
+											   aria-label="{{ 'widget.edit' | translate }}">
+										<md-tooltip md-direction="top">
+											{{ 'widget.edit' | translate }}
+										</md-tooltip>
+										<md-icon class="material-icons">
+											edit
+										</md-icon>
+									</md-button>
+									<md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
+										ng-disabled="vm.loading()"
+										class="md-icon-button md-primary"
+										ng-click="vm.removeWidget($event, widget)"
+										aria-label="{{ 'widget.remove' | translate }}">
+										<md-tooltip md-direction="top">
+											{{ 'widget.remove' | translate }}
+										</md-tooltip>
+										<md-icon class="material-icons">
+										  close
+										</md-icon>
+									</md-button>
+								</div>
+								<div flex layout="column" class="tb-widget-content">
+									<div flex tb-widget
+										 locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isPreview: vm.isEdit }">
+									</div>
+								</div>
 							</div>
-						</div>
-					</div>
-				</li>
-			</ul>
+							<md-menu-content id="menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
+								<md-menu-item ng-repeat ="item in vm.widgetContextMenuItems">
+									<md-button ng-disabled="!item.enabled" ng-click="item.action(vm.widgetContextMenuEvent, widget)">
+										<md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
+										<span translate>{{item.value}}</span>
+										<span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
+									</md-button>
+								</md-menu-item>
+							</md-menu-content>
+						</md-menu>
+					</li>
+				</ul>
+			</div>
 		</div>
-	</div>
-</md-content>
\ No newline at end of file
+	</md-content>
+	<md-menu-content id="menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
+		<md-menu-item ng-repeat ="item in vm.contextMenuItems">
+			<md-button ng-disabled="!item.enabled" ng-click="item.action(vm.contextMenuEvent)">
+				<md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
+				<span translate>{{item.value}}</span>
+				<span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
+			</md-button>
+		</md-menu-item>
+	</md-menu-content>
+</md-menu>
\ No newline at end of file
diff --git a/ui/src/app/components/keyboard-shortcut.filter.js b/ui/src/app/components/keyboard-shortcut.filter.js
new file mode 100644
index 0000000..289afe2
--- /dev/null
+++ b/ui/src/app/components/keyboard-shortcut.filter.js
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+export default angular.module('thingsboard.filters.keyboardShortcut', [])
+    .filter('keyboardShortcut', KeyboardShortcut)
+    .name;
+
+/*@ngInject*/
+function KeyboardShortcut($window) {
+    return function(str) {
+        if (!str) return;
+        var keys = str.split('-');
+        var isOSX = /Mac OS X/.test($window.navigator.userAgent);
+
+        var seperator = (!isOSX || keys.length > 2) ? '+' : '';
+
+        var abbreviations = {
+            M: isOSX ? '⌘' : 'Ctrl',
+            A: isOSX ? 'Option' : 'Alt',
+            S: 'Shift'
+        };
+
+        return keys.map(function(key, index) {
+            var last = index == keys.length - 1;
+            return last ? key : abbreviations[key];
+        }).join(seperator);
+    };
+}
diff --git a/ui/src/app/components/mousepoint-menu.directive.js b/ui/src/app/components/mousepoint-menu.directive.js
new file mode 100644
index 0000000..b3f24c7
--- /dev/null
+++ b/ui/src/app/components/mousepoint-menu.directive.js
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+export default angular.module('thingsboard.directives.mousepointMenu', [])
+    .directive('tbMousepointMenu', MousepointMenu)
+    .name;
+
+/*@ngInject*/
+function MousepointMenu() {
+
+    var linker = function ($scope, $element, $attrs, RightClickContextMenu) {
+
+        $scope.$mdOpenMousepointMenu = function($event){
+            RightClickContextMenu.offsets = function(){
+                var offset = $element.offset();
+                var x = $event.pageX - offset.left;
+                var y = $event.pageY - offset.top;
+
+                var offsets = {
+                    left: x,
+                    top: y
+                }
+                return offsets;
+            }
+            RightClickContextMenu.open($event);
+        };
+
+        $scope.$mdCloseMousepointMenu = function() {
+            RightClickContextMenu.close();
+        }
+    }
+
+    return {
+        restrict: "A",
+        link: linker,
+        require: 'mdMenu'
+    };
+}
diff --git a/ui/src/app/components/tb-event-directives.js b/ui/src/app/components/tb-event-directives.js
index 1fefd51..f7e7fe9 100644
--- a/ui/src/app/components/tb-event-directives.js
+++ b/ui/src/app/components/tb-event-directives.js
@@ -20,7 +20,7 @@ const PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i;
 var tbEventDirectives = {};
 
 angular.forEach(
-    'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
+    'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave contextmenu keydown keyup keypress submit focus blur copy cut paste'.split(' '),
     function(eventName) {
         var directiveName = directiveNormalize('tb-' + eventName);
         tbEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse) {
diff --git a/ui/src/app/components/widgets-bundle-select.directive.js b/ui/src/app/components/widgets-bundle-select.directive.js
index fcf5def..1525523 100644
--- a/ui/src/app/components/widgets-bundle-select.directive.js
+++ b/ui/src/app/components/widgets-bundle-select.directive.js
@@ -55,12 +55,26 @@ function WidgetsBundleSelect($compile, $templateCache, widgetService, types) {
                     if (widgetsBundles.length > 0) {
                         scope.widgetsBundle = widgetsBundles[0];
                     }
+                } else if (angular.isDefined(scope.selectBundleAlias)) {
+                    selectWidgetsBundleByAlias(scope.selectBundleAlias);
                 }
             },
             function fail() {
             }
         );
 
+        function selectWidgetsBundleByAlias(alias) {
+            if (scope.widgetsBundles && alias) {
+                for (var w in scope.widgetsBundles) {
+                    var widgetsBundle = scope.widgetsBundles[w];
+                    if (widgetsBundle.alias === alias) {
+                        scope.widgetsBundle = widgetsBundle;
+                        break;
+                    }
+                }
+            }
+        }
+
         scope.isSystem = function(item) {
             return item && item.tenantId.id === types.id.nullUid;
         }
@@ -79,6 +93,12 @@ function WidgetsBundleSelect($compile, $templateCache, widgetService, types) {
             scope.updateView();
         });
 
+        scope.$watch('selectBundleAlias', function (newVal, prevVal) {
+            if (newVal !== prevVal) {
+                selectWidgetsBundleByAlias(scope.selectBundleAlias);
+            }
+        });
+
         $compile(element.contents())(scope);
     }
 
@@ -90,7 +110,8 @@ function WidgetsBundleSelect($compile, $templateCache, widgetService, types) {
             bundlesScope: '@',
             theForm: '=?',
             tbRequired: '=?',
-            selectFirstBundle: '='
+            selectFirstBundle: '=',
+            selectBundleAlias: '=?'
         }
     };
 }
\ No newline at end of file
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index 68978c3..896e7a1 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, $window, $rootScope,
+                                            dashboardService, itembuffer, hotkeys, $window, $rootScope,
                                             $scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) {
 
     var user = userService.getCurrentUser();
@@ -48,7 +48,10 @@ export default function DashboardController(types, widgetService, userService,
     vm.addWidgetFromType = addWidgetFromType;
     vm.dashboardInited = dashboardInited;
     vm.dashboardInitFailed = dashboardInitFailed;
+    vm.widgetMouseDown = widgetMouseDown;
     vm.widgetClicked = widgetClicked;
+    vm.prepareDashboardContextMenu = prepareDashboardContextMenu;
+    vm.prepareWidgetContextMenu = prepareWidgetContextMenu;
     vm.editWidget = editWidget;
     vm.isTenantAdmin = isTenantAdmin;
     vm.loadDashboard = loadDashboard;
@@ -63,6 +66,7 @@ export default function DashboardController(types, widgetService, userService,
     vm.toggleDashboardEditMode = toggleDashboardEditMode;
     vm.onRevertWidgetEdit = onRevertWidgetEdit;
     vm.helpLinkIdForWidgetType = helpLinkIdForWidgetType;
+    vm.displayTitle = displayTitle;
 
     vm.widgetsBundle;
 
@@ -194,6 +198,7 @@ export default function DashboardController(types, widgetService, userService,
 
     function dashboardInited(dashboard) {
         vm.dashboardContainer = dashboard;
+        initHotKeys();
     }
 
     function isTenantAdmin() {
@@ -289,18 +294,188 @@ export default function DashboardController(types, widgetService, userService,
                 var delayOffset = transition ? 350 : 0;
                 var delay = transition ? 400 : 300;
                 $timeout(function () {
-                    vm.dashboardContainer.highlightWidget(vm.editingWidgetIndex, delay);
+                    vm.dashboardContainer.highlightWidget(widget, delay);
                 }, delayOffset, false);
             }
         }
     }
 
+    function widgetMouseDown($event, widget) {
+        if (vm.isEdit && !vm.isEditingWidget) {
+            vm.dashboardContainer.selectWidget(widget, 0);
+        }
+    }
+
     function widgetClicked($event, widget) {
         if (vm.isEditingWidget) {
             editWidget($event, widget);
         }
     }
 
+    function initHotKeys() {
+        $translate(['action.copy', 'action.paste', 'action.delete']).then(function (translations) {
+            hotkeys.bindTo($scope)
+                .add({
+                    combo: 'ctrl+c',
+                    description: translations['action.copy'],
+                    allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                    callback: function (event) {
+                        if (vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+                            var widget = vm.dashboardContainer.getSelectedWidget();
+                            if (widget) {
+                                event.preventDefault();
+                                copyWidget(event, widget);
+                            }
+                        }
+                    }
+                })
+                .add({
+                    combo: 'ctrl+v',
+                    description: translations['action.paste'],
+                    allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                    callback: function (event) {
+                        if (vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+                            if (itembuffer.hasWidget()) {
+                                event.preventDefault();
+                                pasteWidget(event);
+                            }
+                        }
+                    }
+                })
+                .add({
+                    combo: 'ctrl+x',
+                    description: translations['action.delete'],
+                    allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                    callback: function (event) {
+                        if (vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+                            var widget = vm.dashboardContainer.getSelectedWidget();
+                            if (widget) {
+                                event.preventDefault();
+                                removeWidget(event, widget);
+                            }
+                        }
+                    }
+                });
+        });
+    }
+
+    function prepareDashboardContextMenu() {
+        var dashboardContextActions = [];
+        if (vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+            dashboardContextActions.push(
+                {
+                    action: openDashboardSettings,
+                    enabled: true,
+                    value: "dashboard.settings",
+                    icon: "settings"
+                }
+            );
+            dashboardContextActions.push(
+                {
+                    action: openDeviceAliases,
+                    enabled: true,
+                    value: "device.aliases",
+                    icon: "devices_other"
+                }
+            );
+            dashboardContextActions.push(
+                {
+                    action: pasteWidget,
+                    enabled: itembuffer.hasWidget(),
+                    value: "action.paste",
+                    icon: "content_paste",
+                    shortcut: "M-V"
+                }
+            );
+        }
+        return dashboardContextActions;
+    }
+
+    function pasteWidget($event) {
+        var pos = vm.dashboardContainer.getEventGridPosition($event);
+        itembuffer.pasteWidget(vm.dashboard, pos);
+    }
+
+    function prepareWidgetContextMenu() {
+        var widgetContextActions = [];
+        if (vm.isEdit && !vm.isEditingWidget) {
+            widgetContextActions.push(
+                {
+                    action: editWidget,
+                    enabled: true,
+                    value: "action.edit",
+                    icon: "edit"
+                }
+            );
+            if (!vm.widgetEditMode) {
+                widgetContextActions.push(
+                    {
+                        action: copyWidget,
+                        enabled: true,
+                        value: "action.copy",
+                        icon: "content_copy",
+                        shortcut: "M-C"
+                    }
+                );
+                widgetContextActions.push(
+                    {
+                        action: removeWidget,
+                        enabled: true,
+                        value: "action.delete",
+                        icon: "clear",
+                        shortcut: "M-X"
+                    }
+                );
+            }
+        }
+        return widgetContextActions;
+    }
+
+    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);
+    }
+
     function helpLinkIdForWidgetType() {
         var link = 'widgetsConfig';
         if (vm.editingWidget && vm.editingWidget.type) {
@@ -322,6 +497,15 @@ export default function DashboardController(types, widgetService, userService,
         return link;
     }
 
+    function displayTitle() {
+        if (vm.dashboard && vm.dashboard.configuration.gridSettings &&
+            angular.isDefined(vm.dashboard.configuration.gridSettings.showTitle)) {
+            return vm.dashboard.configuration.gridSettings.showTitle;
+        } else {
+            return true;
+        }
+    }
+
     function onRevertWidgetEdit(widgetForm) {
         if (widgetForm.$dirty) {
             widgetForm.$setPristine();
@@ -331,7 +515,9 @@ export default function DashboardController(types, widgetService, userService,
 
     function saveWidget(widgetForm) {
         widgetForm.$setPristine();
-        vm.widgets[vm.editingWidgetIndex] = angular.copy(vm.editingWidget);
+        var widget = angular.copy(vm.editingWidget);
+        vm.widgets[vm.editingWidgetIndex] = widget;
+        vm.dashboardContainer.highlightWidget(widget, 0);
     }
 
     function onEditWidgetClosed() {
@@ -421,8 +607,8 @@ export default function DashboardController(types, widgetService, userService,
         });
     }
 
-    function toggleDashboardEditMode() {
-        vm.isEdit = !vm.isEdit;
+    function setEditMode(isEdit, revert) {
+        vm.isEdit = isEdit;
         if (vm.isEdit) {
             if (vm.widgetEditMode) {
                 vm.prevWidgets = angular.copy(vm.widgets);
@@ -433,14 +619,23 @@ export default function DashboardController(types, widgetService, userService,
             if (vm.widgetEditMode) {
                 vm.widgets = vm.prevWidgets;
             } else {
-                vm.dashboard = vm.prevDashboard;
-                vm.widgets = vm.dashboard.configuration.widgets;
+                if (vm.dashboardContainer) {
+                    vm.dashboardContainer.resetHighlight();
+                }
+                if (revert) {
+                    vm.dashboard = vm.prevDashboard;
+                    vm.widgets = vm.dashboard.configuration.widgets;
+                }
             }
         }
     }
 
+    function toggleDashboardEditMode() {
+        setEditMode(!vm.isEdit, true);
+    }
+
     function saveDashboard() {
-        vm.isEdit = false;
+        setEditMode(false, false);
         notifyDashboardUpdated();
     }
 
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index 0c815a6..ca46b97 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -51,7 +51,7 @@
         </md-button>
     </section>
     <section ng-if="!vm.widgetEditMode" class="tb-dashboard-title" layout="row" layout-align="center center">
-        <h3 ng-show="!vm.isEdit">{{ vm.dashboard.title }}</h3>
+        <h3 ng-show="!vm.isEdit && vm.displayTitle()">{{ vm.dashboard.title }}</h3>
         <md-input-container ng-show="vm.isEdit" class="md-block" style="height: 30px;">
             <label translate>dashboard.title</label>
             <input class="tb-dashboard-title" required name="title" ng-model="vm.dashboard.title">
@@ -64,7 +64,7 @@
         </md-button>
     </section>
     <div class="tb-absolute-fill"
-         ng-class="{ 'tb-padded' : !vm.widgetEditMode, 'tb-shrinked' : vm.isEditingWidget }">
+         ng-class="{ 'tb-padded' : !vm.widgetEditMode && (vm.isEdit || vm.displayTitle()), 'tb-shrinked' : vm.isEditingWidget }">
         <tb-dashboard
                 dashboard-style="{'background-color': vm.dashboard.configuration.gridSettings.backgroundColor,
                                   'background-image': 'url('+vm.dashboard.configuration.gridSettings.backgroundImageUrl+')',
@@ -82,7 +82,11 @@
                 is-edit-action-enabled="vm.isEdit || vm.widgetEditMode"
                 is-remove-action-enabled="vm.isEdit && !vm.widgetEditMode"
                 on-edit-widget="vm.editWidget(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)"
+                prepare-dashboard-context-menu="vm.prepareDashboardContextMenu()"
+                prepare-widget-context-menu="vm.prepareWidgetContextMenu(widget)"
                 on-remove-widget="vm.removeWidget(event, widget)"
                 load-widgets="vm.loadDashboard()"
                 on-init="vm.dashboardInited(dashboard)"
diff --git a/ui/src/app/dashboard/dashboard-settings.controller.js b/ui/src/app/dashboard/dashboard-settings.controller.js
index d15359e..3a46566 100644
--- a/ui/src/app/dashboard/dashboard-settings.controller.js
+++ b/ui/src/app/dashboard/dashboard-settings.controller.js
@@ -28,6 +28,10 @@ export default function DashboardSettingsController($scope, $mdDialog, gridSetti
 
     vm.gridSettings = gridSettings || {};
 
+    if (angular.isUndefined(vm.gridSettings.showTitle)) {
+        vm.gridSettings.showTitle = true;
+    }
+
     vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)';
     vm.gridSettings.columns = vm.gridSettings.columns || 24;
     vm.gridSettings.margins = vm.gridSettings.margins || [10, 10];
diff --git a/ui/src/app/dashboard/dashboard-settings.tpl.html b/ui/src/app/dashboard/dashboard-settings.tpl.html
index f69eb02..e28798d 100644
--- a/ui/src/app/dashboard/dashboard-settings.tpl.html
+++ b/ui/src/app/dashboard/dashboard-settings.tpl.html
@@ -31,6 +31,11 @@
         <md-dialog-content>
             <div class="md-dialog-content">
                 <fieldset ng-disabled="loading">
+                    <div layout="row" layout-padding>
+                        <md-checkbox flex aria-label="{{ 'dashboard.display-title' | translate }}"
+                                     ng-model="vm.gridSettings.showTitle">{{ 'dashboard.display-title' | translate }}
+                        </md-checkbox>
+                    </div>
                     <md-input-container class="md-block">
                         <label translate>dashboard.columns-count</label>
                         <input required type="number" step="any" name="columns" ng-model="vm.gridSettings.columns" min="10"
diff --git a/ui/src/app/dashboard/index.js b/ui/src/app/dashboard/index.js
index 7400463..461f56e 100644
--- a/ui/src/app/dashboard/index.js
+++ b/ui/src/app/dashboard/index.js
@@ -29,6 +29,7 @@ import thingsboardDashboard from '../components/dashboard.directive';
 import thingsboardExpandFullscreen from '../components/expand-fullscreen.directive';
 import thingsboardWidgetsBundleSelect from '../components/widgets-bundle-select.directive';
 import thingsboardTypes from '../common/types.constant';
+import thingsboardItemBuffer from '../services/item-buffer.service';
 
 import DashboardRoutes from './dashboard.routes';
 import DashboardsController from './dashboards.controller';
@@ -45,6 +46,7 @@ export default angular.module('thingsboard.dashboard', [
     uiRouter,
     gridster.name,
     thingsboardTypes,
+    thingsboardItemBuffer,
     thingsboardGrid,
     thingsboardApiWidget,
     thingsboardApiUser,
diff --git a/ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js b/ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js
index 9149cf1..3f6d769 100644
--- a/ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js
+++ b/ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 /*@ngInject*/
-export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, dashboardService, deviceId, deviceName, widget) {
+export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, itembuffer, dashboardService, deviceId, deviceName, widget) {
 
     var vm = this;
 
@@ -34,62 +34,20 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog, 
     function add() {
         $scope.theForm.$setPristine();
         var theDashboard;
-        var deviceAliases;
-        widget.col = 0;
-        widget.sizeX /= 2;
-        widget.sizeY /= 2;
         if (vm.addToDashboardType === 0) {
             theDashboard = vm.dashboard;
-            if (!theDashboard.configuration) {
-                theDashboard.configuration = {};
-            }
-            deviceAliases = theDashboard.configuration.deviceAliases;
-            if (!deviceAliases) {
-                deviceAliases = {};
-                theDashboard.configuration.deviceAliases = deviceAliases;
-            }
-            var newAliasId;
-            for (var aliasId in deviceAliases) {
-                if (deviceAliases[aliasId].deviceId === deviceId) {
-                    newAliasId = aliasId;
-                    break;
-                }
-            }
-            if (!newAliasId) {
-                var newAliasName = createDeviceAliasName(deviceAliases, deviceName);
-                newAliasId = 0;
-                for (aliasId in deviceAliases) {
-                    newAliasId = Math.max(newAliasId, aliasId);
-                }
-                newAliasId++;
-                deviceAliases[newAliasId] = {alias: newAliasName, deviceId: deviceId};
-            }
-            widget.config.datasources[0].deviceAliasId = newAliasId;
-
-            if (!theDashboard.configuration.widgets) {
-                theDashboard.configuration.widgets = [];
-            }
-
-            var row = 0;
-            for (var w in theDashboard.configuration.widgets) {
-                var existingWidget = theDashboard.configuration.widgets[w];
-                var wRow = existingWidget.row ? existingWidget.row : 0;
-                var wSizeY = existingWidget.sizeY ? existingWidget.sizeY : 1;
-                var bottom = wRow + wSizeY;
-                row = Math.max(row, bottom);
-            }
-            widget.row = row;
-            theDashboard.configuration.widgets.push(widget);
         } else {
             theDashboard = vm.newDashboard;
-            deviceAliases = {};
-            deviceAliases['1'] = {alias: deviceName, deviceId: deviceId};
-            theDashboard.configuration = {};
-            theDashboard.configuration.widgets = [];
-            widget.row = 0;
-            theDashboard.configuration.widgets.push(widget);
-            theDashboard.configuration.deviceAliases = deviceAliases;
         }
+        var aliasesInfo = {
+            datasourceAliases: {},
+            targetDeviceAliases: {}
+        };
+        aliasesInfo.datasourceAliases[0] = {
+            aliasName: deviceName,
+            deviceId: deviceId
+        };
+        theDashboard = itembuffer.addWidgetToDashboard(theDashboard, widget, aliasesInfo, 48, -1, -1);
         dashboardService.saveDashboard(theDashboard).then(
             function success(dashboard) {
                 $mdDialog.hide();
@@ -98,25 +56,6 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog, 
                 }
             }
         );
-
-    }
-
-    function createDeviceAliasName(deviceAliases, alias) {
-        var c = 0;
-        var newAlias = angular.copy(alias);
-        var unique = false;
-        while (!unique) {
-            unique = true;
-            for (var devAliasId in deviceAliases) {
-                var devAlias = deviceAliases[devAliasId];
-                if (newAlias === devAlias.alias) {
-                    c++;
-                    newAlias = alias + c;
-                    unique = false;
-                }
-            }
-        }
-        return newAlias;
     }
 
 }
diff --git a/ui/src/app/device/attribute/attribute-table.directive.js b/ui/src/app/device/attribute/attribute-table.directive.js
index 0a5e0cd..24e9022 100644
--- a/ui/src/app/device/attribute/attribute-table.directive.js
+++ b/ui/src/app/device/attribute/attribute-table.directive.js
@@ -239,6 +239,8 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
                 index: 0
             }
             scope.widgetsBundle = null;
+            scope.firstBundle = true;
+            scope.selectedWidgetsBundleAlias = types.systemBundleAlias.cards;
 
             scope.deviceAliases = {};
             scope.deviceAliases['1'] = {alias: scope.deviceName, deviceId: scope.deviceId};
@@ -326,13 +328,6 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
                     }
                 }
             });
-
-            widgetService.getWidgetsBundleByAlias(types.systemBundleAlias.cards).then(
-                function success(widgetsBundle) {
-                    scope.firstBundle = true;
-                    scope.widgetsBundle = widgetsBundle;
-                }
-            );
         }
 
         scope.exitWidgetMode = function() {
@@ -344,6 +339,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
                 scope.widgetsIndexWatch();
                 scope.widgetsIndexWatch = null;
             }
+            scope.selectedWidgetsBundleAlias = null;
             scope.mode = 'default';
             scope.getDeviceAttributes(true);
         }
diff --git a/ui/src/app/device/attribute/attribute-table.tpl.html b/ui/src/app/device/attribute/attribute-table.tpl.html
index 880d880..a88fe02 100644
--- a/ui/src/app/device/attribute/attribute-table.tpl.html
+++ b/ui/src/app/device/attribute/attribute-table.tpl.html
@@ -105,7 +105,8 @@
                     <tb-widgets-bundle-select flex-offset="5"
                                               flex
                                               ng-model="widgetsBundle"
-                                              select-first-bundle="false">
+                                              select-first-bundle="false"
+                                              select-bundle-alias="selectedWidgetsBundleAlias">
                     </tb-widgets-bundle-select>
                 </div>
                 <md-button ng-show="widgetsList.length > 0" class="md-accent md-hue-2 md-raised" ng-click="addWidgetToDashboard($event)">
diff --git a/ui/src/app/services/item-buffer.service.js b/ui/src/app/services/item-buffer.service.js
new file mode 100644
index 0000000..56d8d6f
--- /dev/null
+++ b/ui/src/app/services/item-buffer.service.js
@@ -0,0 +1,191 @@
+/*
+ * 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 angularStorage from 'angular-storage';
+
+export default angular.module('thingsboard.itembuffer', [angularStorage])
+    .factory('itembuffer', ItemBuffer)
+    .factory('bufferStore', function(store) {
+        var newStore = store.getNamespacedStore('tbBufferStore', null, null, false);
+        return newStore;
+    })
+    .name;
+
+/*@ngInject*/
+function ItemBuffer(bufferStore) {
+
+    const WIDGET_ITEM = "widget_item";
+
+    var service = {
+        copyWidget: copyWidget,
+        hasWidget: hasWidget,
+        pasteWidget: pasteWidget,
+        addWidgetToDashboard: addWidgetToDashboard
+    }
+
+    return service;
+
+    /**
+     aliasesInfo {
+        datasourceAliases: {
+            datasourceIndex: {
+                aliasName: "...",
+                deviceId: "..."
+            }
+        }
+        targetDeviceAliases: {
+            targetDeviceAliasIndex: {
+                aliasName: "...",
+                deviceId: "..."
+            }
+        }
+        ....
+     }
+    **/
+
+    function copyWidget(widget, aliasesInfo, originalColumns) {
+        var widgetItem = {
+            widget: widget,
+            aliasesInfo: aliasesInfo,
+            originalColumns: originalColumns
+        }
+        bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem));
+    }
+
+    function hasWidget() {
+        return bufferStore.get(WIDGET_ITEM);
+    }
+
+    function pasteWidget(targetDasgboard, position) {
+        var widgetItemJson = bufferStore.get(WIDGET_ITEM);
+        if (widgetItemJson) {
+            var widgetItem = angular.fromJson(widgetItemJson);
+            var widget = widgetItem.widget;
+            var aliasesInfo = widgetItem.aliasesInfo;
+            var originalColumns = widgetItem.originalColumns;
+            var targetRow = -1;
+            var targetColumn = -1;
+            if (position) {
+                targetRow = position.row;
+                targetColumn = position.column;
+            }
+            addWidgetToDashboard(targetDasgboard, widget, aliasesInfo, originalColumns, targetRow, targetColumn);
+        }
+    }
+
+    function addWidgetToDashboard(dashboard, widget, aliasesInfo, originalColumns, row, column) {
+        var theDashboard;
+        if (dashboard) {
+            theDashboard = dashboard;
+        } else {
+            theDashboard = {};
+        }
+        if (!theDashboard.configuration) {
+            theDashboard.configuration = {};
+        }
+        if (!theDashboard.configuration.deviceAliases) {
+            theDashboard.configuration.deviceAliases = {};
+        }
+        updateAliases(theDashboard, widget, aliasesInfo);
+
+        if (!theDashboard.configuration.widgets) {
+            theDashboard.configuration.widgets = [];
+        }
+        var targetColumns = 24;
+        if (theDashboard.configuration.gridSettings &&
+            theDashboard.configuration.gridSettings.columns) {
+            targetColumns = theDashboard.configuration.gridSettings.columns;
+        }
+        if (targetColumns != originalColumns) {
+            var ratio = targetColumns / originalColumns;
+            widget.sizeX *= ratio;
+            widget.sizeY *= ratio;
+        }
+        if (row > -1 && column > - 1) {
+            widget.row = row;
+            widget.col = column;
+        } else {
+            row = 0;
+            for (var w in theDashboard.configuration.widgets) {
+                var existingWidget = theDashboard.configuration.widgets[w];
+                var wRow = existingWidget.row ? existingWidget.row : 0;
+                var wSizeY = existingWidget.sizeY ? existingWidget.sizeY : 1;
+                var bottom = wRow + wSizeY;
+                row = Math.max(row, bottom);
+            }
+            widget.row = row;
+            widget.col = 0;
+        }
+        theDashboard.configuration.widgets.push(widget);
+        return theDashboard;
+    }
+
+    function updateAliases(dashboard, widget, aliasesInfo) {
+        var deviceAliases = dashboard.configuration.deviceAliases;
+        var aliasInfo;
+        var newAliasId;
+        for (var datasourceIndex in aliasesInfo.datasourceAliases) {
+            aliasInfo = aliasesInfo.datasourceAliases[datasourceIndex];
+            newAliasId = getDeviceAliasId(deviceAliases, aliasInfo);
+            widget.config.datasources[datasourceIndex].deviceAliasId = newAliasId;
+        }
+        for (var targetDeviceAliasIndex in aliasesInfo.targetDeviceAliases) {
+            aliasInfo = aliasesInfo.targetDeviceAliases[targetDeviceAliasIndex];
+            newAliasId = getDeviceAliasId(deviceAliases, aliasInfo);
+            widget.config.targetDeviceAliasIds[targetDeviceAliasIndex] = newAliasId;
+        }
+    }
+
+    function getDeviceAliasId(deviceAliases, aliasInfo) {
+        var newAliasId;
+        for (var aliasId in deviceAliases) {
+            if (deviceAliases[aliasId].deviceId === aliasInfo.deviceId) {
+                newAliasId = aliasId;
+                break;
+            }
+        }
+        if (!newAliasId) {
+            var newAliasName = createDeviceAliasName(deviceAliases, aliasInfo.aliasName);
+            newAliasId = 0;
+            for (aliasId in deviceAliases) {
+                newAliasId = Math.max(newAliasId, aliasId);
+            }
+            newAliasId++;
+            deviceAliases[newAliasId] = {alias: newAliasName, deviceId: aliasInfo.deviceId};
+        }
+        return newAliasId;
+    }
+
+    function createDeviceAliasName(deviceAliases, alias) {
+        var c = 0;
+        var newAlias = angular.copy(alias);
+        var unique = false;
+        while (!unique) {
+            unique = true;
+            for (var devAliasId in deviceAliases) {
+                var devAlias = deviceAliases[devAliasId];
+                if (newAlias === devAlias.alias) {
+                    c++;
+                    newAlias = alias + c;
+                    unique = false;
+                }
+            }
+        }
+        return newAlias;
+    }
+
+
+}
\ No newline at end of file
diff --git a/ui/src/locale/en_US.json b/ui/src/locale/en_US.json
index 9da68d6..5de8938 100644
--- a/ui/src/locale/en_US.json
+++ b/ui/src/locale/en_US.json
@@ -38,7 +38,9 @@
     "create": "Create",
     "drag": "Drag",
     "refresh": "Refresh",
-    "undo": "Undo"
+    "undo": "Undo",
+    "copy": "Copy",
+    "paste": "Paste"
   },
   "admin": {
     "general": "General",
@@ -211,7 +213,8 @@
     "vertical-margin": "Vertical margin",
     "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."
+    "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.",
+    "display-title": "Display dashboard title"
   },
   "datakey": {
     "settings": "Settings",
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index e8f8283..b9b869e 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -169,6 +169,9 @@ md-menu-item {
 md-menu-item {
   .md-button {
     display: block;
+    .tb-alt-text {
+      float: right;
+    }
   }
 }