From 04d25dc58a2c2570e644c56c3a76d8d8010adc5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 2 Mar 2015 22:24:01 +0100 Subject: [PATCH] Dashboard: When saving a dashboard and another user has made changes inbetween, the user is promted with a warning if he really wants to overwrite the other's changes, Closes #718 --- CHANGELOG.md | 1 + pkg/api/dashboard.go | 8 +- pkg/models/dashboards.go | 8 +- pkg/services/sqlstore/dashboard.go | 21 ++- .../features/dashboard/cloneDashboardCtrl.js | 9 +- .../features/dashboard/dashboardNavCtrl.js | 35 ++++- src/app/features/dashboard/dashboardSrv.js | 10 +- .../features/dashboard/unsavedChangesSrv.js | 2 + src/app/partials/confirm_modal.html | 2 +- src/app/routes/all.js | 116 ++++++++++++++++ src/app/routes/dashLoadControllers.js | 126 ++++++++++++++++++ src/app/services/alertSrv.js | 1 + src/app/services/backendSrv.js | 5 +- src/test/specs/dashboardSrv-specs.js | 2 +- 14 files changed, 324 insertions(+), 22 deletions(-) create mode 100644 src/app/routes/all.js create mode 100644 src/app/routes/dashLoadControllers.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 514a457e5db..db4e180ece1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 2.0.0 (unreleased) **New features** +- [Issue #718](https://github.com/grafana/grafana/issues/718). Dashboard: When saving a dashboard and another user has made changes inbetween the user is promted with a warning if he really wants to overwrite the other's changes - [Issue #1331](https://github.com/grafana/grafana/issues/1331). Graph & Singlestat: New axis/unit format selector and more units (kbytes, Joule, Watt, eV), and new design for graph axis & grid tab and single stat options tab views - [Issue #1241](https://github.com/grafana/grafana/issues/1242). Timepicker: New option in timepicker (under dashboard settings), to change ``now`` to be for example ``now-1m``, usefull when you want to ignore last minute because it contains incomplete data - [Issue #171](https://github.com/grafana/grafana/issues/171). Panel: Different time periods, panels can override dashboard relative time and/or add a time shift diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 925a790305e..8cde5a8bc8a 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -77,14 +77,18 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) { err := bus.Dispatch(&cmd) if err != nil { if err == m.ErrDashboardWithSameNameExists { - c.JsonApiErr(400, "Dashboard with the same title already exists", nil) + c.JSON(412, util.DynMap{"status": "name-exists", "message": err.Error()}) + return + } + if err == m.ErrDashboardVersionMismatch { + c.JSON(412, util.DynMap{"status": "version-mismatch", "message": err.Error()}) return } c.JsonApiErr(500, "Failed to save dashboard", err) return } - c.JSON(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug}) + c.JSON(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version}) } func GetHomeDashboard(c *middleware.Context) { diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index a5fd415ac0b..4043063278e 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -11,6 +11,7 @@ import ( var ( ErrDashboardNotFound = errors.New("Account not found") ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists") + ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else") ) type Dashboard struct { @@ -58,6 +59,10 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { if dash.Data["id"] != nil { dash.Id = int64(dash.Data["id"].(float64)) + + if dash.Data["version"] != nil { + dash.Version = int(dash.Data["version"].(float64)) + } } return dash @@ -79,7 +84,8 @@ func (dash *Dashboard) UpdateSlug() { // type SaveDashboardCommand struct { - Dashboard map[string]interface{} `json:"dashboard"` + Dashboard map[string]interface{} `json:"dashboard" binding:"Required"` + Overwrite bool `json:"overwrite"` OrgId int64 `json:"-"` Result *Dashboard diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 243def6f4c2..bf748b600f4 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -28,13 +28,30 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { return err } - if hasExisting && dash.Id != existing.Id { - return m.ErrDashboardWithSameNameExists + if hasExisting { + // another dashboard with same name + if dash.Id != existing.Id { + if cmd.Overwrite { + dash.Id = existing.Id + } else { + return m.ErrDashboardWithSameNameExists + } + } + // check for is someone else has written in between + if dash.Version != existing.Version { + if cmd.Overwrite { + dash.Version = existing.Version + } else { + return m.ErrDashboardVersionMismatch + } + } } if dash.Id == 0 { _, err = sess.Insert(dash) } else { + dash.Version += 1 + dash.Data["version"] = dash.Version _, err = sess.Id(dash.Id).Update(dash) } diff --git a/src/app/features/dashboard/cloneDashboardCtrl.js b/src/app/features/dashboard/cloneDashboardCtrl.js index 855129ecaf9..8b41fc786a3 100644 --- a/src/app/features/dashboard/cloneDashboardCtrl.js +++ b/src/app/features/dashboard/cloneDashboardCtrl.js @@ -6,26 +6,21 @@ function (angular) { var module = angular.module('grafana.controllers'); - module.controller('CloneDashboardCtrl', function($scope, datasourceSrv, $location) { + module.controller('CloneDashboardCtrl', function($scope, backendSrv, $location) { $scope.init = function() { - $scope.db = datasourceSrv.getGrafanaDB(); $scope.clone.id = null; $scope.clone.editable = true; $scope.clone.title = $scope.clone.title + " Copy"; }; $scope.saveClone = function() { - $scope.db.saveDashboard($scope.clone) + backendSrv.saveDashboard($scope.clone) .then(function(result) { - $scope.appEvent('alert-success', ['Dashboard saved', 'Saved as ' + result.title]); $location.url(result.url); $scope.appEvent('dashboard-saved', $scope.clone); $scope.dismiss(); - - }, function(err) { - $scope.appEvent('alert-error', ['Save failed', err]); }); }; }); diff --git a/src/app/features/dashboard/dashboardNavCtrl.js b/src/app/features/dashboard/dashboardNavCtrl.js index a577749945b..7066a982c99 100644 --- a/src/app/features/dashboard/dashboardNavCtrl.js +++ b/src/app/features/dashboard/dashboardNavCtrl.js @@ -55,10 +55,11 @@ function (angular, _, moment) { $scope.appEvent('hide-dash-editor'); }; - $scope.saveDashboard = function() { + $scope.saveDashboard = function(options) { var clone = angular.copy($scope.dashboard); - backendSrv.saveDashboard(clone).then(function(data) { + backendSrv.saveDashboard(clone, options).then(function(data) { + $scope.dashboard.version = data.version; $scope.appEvent('dashboard-saved', $scope.dashboard); var dashboardUrl = '/dashboard/db/' + data.slug; @@ -68,7 +69,35 @@ function (angular, _, moment) { } $scope.appEvent('alert-success', ['Dashboard saved', 'Saved as ' + clone.title]); - }); + }, $scope.handleSaveDashError); + }; + + $scope.handleSaveDashError = function(err) { + if (err.data && err.data.status === "version-mismatch" ) { + err.isHandled = true; + + $scope.appEvent('confirm-modal', { + title: 'Someone else has updated this dashboard!', + text: "Do you STILL want to save?", + icon: "fa-warning", + onConfirm: function() { + $scope.saveDashboard({overwrite: true}); + } + }); + } + + if (err.data && err.data.status === "name-exists" ) { + err.isHandled = true; + + $scope.appEvent('confirm-modal', { + title: 'Another dashboard with the same name exists', + text: "Do you STILL want to save and ovewrite it?", + icon: "fa-warning", + onConfirm: function() { + $scope.saveDashboard({overwrite: true}); + } + }); + } }; $scope.deleteDashboard = function() { diff --git a/src/app/features/dashboard/dashboardSrv.js b/src/app/features/dashboard/dashboardSrv.js index ef33c211d04..90ec885ed41 100644 --- a/src/app/features/dashboard/dashboardSrv.js +++ b/src/app/features/dashboard/dashboardSrv.js @@ -18,6 +18,10 @@ function (angular, $, kbn, _, moment) { data = {}; } + if (!data.id && data.version) { + data.schemaVersion = data.version; + } + this.id = data.id || null; this.title = data.title || 'No Title'; this.originalTitle = this.title; @@ -33,8 +37,8 @@ function (angular, $, kbn, _, moment) { this.templating = this._ensureListExist(data.templating); this.annotations = this._ensureListExist(data.annotations); this.refresh = data.refresh; + this.schemaVersion = data.schemaVersion || 0; this.version = data.version || 0; - this.hideAllLegends = data.hideAllLegends || false; if (this.nav.length === 0) { this.nav.push({ type: 'timepicker' }); @@ -134,9 +138,9 @@ function (angular, $, kbn, _, moment) { p._updateSchema = function(old) { var i, j, k; - var oldVersion = this.version; + var oldVersion = this.schemaVersion; var panelUpgrades = []; - this.version = 6; + this.schemaVersion = 6; if (oldVersion === 6) { return; diff --git a/src/app/features/dashboard/unsavedChangesSrv.js b/src/app/features/dashboard/unsavedChangesSrv.js index a2e419720f4..0e8f6b34e0d 100644 --- a/src/app/features/dashboard/unsavedChangesSrv.js +++ b/src/app/features/dashboard/unsavedChangesSrv.js @@ -83,6 +83,8 @@ function(angular, _, config) { // ignore timespan changes current.time = original.time = {}; current.refresh = original.refresh; + // ignore version + current.version = original.version; // ignore template variable values _.each(current.templating.list, function(value, index) { diff --git a/src/app/partials/confirm_modal.html b/src/app/partials/confirm_modal.html index 03398c2811e..9e92a4af8bb 100644 --- a/src/app/partials/confirm_modal.html +++ b/src/app/partials/confirm_modal.html @@ -1,7 +1,7 @@