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 @@
+
+
+
+
Unsaved changes
+
+
+
+
+
+
+
+
+
+
\ 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'