diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d96f4dfbdf..b103d4fcf8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - Increased resolution for graphite datapoints (maxDataPoints), now equal to panel pixel width. (Closes #5) - Improvement to influxdb query editor, can now add where clause and alias (Issue #331, thanks @mavimo) - New config setting for graphite datasource to control if json render request is POST or GET (Issue #345) +- Unsaved changes warning feature (Issue #324) + # 1.5.3 (2014-04-17) - Add support for async scripted dashboards (Issue #274) diff --git a/src/app/components/settings.js b/src/app/components/settings.js index b7785d5ce4b..734dd34e947 100644 --- a/src/app/components/settings.js +++ b/src/app/components/settings.js @@ -25,6 +25,7 @@ function (_, crypto) { grafana_index : 'grafana-dash', elasticsearch_all_disabled : false, timezoneOffset : null, + unsaved_changes_warning : true }; // This initializes a new hash on purpose, to avoid adding parameters to diff --git a/src/app/controllers/dash.js b/src/app/controllers/dash.js index b9556a8e4da..98f5df2983e 100644 --- a/src/app/controllers/dash.js +++ b/src/app/controllers/dash.js @@ -30,7 +30,8 @@ function (angular, $, config, _) { var module = angular.module('kibana.controllers'); module.controller('DashCtrl', function( - $scope, $rootScope, $route, ejsResource, dashboard, alertSrv, panelMove, keyboardManager, grafanaVersion) { + $scope, $rootScope, ejsResource, dashboard, + alertSrv, panelMove, keyboardManager, grafanaVersion) { $scope.requiredElasticSearchVersion = ">=0.90.3"; diff --git a/src/app/controllers/dashLoader.js b/src/app/controllers/dashLoader.js index 4cbf5c3aa1b..7f9dc9232dd 100644 --- a/src/app/controllers/dashLoader.js +++ b/src/app/controllers/dashLoader.js @@ -65,23 +65,20 @@ function (angular, _, moment) { }; $scope.elasticsearch_save = function(type,ttl) { - dashboard.elasticsearch_save( - type, - ($scope.elasticsearch.title || dashboard.current.title), - ($scope.loader.save_temp_ttl_enable ? ttl : false) - ).then(function(result) { - if(_.isUndefined(result._id)) { - alertSrv.set('Save failed','Dashboard could not be saved to Elasticsearch','error',5000); - return; - } + dashboard.elasticsearch_save(type, dashboard.current.title, ttl) + .then(function(result) { + if(_.isUndefined(result._id)) { + alertSrv.set('Save failed','Dashboard could not be saved to Elasticsearch','error',5000); + return; + } - alertSrv.set('Dashboard Saved', 'This dashboard has been saved to Elasticsearch as "' + result._id + '"','success', 5000); - if(type === 'temp') { - $scope.share = dashboard.share_link(dashboard.current.title,'temp',result._id); - } + alertSrv.set('Dashboard Saved', 'Dashboard has been saved to Elasticsearch as "' + result._id + '"','success', 5000); + if(type === 'temp') { + $scope.share = dashboard.share_link(dashboard.current.title,'temp',result._id); + } - $rootScope.$emit('dashboard-saved'); - }); + $rootScope.$emit('dashboard-saved'); + }); }; $scope.elasticsearch_delete = function(id) { diff --git a/src/app/partials/unsaved-changes.html b/src/app/partials/unsaved-changes.html new file mode 100644 index 00000000000..87220e41e8d --- /dev/null +++ b/src/app/partials/unsaved-changes.html @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/src/app/services/all.js b/src/app/services/all.js index 2f2ecfb3543..53a4f521ac6 100644 --- a/src/app/services/all.js +++ b/src/app/services/all.js @@ -8,5 +8,6 @@ define([ './keyboardManager', './annotationsSrv', './playlistSrv', + './unsavedChangesSrv', ], function () {}); \ No newline at end of file diff --git a/src/app/services/dashboard.js b/src/app/services/dashboard.js index 002e8e05156..2addf29d0b9 100644 --- a/src/app/services/dashboard.js +++ b/src/app/services/dashboard.js @@ -63,6 +63,7 @@ function (angular, $, kbn, _, config, moment, Modernizr) { $rootScope.$on('$routeChangeSuccess',function(){ // Clear the current dashboard to prevent reloading self.current = {}; + self.original = null; self.indices = []; route(); }); @@ -157,16 +158,8 @@ function (angular, $, kbn, _, config, moment, Modernizr) { // Set the current dashboard self.current = angular.copy(dashboard); - // Delay this until we're sure that querySrv and filterSrv are ready - $timeout(function() { - // Ok, now that we've setup the current dashboard, we can inject our services - filterSrv = $injector.get('filterSrv'); - filterSrv.init(); - - },0).then(function() { - // Call refresh to calculate the indices and notify the panels that we're ready to roll - self.refresh(); - }); + filterSrv = $injector.get('filterSrv'); + filterSrv.init(); if(dashboard.refresh) { self.set_interval(dashboard.refresh); @@ -181,6 +174,10 @@ function (angular, $, kbn, _, config, moment, Modernizr) { $rootScope.$emit('dashboard-loaded'); + $timeout(function() { + self.original = angular.copy(self.current); + }, 500); + return true; }; @@ -393,6 +390,7 @@ function (angular, $, kbn, _, config, moment, Modernizr) { if(type === 'dashboard') { $location.path('/dashboard/elasticsearch/'+title); } + self.original = angular.copy(self.current); return result; }, // Failure diff --git a/src/app/services/unsavedChangesSrv.js b/src/app/services/unsavedChangesSrv.js new file mode 100644 index 00000000000..a0d2a079d34 --- /dev/null +++ b/src/app/services/unsavedChangesSrv.js @@ -0,0 +1,102 @@ +define([ + 'angular', + 'underscore', + 'config', +], +function (angular, _, config) { + 'use strict'; + + if (!config.unsaved_changes_warning) { + return; + } + + var module = angular.module('kibana.services'); + + module.service('unsavedChangesSrv', function($rootScope, $modal, dashboard, $q, $location, $timeout) { + var self = this; + var modalScope = $rootScope.$new(); + + window.onbeforeunload = function () { + if (self.has_unsaved_changes()) { + return "There are unsaved changes to this dashboard"; + } + }; + + this.init = function() { + $rootScope.$on("$locationChangeStart", function(event, next) { + if (self.has_unsaved_changes()) { + event.preventDefault(); + self.next = next; + self.open_modal(); + } + }); + }; + + this.open_modal = function () { + var confirmModal = $modal({ + template: './app/partials/unsaved-changes.html', + persist: true, + show: false, + scope: modalScope, + keyboard: false + }); + + $q.when(confirmModal).then(function(modalEl) { + modalEl.modal('show'); + }); + }; + + this.has_unsaved_changes = function () { + if (!dashboard.original) { + return false; + } + + var current = angular.copy(dashboard.current); + var original = dashboard.original; + + // ignore timespan changes + current.services.filter.time = original.services.filter.time = {}; + current.refresh = original.refresh; + + var currentTimepicker = _.findWhere(current.nav, { type: 'timepicker' }); + var originalTimepicker = _.findWhere(original.nav, { type: 'timepicker' }); + + if (currentTimepicker && originalTimepicker) { + currentTimepicker.now = originalTimepicker.now; + } + + var currentJson = angular.toJson(current); + var originalJson = angular.toJson(original); + + if (currentJson !== originalJson) { + return true; + } + + return false; + }; + + this.goto_next = function () { + var baseLen = $location.absUrl().length - $location.url().length; + var nextUrl = self.next.substring(baseLen); + $location.url(nextUrl); + }; + + modalScope.ignore = function() { + dashboard.original = null; + self.goto_next(); + }; + + modalScope.save = function() { + var unregister = $rootScope.$on('dashboard-saved', function() { + self.goto_next(); + }); + + $timeout(unregister, 2000); + + $rootScope.$emit('save-dashboard'); + }; + + }).run(function(unsavedChangesSrv) { + unsavedChangesSrv.init(); + }); +}); \ No newline at end of file diff --git a/src/config.sample.js b/src/config.sample.js index 511e2bc69e8..6fbbe195e26 100644 --- a/src/config.sample.js +++ b/src/config.sample.js @@ -57,8 +57,17 @@ function (Settings) { timezoneOffset: null, + /** + * Elasticsearch index for storing dashboards + * + */ grafana_index: "grafana-dash", + /** + * set to false to disable unsaved changes warning + */ + unsaved_changes_warning: true, + panel_names: [ 'text', 'graphite'