From 29c80b3b85e50f63d1bfb0092f71bc5535f1baef Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 26 Dec 2016 17:07:47 +0200 Subject: [PATCH] Add widgets copy/past ability --- ui/src/app/app.js | 2 + ui/src/app/components/dashboard.directive.js | 108 +++++++-- ui/src/app/components/dashboard.tpl.html | 144 +++++++----- .../components/keyboard-shortcut.filter.js | 40 ++++ .../components/mousepoint-menu.directive.js | 51 +++++ ui/src/app/components/tb-event-directives.js | 2 +- .../widgets-bundle-select.directive.js | 23 +- .../dashboard-settings.controller.js | 4 + .../app/dashboard/dashboard-settings.tpl.html | 5 + ui/src/app/dashboard/dashboard.controller.js | 211 +++++++++++++++++- ui/src/app/dashboard/dashboard.tpl.html | 8 +- ui/src/app/dashboard/index.js | 2 + ...d-widget-to-dashboard-dialog.controller.js | 81 +------ .../attribute/attribute-table.directive.js | 10 +- .../device/attribute/attribute-table.tpl.html | 3 +- ui/src/app/services/item-buffer.service.js | 191 ++++++++++++++++ ui/src/locale/en_US.json | 7 +- ui/src/scss/main.scss | 3 + 18 files changed, 728 insertions(+), 167 deletions(-) create mode 100644 ui/src/app/components/keyboard-shortcut.filter.js create mode 100644 ui/src/app/components/mousepoint-menu.directive.js create mode 100644 ui/src/app/services/item-buffer.service.js diff --git a/ui/src/app/app.js b/ui/src/app/app.js index 3aa6cead02..982f15e84f 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 f90b0552f8..7c5094a6ff 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 f0cecefec1..c43caac54f 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"> - -
-
-
    - -
  • -
    -
    - {{widget.config.title}} - -
    -
    - - - - {{ 'widget.edit' | translate }} - - - edit - - - - - {{ 'widget.remove' | translate }} - - - close - - -
    -
    -
    + + +
    +
    +
      + +
    • + +
      +
      + {{widget.config.title}} + +
      +
      + + + + {{ 'widget.edit' | translate }} + + + edit + + + + + {{ 'widget.remove' | translate }} + + + close + + +
      +
      +
      +
      +
      -
    -
    -
  • -
+ + + + {{item.icon}} + {{item.value}} + {{ item.shortcut | keyboardShortcut }} + + + + + + +
- -
\ No newline at end of file + + + + + {{item.icon}} + {{item.value}} + {{ item.shortcut | keyboardShortcut }} + + + + \ 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 0000000000..289afe291e --- /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 0000000000..b3f24c782d --- /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 1fefd51aae..f7e7fe9fc6 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 fcf5defecf..1525523626 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-settings.controller.js b/ui/src/app/dashboard/dashboard-settings.controller.js index d15359e31a..3a46566c45 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 f69eb02a51..e28798dfb0 100644 --- a/ui/src/app/dashboard/dashboard-settings.tpl.html +++ b/ui/src/app/dashboard/dashboard-settings.tpl.html @@ -31,6 +31,11 @@
+
+ {{ 'dashboard.display-title' | translate }} + +
-

{{ vm.dashboard.title }}

+

{{ vm.dashboard.title }}

@@ -64,7 +64,7 @@
+ ng-class="{ 'tb-padded' : !vm.widgetEditMode && (vm.isEdit || vm.displayTitle()), 'tb-shrinked' : vm.isEditingWidget }"> + select-first-bundle="false" + select-bundle-alias="selectedWidgetsBundleAlias">
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 0000000000..56d8d6f571 --- /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 9da68d6182..5de8938cd6 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 e8f8283ffe..b9b869ef0a 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; + } } }