Browse Source

migrated files to ts + fixed specfile

Patrick O'Carroll 8 years ago
parent
commit
9b63a81756

+ 3 - 3
public/app/features/dashboard/all.ts

@@ -1,18 +1,18 @@
 import './dashboard_ctrl';
 import './alerting_srv';
 import './history/history';
-import './dashboardLoaderSrv';
+import './dashboard_loader_srv';
 import './dashnav/dashnav';
 import './submenu/submenu';
 import './save_as_modal';
 import './save_modal';
 import './shareModalCtrl';
-import './shareSnapshotCtrl';
+import './share_snapshot_ctrl';
 import './dashboard_srv';
 import './view_state_srv';
 import './validation_srv';
 import './time_srv';
-import './unsavedChangesSrv';
+import './unsaved_changes_srv';
 import './unsaved_changes_modal';
 import './timepicker/timepicker';
 import './upload';

+ 0 - 109
public/app/features/dashboard/dashboardLoaderSrv.js

@@ -1,109 +0,0 @@
-define([
-  'angular',
-  'moment',
-  'lodash',
-  'jquery',
-  'app/core/utils/kbn',
-  'app/core/utils/datemath',
-  'app/core/services/impression_srv'
-],
-function (angular, moment, _, $, kbn, dateMath, impressionSrv) {
-  'use strict';
-
-  kbn = kbn.default;
-  impressionSrv = impressionSrv.default;
-
-  var module = angular.module('grafana.services');
-
-  module.service('dashboardLoaderSrv', function(backendSrv,
-                                                   dashboardSrv,
-                                                   datasourceSrv,
-                                                   $http, $q, $timeout,
-                                                   contextSrv, $routeParams,
-                                                   $rootScope) {
-    var self = this;
-
-    this._dashboardLoadFailed = function(title, snapshot) {
-      snapshot = snapshot || false;
-      return {
-        meta: { canStar: false, isSnapshot: snapshot, canDelete: false, canSave: false, canEdit: false, dashboardNotFound: true },
-        dashboard: {title: title }
-      };
-    };
-
-    this.loadDashboard = function(type, slug) {
-      var promise;
-
-      if (type === 'script') {
-        promise = this._loadScriptedDashboard(slug);
-      } else if (type === 'snapshot') {
-        promise = backendSrv.get('/api/snapshots/' + $routeParams.slug)
-          .catch(function() {
-            return self._dashboardLoadFailed("Snapshot not found", true);
-          });
-      } else {
-        promise = backendSrv.getDashboard($routeParams.type, $routeParams.slug)
-          .then(function(result) {
-            if (result.meta.isFolder) {
-              $rootScope.appEvent("alert-error", ['Dashboard not found']);
-              throw new Error("Dashboard not found");
-            }
-            return result;
-          })
-          .catch(function() {
-            return self._dashboardLoadFailed("Not found");
-          });
-      }
-
-      promise.then(function(result) {
-
-        if (result.meta.dashboardNotFound !== true) {
-          impressionSrv.addDashboardImpression(result.dashboard.id);
-        }
-
-        return result;
-      });
-
-      return promise;
-    };
-
-    this._loadScriptedDashboard = function(file) {
-      var url = 'public/dashboards/'+file.replace(/\.(?!js)/,"/") + '?' + new Date().getTime();
-
-      return $http({ url: url, method: "GET" })
-      .then(this._executeScript).then(function(result) {
-        return { meta: { fromScript: true, canDelete: false, canSave: false, canStar: false}, dashboard: result.data };
-      }, function(err) {
-        console.log('Script dashboard error '+ err);
-        $rootScope.appEvent('alert-error', ["Script Error", "Please make sure it exists and returns a valid dashboard"]);
-        return self._dashboardLoadFailed('Scripted dashboard');
-      });
-    };
-
-    this._executeScript = function(result) {
-      var services = {
-        dashboardSrv: dashboardSrv,
-        datasourceSrv: datasourceSrv,
-        $q: $q,
-      };
-
-      /*jshint -W054 */
-      var script_func = new Function('ARGS','kbn','dateMath','_','moment','window','document','$','jQuery', 'services', result.data);
-      var script_result = script_func($routeParams, kbn, dateMath, _ , moment, window, document, $, $, services);
-
-      // Handle async dashboard scripts
-      if (_.isFunction(script_result)) {
-        var deferred = $q.defer();
-        script_result(function(dashboard) {
-          $timeout(function() {
-            deferred.resolve({ data: dashboard });
-          });
-        });
-        return deferred.promise;
-      }
-
-      return { data: script_result };
-    };
-
-  });
-});

+ 158 - 0
public/app/features/dashboard/dashboard_loader_srv.ts

@@ -0,0 +1,158 @@
+import angular from 'angular';
+import moment from 'moment';
+import _ from 'lodash';
+import $ from 'jquery';
+import kbn from 'app/core/utils/kbn';
+import * as dateMath from 'app/core/utils/datemath';
+import impressionSrv from 'app/core/services/impression_srv';
+
+export class DashboardLoaderSrv {
+  /** @ngInject */
+  constructor(
+    private backendSrv,
+    private dashboardSrv,
+    private datasourceSrv,
+    private $http,
+    private $q,
+    private $timeout,
+    contextSrv,
+    private $routeParams,
+    private $rootScope
+  ) {}
+
+  _dashboardLoadFailed(title, snapshot) {
+    snapshot = snapshot || false;
+    return {
+      meta: {
+        canStar: false,
+        isSnapshot: snapshot,
+        canDelete: false,
+        canSave: false,
+        canEdit: false,
+        dashboardNotFound: true,
+      },
+      dashboard: { title: title },
+    };
+  }
+
+  loadDashboard(type, slug) {
+    var promise;
+
+    if (type === 'script') {
+      promise = this._loadScriptedDashboard(slug);
+    } else if (type === 'snapshot') {
+      promise = this.backendSrv
+        .get('/api/snapshots/' + this.$routeParams.slug)
+        .catch(() => {
+          return this._dashboardLoadFailed('Snapshot not found', true);
+        });
+    } else {
+      promise = this.backendSrv
+        .getDashboard(this.$routeParams.type, this.$routeParams.slug)
+        .then(result => {
+          if (result.meta.isFolder) {
+            this.$rootScope.appEvent('alert-error', ['Dashboard not found']);
+            throw new Error('Dashboard not found');
+          }
+          return result;
+        })
+        .catch(() => {
+          return this._dashboardLoadFailed('Not found', true);
+        });
+    }
+
+    promise.then(function(result) {
+      if (result.meta.dashboardNotFound !== true) {
+        impressionSrv.addDashboardImpression(result.dashboard.id);
+      }
+
+      return result;
+    });
+
+    return promise;
+  }
+
+  _loadScriptedDashboard(file) {
+    var url =
+      'public/dashboards/' +
+      file.replace(/\.(?!js)/, '/') +
+      '?' +
+      new Date().getTime();
+
+    return this.$http({ url: url, method: 'GET' })
+      .then(this._executeScript)
+      .then(
+        function(result) {
+          return {
+            meta: {
+              fromScript: true,
+              canDelete: false,
+              canSave: false,
+              canStar: false,
+            },
+            dashboard: result.data,
+          };
+        },
+        function(err) {
+          console.log('Script dashboard error ' + err);
+          this.$rootScope.appEvent('alert-error', [
+            'Script Error',
+            'Please make sure it exists and returns a valid dashboard',
+          ]);
+          return this._dashboardLoadFailed('Scripted dashboard');
+        }
+      );
+  }
+
+  _executeScript(result) {
+    var services = {
+      dashboardSrv: this.dashboardSrv,
+      datasourceSrv: this.datasourceSrv,
+      $q: this.$q,
+    };
+
+    /*jshint -W054 */
+    var script_func = new Function(
+      'ARGS',
+      'kbn',
+      'dateMath',
+      '_',
+      'moment',
+      'window',
+      'document',
+      '$',
+      'jQuery',
+      'services',
+      result.data
+    );
+    var script_result = script_func(
+      this.$routeParams,
+      kbn,
+      dateMath,
+      _,
+      moment,
+      window,
+      document,
+      $,
+      $,
+      services
+    );
+
+    // Handle async dashboard scripts
+    if (_.isFunction(script_result)) {
+      var deferred = this.$q.defer();
+      script_result(dashboard => {
+        this.$timeout(() => {
+          deferred.resolve({ data: dashboard });
+        });
+      });
+      return deferred.promise;
+    }
+
+    return { data: script_result };
+  }
+}
+
+angular
+  .module('grafana.services')
+  .service('dashboardLoaderSrv', DashboardLoaderSrv);

+ 58 - 55
public/app/features/dashboard/shareSnapshotCtrl.js → public/app/features/dashboard/share_snapshot_ctrl.ts

@@ -1,14 +1,8 @@
-define([
-  'angular',
-  'lodash',
-],
-function (angular, _) {
-  'use strict';
-
-  var module = angular.module('grafana.controllers');
-
-  module.controller('ShareSnapshotCtrl', function($scope, $rootScope, $location, backendSrv, $timeout, timeSrv) {
+import angular from 'angular';
+import _ from 'lodash';
 
+export class ShareSnapshotCtrl {
+  constructor($scope, $rootScope, $location, backendSrv, $timeout, timeSrv) {
     $scope.snapshot = {
       name: $scope.dashboard.title,
       expires: 0,
@@ -18,16 +12,16 @@ function (angular, _) {
     $scope.step = 1;
 
     $scope.expireOptions = [
-      {text: '1 Hour', value: 60*60},
-      {text: '1 Day',  value: 60*60*24},
-      {text: '7 Days', value: 60*60*24*7},
-      {text: 'Never',  value: 0},
+      { text: '1 Hour', value: 60 * 60 },
+      { text: '1 Day', value: 60 * 60 * 24 },
+      { text: '7 Days', value: 60 * 60 * 24 * 7 },
+      { text: 'Never', value: 0 },
     ];
 
     $scope.accessOptions = [
-      {text: 'Anyone with the link', value: 1},
-      {text: 'Organization users',  value: 2},
-      {text: 'Public on the web', value: 3},
+      { text: 'Anyone with the link', value: 1 },
+      { text: 'Organization users', value: 2 },
+      { text: 'Public on the web', value: 3 },
     ];
 
     $scope.init = function() {
@@ -42,7 +36,7 @@ function (angular, _) {
 
     $scope.createSnapshot = function(external) {
       $scope.dashboard.snapshot = {
-        timestamp: new Date()
+        timestamp: new Date(),
       };
 
       if (!external) {
@@ -69,31 +63,37 @@ function (angular, _) {
         expires: $scope.snapshot.expires,
       };
 
-      var postUrl = external ? $scope.externalUrl + $scope.apiUrl : $scope.apiUrl;
-
-      backendSrv.post(postUrl, cmdData).then(function(results) {
-        $scope.loading = false;
-
-        if (external) {
-          $scope.deleteUrl = results.deleteUrl;
-          $scope.snapshotUrl = results.url;
-          $scope.saveExternalSnapshotRef(cmdData, results);
-        } else {
-          var url = $location.url();
-          var baseUrl = $location.absUrl();
-
-          if (url !== '/') {
-            baseUrl = baseUrl.replace(url, '') + '/';
+      var postUrl = external
+        ? $scope.externalUrl + $scope.apiUrl
+        : $scope.apiUrl;
+
+      backendSrv.post(postUrl, cmdData).then(
+        function(results) {
+          $scope.loading = false;
+
+          if (external) {
+            $scope.deleteUrl = results.deleteUrl;
+            $scope.snapshotUrl = results.url;
+            $scope.saveExternalSnapshotRef(cmdData, results);
+          } else {
+            var url = $location.url();
+            var baseUrl = $location.absUrl();
+
+            if (url !== '/') {
+              baseUrl = baseUrl.replace(url, '') + '/';
+            }
+
+            $scope.snapshotUrl = baseUrl + 'dashboard/snapshot/' + results.key;
+            $scope.deleteUrl =
+              baseUrl + 'api/snapshots-delete/' + results.deleteKey;
           }
 
-          $scope.snapshotUrl = baseUrl + 'dashboard/snapshot/' + results.key;
-          $scope.deleteUrl = baseUrl + 'api/snapshots-delete/' + results.deleteKey;
+          $scope.step = 2;
+        },
+        function() {
+          $scope.loading = false;
         }
-
-        $scope.step = 2;
-      }, function() {
-        $scope.loading = false;
-      });
+      );
     };
 
     $scope.getSnapshotUrl = function() {
@@ -116,21 +116,22 @@ function (angular, _) {
 
       // remove annotation queries
       dash.annotations.list = _.chain(dash.annotations.list)
-      .filter(function(annotation) {
-        return annotation.enable;
-      })
-      .map(function(annotation) {
-        return {
-          name: annotation.name,
-          enable: annotation.enable,
-          iconColor: annotation.iconColor,
-          snapshotData: annotation.snapshotData
-        };
-      }).value();
+        .filter(function(annotation) {
+          return annotation.enable;
+        })
+        .map(function(annotation) {
+          return {
+            name: annotation.name,
+            enable: annotation.enable,
+            iconColor: annotation.iconColor,
+            snapshotData: annotation.snapshotData,
+          };
+        })
+        .value();
 
       // remove template queries
       _.each(dash.templating.list, function(variable) {
-        variable.query = "";
+        variable.query = '';
         variable.options = variable.current;
         variable.refresh = false;
       });
@@ -168,7 +169,9 @@ function (angular, _) {
       cmdData.deleteKey = results.deleteKey;
       backendSrv.post('/api/snapshots/', cmdData);
     };
+  }
+}
 
-  });
-
-});
+angular
+  .module('grafana.controllers')
+  .controller('ShareSnapshotCtrl', ShareSnapshotCtrl);

+ 21 - 5
public/app/features/dashboard/specs/unsaved_changes_srv_specs.ts

@@ -6,14 +6,17 @@ import {
   sinon,
   angularMocks,
 } from 'test/lib/common';
-import 'app/features/dashboard/unsavedChangesSrv';
+import { Tracker } from 'app/features/dashboard/unsaved_changes_srv';
 import 'app/features/dashboard/dashboard_srv';
+import { contextSrv } from 'app/core/core';
 
 describe('unsavedChangesSrv', function() {
-  var _unsavedChangesSrv;
   var _dashboardSrv;
   var _contextSrvStub = { isEditor: true };
   var _rootScope;
+  var _location;
+  var _timeout;
+  var _window;
   var tracker;
   var dash;
   var scope;
@@ -32,11 +35,15 @@ describe('unsavedChangesSrv', function() {
       unsavedChangesSrv,
       $location,
       $rootScope,
-      dashboardSrv
+      dashboardSrv,
+      $timeout,
+      $window
     ) {
-      _unsavedChangesSrv = unsavedChangesSrv;
       _dashboardSrv = dashboardSrv;
       _rootScope = $rootScope;
+      _location = $location;
+      _timeout = $timeout;
+      _window = $window;
     })
   );
 
@@ -54,7 +61,16 @@ describe('unsavedChangesSrv', function() {
     scope.appEvent = sinon.spy();
     scope.onAppEvent = sinon.spy();
 
-    tracker = new _unsavedChangesSrv.Tracker(dash, scope);
+    tracker = new Tracker(
+      dash,
+      scope,
+      undefined,
+      _location,
+      _window,
+      _timeout,
+      contextSrv,
+      _rootScope
+    );
   });
 
   it('No changes should not have changes', function() {

+ 0 - 189
public/app/features/dashboard/unsavedChangesSrv.js

@@ -1,189 +0,0 @@
-define([
-  'angular',
-  'lodash',
-],
-function(angular, _) {
-  'use strict';
-
-  var module = angular.module('grafana.services');
-
-  module.service('unsavedChangesSrv', function($rootScope, $q, $location, $timeout, contextSrv, dashboardSrv, $window) {
-
-    function Tracker(dashboard, scope, originalCopyDelay) {
-      var self = this;
-
-      this.current = dashboard;
-      this.originalPath = $location.path();
-      this.scope = scope;
-
-      // register events
-      scope.onAppEvent('dashboard-saved', function() {
-        this.original = this.current.getSaveModelClone();
-        this.originalPath = $location.path();
-      }.bind(this));
-
-      $window.onbeforeunload = function() {
-        if (self.ignoreChanges()) { return; }
-        if (self.hasChanges()) {
-          return "There are unsaved changes to this dashboard";
-        }
-      };
-
-      scope.$on("$locationChangeStart", function(event, next) {
-        // check if we should look for changes
-        if (self.originalPath === $location.path()) { return true; }
-        if (self.ignoreChanges()) { return true; }
-
-        if (self.hasChanges()) {
-          event.preventDefault();
-          self.next = next;
-
-          $timeout(function() {
-            self.open_modal();
-          });
-        }
-      });
-
-      if (originalCopyDelay) {
-        $timeout(function() {
-          // wait for different services to patch the dashboard (missing properties)
-          self.original = dashboard.getSaveModelClone();
-        }, originalCopyDelay);
-      } else {
-        self.original = dashboard.getSaveModelClone();
-      }
-    }
-
-    var p = Tracker.prototype;
-
-    // for some dashboards and users
-    // changes should be ignored
-    p.ignoreChanges = function() {
-      if (!this.original) { return true; }
-      if (!contextSrv.isEditor) { return true; }
-      if (!this.current || !this.current.meta) { return true; }
-
-      var meta = this.current.meta;
-      return !meta.canSave || meta.fromScript || meta.fromFile;
-    };
-
-    // remove stuff that should not count in diff
-    p.cleanDashboardFromIgnoredChanges = function(dash) {
-      // ignore time and refresh
-      dash.time = 0;
-      dash.refresh = 0;
-      dash.schemaVersion = 0;
-
-      // filter row and panels properties that should be ignored
-      dash.rows = _.filter(dash.rows, function(row) {
-        if (row.repeatRowId) {
-          return false;
-        }
-
-        row.panels = _.filter(row.panels, function(panel) {
-          if (panel.repeatPanelId) {
-            return false;
-          }
-
-          // remove scopedVars
-          panel.scopedVars = null;
-
-          // ignore span changes
-          panel.span = null;
-
-          // ignore panel legend sort
-          if (panel.legend)  {
-            delete panel.legend.sort;
-            delete panel.legend.sortDesc;
-          }
-
-          return true;
-        });
-
-        // ignore collapse state
-        row.collapse = false;
-        return true;
-      });
-
-      dash.panels = _.filter(dash.panels, function(panel) {
-        if (panel.repeatPanelId) {
-          return false;
-        }
-
-        // remove scopedVars
-        panel.scopedVars = null;
-
-        // ignore panel legend sort
-        if (panel.legend)  {
-          delete panel.legend.sort;
-          delete panel.legend.sortDesc;
-        }
-
-        return true;
-      });
-
-      // ignore template variable values
-      _.each(dash.templating.list, function(value) {
-        value.current = null;
-        value.options = null;
-        value.filters = null;
-      });
-    };
-
-    p.hasChanges = function() {
-      var current = this.current.getSaveModelClone();
-      var original = this.original;
-
-      this.cleanDashboardFromIgnoredChanges(current);
-      this.cleanDashboardFromIgnoredChanges(original);
-
-      var currentTimepicker = _.find(current.nav, { type: 'timepicker' });
-      var originalTimepicker = _.find(original.nav, { type: 'timepicker' });
-
-      if (currentTimepicker && originalTimepicker) {
-        currentTimepicker.now = originalTimepicker.now;
-      }
-
-      var currentJson = angular.toJson(current);
-      var originalJson = angular.toJson(original);
-
-      return currentJson !== originalJson;
-    };
-
-    p.discardChanges = function() {
-      this.original = null;
-      this.gotoNext();
-    };
-
-    p.open_modal = function() {
-      $rootScope.appEvent('show-modal', {
-        templateHtml: '<unsaved-changes-modal dismiss="dismiss()"></unsaved-changes-modal>',
-        modalClass: 'modal--narrow confirm-modal'
-      });
-    };
-
-    p.saveChanges = function() {
-      var self = this;
-      var cancel = $rootScope.$on('dashboard-saved', function() {
-        cancel();
-        $timeout(function() {
-          self.gotoNext();
-        });
-      });
-
-      $rootScope.appEvent('save-dashboard');
-    };
-
-    p.gotoNext = function() {
-      var baseLen = $location.absUrl().length - $location.url().length;
-      var nextUrl = this.next.substring(baseLen);
-      $location.url(nextUrl);
-    };
-
-    this.Tracker = Tracker;
-    this.init = function(dashboard, scope) {
-      this.tracker = new Tracker(dashboard, scope, 1000);
-      return this.tracker;
-    };
-  });
-});

+ 236 - 0
public/app/features/dashboard/unsaved_changes_srv.ts

@@ -0,0 +1,236 @@
+import angular from 'angular';
+import _ from 'lodash';
+
+export class Tracker {
+  current: any;
+  originalPath: any;
+  scope: any;
+  original: any;
+  next: any;
+  $window: any;
+
+  /** @ngInject */
+  constructor(
+    dashboard,
+    scope,
+    originalCopyDelay,
+    private $location,
+    $window,
+    private $timeout,
+    private contextSrv,
+    private $rootScope
+  ) {
+    this.$location = $location;
+    this.$window = $window;
+
+    this.current = dashboard;
+    this.originalPath = $location.path();
+    this.scope = scope;
+
+    // register events
+    scope.onAppEvent('dashboard-saved', () => {
+      this.original = this.current.getSaveModelClone();
+      this.originalPath = $location.path();
+    });
+
+    $window.onbeforeunload = () => {
+      if (this.ignoreChanges()) {
+        return '';
+      }
+      if (this.hasChanges()) {
+        return 'There are unsaved changes to this dashboard';
+      }
+      return '';
+    };
+
+    scope.$on('$locationChangeStart', (event, next) => {
+      // check if we should look for changes
+      if (this.originalPath === $location.path()) {
+        return true;
+      }
+      if (this.ignoreChanges()) {
+        return true;
+      }
+
+      if (this.hasChanges()) {
+        event.preventDefault();
+        this.next = next;
+
+        this.$timeout(() => {
+          this.open_modal();
+        });
+      }
+      return false;
+    });
+
+    if (originalCopyDelay) {
+      this.$timeout(() => {
+        // wait for different services to patch the dashboard (missing properties)
+        this.original = dashboard.getSaveModelClone();
+      }, originalCopyDelay);
+    } else {
+      this.original = dashboard.getSaveModelClone();
+    }
+  }
+
+  // for some dashboards and users
+  // changes should be ignored
+  ignoreChanges() {
+    if (!this.original) {
+      return true;
+    }
+    if (!this.contextSrv.isEditor) {
+      return true;
+    }
+    if (!this.current || !this.current.meta) {
+      return true;
+    }
+
+    var meta = this.current.meta;
+    return !meta.canSave || meta.fromScript || meta.fromFile;
+  }
+
+  // remove stuff that should not count in diff
+  cleanDashboardFromIgnoredChanges(dash) {
+    // ignore time and refresh
+    dash.time = 0;
+    dash.refresh = 0;
+    dash.schemaVersion = 0;
+
+    // filter row and panels properties that should be ignored
+    dash.rows = _.filter(dash.rows, function(row) {
+      if (row.repeatRowId) {
+        return false;
+      }
+
+      row.panels = _.filter(row.panels, function(panel) {
+        if (panel.repeatPanelId) {
+          return false;
+        }
+
+        // remove scopedVars
+        panel.scopedVars = null;
+
+        // ignore span changes
+        panel.span = null;
+
+        // ignore panel legend sort
+        if (panel.legend) {
+          delete panel.legend.sort;
+          delete panel.legend.sortDesc;
+        }
+
+        return true;
+      });
+
+      // ignore collapse state
+      row.collapse = false;
+      return true;
+    });
+
+    dash.panels = _.filter(dash.panels, panel => {
+      if (panel.repeatPanelId) {
+        return false;
+      }
+
+      // remove scopedVars
+      panel.scopedVars = null;
+
+      // ignore panel legend sort
+      if (panel.legend) {
+        delete panel.legend.sort;
+        delete panel.legend.sortDesc;
+      }
+
+      return true;
+    });
+
+    // ignore template variable values
+    _.each(dash.templating.list, function(value) {
+      value.current = null;
+      value.options = null;
+      value.filters = null;
+    });
+  }
+
+  hasChanges() {
+    var current = this.current.getSaveModelClone();
+    var original = this.original;
+
+    this.cleanDashboardFromIgnoredChanges(current);
+    this.cleanDashboardFromIgnoredChanges(original);
+
+    var currentTimepicker = _.find(current.nav, { type: 'timepicker' });
+    var originalTimepicker = _.find(original.nav, { type: 'timepicker' });
+
+    if (currentTimepicker && originalTimepicker) {
+      currentTimepicker.now = originalTimepicker.now;
+    }
+
+    var currentJson = angular.toJson(current);
+    var originalJson = angular.toJson(original);
+
+    return currentJson !== originalJson;
+  }
+
+  discardChanges() {
+    this.original = null;
+    this.gotoNext();
+  }
+
+  open_modal() {
+    this.$rootScope.appEvent('show-modal', {
+      templateHtml:
+        '<unsaved-changes-modal dismiss="dismiss()"></unsaved-changes-modal>',
+      modalClass: 'modal--narrow confirm-modal',
+    });
+  }
+
+  saveChanges() {
+    var self = this;
+    var cancel = this.$rootScope.$on('dashboard-saved', () => {
+      cancel();
+      this.$timeout(() => {
+        self.gotoNext();
+      });
+    });
+
+    this.$rootScope.appEvent('save-dashboard');
+  }
+
+  gotoNext() {
+    var baseLen = this.$location.absUrl().length - this.$location.url().length;
+    var nextUrl = this.next.substring(baseLen);
+    this.$location.url(nextUrl);
+  }
+}
+
+/** @ngInject */
+export function unsavedChangesSrv(
+  $rootScope,
+  $q,
+  $location,
+  $timeout,
+  contextSrv,
+  dashboardSrv,
+  $window
+) {
+  this.Tracker = Tracker;
+  this.init = function(dashboard, scope) {
+    this.tracker = new Tracker(
+      dashboard,
+      scope,
+      1000,
+      $location,
+      $window,
+      $timeout,
+      contextSrv,
+      $rootScope
+    );
+    return this.tracker;
+  };
+}
+
+angular
+  .module('grafana.services')
+  .service('unsavedChangesSrv', unsavedChangesSrv);