diff --git a/client/app/components/dashboards/gridstack/index.js b/client/app/components/dashboards/gridstack/index.js index cd1c16c85b..89c960aef5 100644 --- a/client/app/components/dashboards/gridstack/index.js +++ b/client/app/components/dashboards/gridstack/index.js @@ -69,6 +69,7 @@ function gridstack($parse, dashboardGridOptions) { scope: { editing: '=', batchUpdate: '=', // set by directive - for using in wrapper components + onLayoutChanged: '=', isOneColumnMode: '=', }, controller() { @@ -121,67 +122,6 @@ function gridstack($parse, dashboardGridOptions) { }); }; - this.batchUpdateWidgets = (items) => { - // This method is used to update multiple widgets with a single - // reflow (for example, restore positions when dashboard editing cancelled). - // "dirty" part of code: updating grid and DOM nodes directly. - // layout reflow is triggered by `batchUpdate`/`commit` calls - this.update((grid) => { - _.each(grid.grid.nodes, (node) => { - const item = items[node.id]; - if (item) { - if (_.isNumber(item.col)) { - node.x = parseFloat(item.col); - node.el.attr('data-gs-x', node.x); - node._dirty = true; - } - - if (_.isNumber(item.row)) { - node.y = parseFloat(item.row); - node.el.attr('data-gs-y', node.y); - node._dirty = true; - } - - if (_.isNumber(item.sizeX)) { - node.width = parseFloat(item.sizeX); - node.el.attr('data-gs-width', node.width); - node._dirty = true; - } - - if (_.isNumber(item.sizeY)) { - node.height = parseFloat(item.sizeY); - node.el.attr('data-gs-height', node.height); - node._dirty = true; - } - - if (_.isNumber(item.minSizeX)) { - node.minWidth = parseFloat(item.minSizeX); - node.el.attr('data-gs-min-width', node.minWidth); - node._dirty = true; - } - - if (_.isNumber(item.maxSizeX)) { - node.maxWidth = parseFloat(item.maxSizeX); - node.el.attr('data-gs-max-width', node.maxWidth); - node._dirty = true; - } - - if (_.isNumber(item.minSizeY)) { - node.minHeight = parseFloat(item.minSizeY); - node.el.attr('data-gs-min-height', node.minHeight); - node._dirty = true; - } - - if (_.isNumber(item.maxSizeY)) { - node.maxHeight = parseFloat(item.maxSizeY); - node.el.attr('data-gs-max-height', node.maxHeight); - node._dirty = true; - } - } - }); - }); - }; - this.removeWidget = ($element) => { const grid = this.grid(); if (grid) { @@ -300,6 +240,7 @@ function gridstack($parse, dashboardGridOptions) { $(node.el).trigger('gridstack.changed', node); } }); + $scope.onLayoutChanged(); changedNodes = {}; }); diff --git a/client/app/pages/dashboards/dashboard.html b/client/app/pages/dashboards/dashboard.html index 9246b02c5e..3bc39e448d 100644 --- a/client/app/pages/dashboards/dashboard.html +++ b/client/app/pages/dashboards/dashboard.html @@ -15,17 +15,15 @@

-
- +
+ + Saved + -
@@ -91,8 +89,8 @@

-
{ + const saveDashboardLayout = () => { if (!this.dashboard.canEdit()) { return; } + // calc diff, bail if none + const changedWidgets = getWidgetsWithChangedPositions(this.dashboard.widgets); + if (!changedWidgets.length) { + this.isLayoutDirty = false; + $scope.$applyAsync(); + return; + } + this.saveInProgress = true; - const showMessages = true; return $q - .all(_.map(widgets, widget => widget.save())) + .all(_.map(changedWidgets, widget => widget.save())) .then(() => { - if (showMessages) { - notification.success('Changes saved.'); - } - // Update original widgets positions - _.each(widgets, (widget) => { - _.extend(widget.$originalPosition, widget.options.position); - }); + this.isLayoutDirty = false; }) .catch(() => { - if (showMessages) { - notification.error('Error saving changes.'); - } + notification.error('Error saving changes.'); }) .finally(() => { this.saveInProgress = false; }); }; + const saveDashboardLayoutDebounced = _.debounce(saveDashboardLayout, 1000); + this.layoutEditing = false; this.isFullscreen = false; this.refreshRate = null; @@ -84,6 +85,7 @@ function DashboardCtrl( this.showPermissionsControl = clientConfig.showPermissionsControl; this.globalParameters = []; this.isDashboardOwner = false; + this.isLayoutDirty = false; this.refreshRates = clientConfig.dashboardRefreshIntervals.map(interval => ({ name: durationHumanize(interval), @@ -242,28 +244,17 @@ function DashboardCtrl( }); }; - this.editLayout = (enableEditing, applyChanges) => { - if (!this.isGridDisabled) { - if (!enableEditing) { - if (applyChanges) { - const changedWidgets = getWidgetsWithChangedPositions(this.dashboard.widgets); - saveDashboardLayout(changedWidgets); - } else { - // Revert changes - const items = {}; - _.each(this.dashboard.widgets, (widget) => { - _.extend(widget.options.position, widget.$originalPosition); - items[widget.id] = widget.options.position; - }); - this.dashboard.widgets = Dashboard.prepareWidgetsForDashboard(this.dashboard.widgets); - if (this.updateGridItems) { - this.updateGridItems(items); - } - } - } - - this.layoutEditing = enableEditing; + this.onLayoutChanged = () => { + // prevent unnecessary save when gridstack is loaded + if (!this.layoutEditing) { + return; } + this.isLayoutDirty = true; + saveDashboardLayoutDebounced(); + }; + + this.editLayout = (enableEditing) => { + this.layoutEditing = enableEditing; }; this.loadTags = () => getTags('api/dashboards/tags').then(tags => _.map(tags, t => t.name)); @@ -405,12 +396,11 @@ function DashboardCtrl( this.removeWidget = (widgetId) => { this.dashboard.widgets = this.dashboard.widgets.filter(w => w.id !== undefined && w.id !== widgetId); this.extractGlobalParameters(); + $scope.$applyAsync(); + if (!this.layoutEditing) { // We need to wait a bit while `angular` updates widgets, and only then save new layout - $timeout(() => { - const changedWidgets = getWidgetsWithChangedPositions(this.dashboard.widgets); - saveDashboardLayout(changedWidgets); - }, 50); + $timeout(saveDashboardLayout, 50); } }; diff --git a/client/app/pages/dashboards/dashboard.less b/client/app/pages/dashboards/dashboard.less index bb685b4fbf..b846e4efab 100644 --- a/client/app/pages/dashboards/dashboard.less +++ b/client/app/pages/dashboards/dashboard.less @@ -112,6 +112,23 @@ } } +.dashboard__control { + .save-status { + vertical-align: middle; + margin-right: 7px; + font-size: 12px; + position: relative; + + &[data-dirty="true"] { + opacity: 0.6; + + &::after { + content: "*"; + } + } + } +} + // Mobile fixes @media (max-width: 767px) { diff --git a/client/app/services/widget.js b/client/app/services/widget.js index 9bd4221e96..36540500c1 100644 --- a/client/app/services/widget.js +++ b/client/app/services/widget.js @@ -92,6 +92,10 @@ function WidgetFactory($http, $location, Query, Visualization, dashboardGridOpti this.options.position.autoHeight = true; } + this.updateOriginalPosition(); + } + + updateOriginalPosition() { // Save original position (create a shallow copy) this.$originalPosition = extend({}, this.options.position); } @@ -161,6 +165,8 @@ function WidgetFactory($http, $location, Query, Visualization, dashboardGridOpti this[k] = v; }); + this.updateOriginalPosition(); + return this; }); }