Browse Source

refactor(alerting): initial step for seperating alerting and dash model, #6273

Torkel Ödegaard 9 years ago
parent
commit
30645a6d3d

+ 1 - 1
pkg/api/alerting.go

@@ -252,7 +252,7 @@ func NotificationTest(c *middleware.Context, dto dtos.NotificationTestCommand) R
 	return ApiSuccess("Test notification sent")
 	return ApiSuccess("Test notification sent")
 }
 }
 
 
-//POST /api/:alertId/pause
+//POST /api/alerts/:alertId/pause
 func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response {
 func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response {
 	cmd := models.PauseAlertCommand{
 	cmd := models.PauseAlertCommand{
 		OrgId:   c.OrgId,
 		OrgId:   c.OrgId,

+ 22 - 0
public/app/features/dashboard/alerting_srv.ts

@@ -0,0 +1,22 @@
+///<reference path="../../headers/common.d.ts" />
+
+import config from 'app/core/config';
+import angular from 'angular';
+import moment from 'moment';
+import _ from 'lodash';
+
+import coreModule from 'app/core/core_module';
+
+export class AlertingSrv {
+  dashboard: any;
+  alerts: any[];
+
+  init(dashboard, alerts) {
+    this.dashboard = dashboard;
+    this.alerts = alerts || [];
+  }
+}
+
+
+coreModule.service('alertingSrv', AlertingSrv);
+

+ 1 - 0
public/app/features/dashboard/all.js

@@ -1,5 +1,6 @@
 define([
 define([
   './dashboard_ctrl',
   './dashboard_ctrl',
+  './alerting_srv',
   './dashboardLoaderSrv',
   './dashboardLoaderSrv',
   './dashnav/dashnav',
   './dashnav/dashnav',
   './submenu/submenu',
   './submenu/submenu',

+ 2 - 0
public/app/features/dashboard/dashboard_ctrl.ts

@@ -16,6 +16,7 @@ export class DashboardCtrl {
     dashboardKeybindings,
     dashboardKeybindings,
     timeSrv,
     timeSrv,
     variableSrv,
     variableSrv,
+    alertingSrv,
     dashboardSrv,
     dashboardSrv,
     unsavedChangesSrv,
     unsavedChangesSrv,
     dynamicDashboardSrv,
     dynamicDashboardSrv,
@@ -43,6 +44,7 @@ export class DashboardCtrl {
 
 
         // init services
         // init services
         timeSrv.init(dashboard);
         timeSrv.init(dashboard);
+        alertingSrv.init(dashboard, data.alerts);
 
 
         // template values service needs to initialize completely before
         // template values service needs to initialize completely before
         // the rest of the dashboard can load
         // the rest of the dashboard can load

+ 71 - 553
public/app/features/dashboard/dashboard_srv.ts

@@ -1,595 +1,113 @@
 ///<reference path="../../headers/common.d.ts" />
 ///<reference path="../../headers/common.d.ts" />
 
 
-import config from 'app/core/config';
-import angular from 'angular';
-import moment from 'moment';
 import _ from 'lodash';
 import _ from 'lodash';
-import $ from 'jquery';
-
-import {Emitter} from 'app/core/core';
-import {contextSrv} from 'app/core/services/context_srv';
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
+import {DashboardModel} from './model';
 
 
-export class DashboardModel {
-  id: any;
-  title: any;
-  autoUpdate: any;
-  description: any;
-  tags: any;
-  style: any;
-  timezone: any;
-  editable: any;
-  hideControls: any;
-  sharedCrosshair: any;
-  rows: any;
-  time: any;
-  timepicker: any;
-  templating: any;
-  annotations: any;
-  refresh: any;
-  snapshot: any;
-  schemaVersion: number;
-  version: number;
-  revision: number;
-  links: any;
-  gnetId: any;
-  meta: any;
-  events: any;
-
-  constructor(data, meta) {
-    if (!data) {
-      data = {};
-    }
-
-    this.events = new Emitter();
-    this.id = data.id || null;
-    this.revision = data.revision;
-    this.title = data.title || 'No Title';
-    this.autoUpdate = data.autoUpdate;
-    this.description = data.description;
-    this.tags = data.tags || [];
-    this.style = data.style || "dark";
-    this.timezone = data.timezone || '';
-    this.editable = data.editable !== false;
-    this.hideControls = data.hideControls || false;
-    this.sharedCrosshair = data.sharedCrosshair || false;
-    this.rows = data.rows || [];
-    this.time = data.time || { from: 'now-6h', to: 'now' };
-    this.timepicker = data.timepicker || {};
-    this.templating = this.ensureListExist(data.templating);
-    this.annotations = this.ensureListExist(data.annotations);
-    this.refresh = data.refresh;
-    this.snapshot = data.snapshot;
-    this.schemaVersion = data.schemaVersion || 0;
-    this.version = data.version || 0;
-    this.links = data.links || [];
-    this.gnetId = data.gnetId || null;
-
-    this.updateSchema(data);
-    this.initMeta(meta);
-  }
-
-  private initMeta(meta) {
-    meta = meta || {};
-
-    meta.canShare = meta.canShare !== false;
-    meta.canSave = meta.canSave !== false;
-    meta.canStar = meta.canStar !== false;
-    meta.canEdit = meta.canEdit !== false;
-
-    if (!this.editable) {
-      meta.canEdit = false;
-      meta.canDelete = false;
-      meta.canSave = false;
-      this.hideControls = true;
-    }
+export class DashboardSrv {
+  dash: any;
 
 
-    this.meta = meta;
+  /** @ngInject */
+  constructor(private backendSrv, private $rootScope, private $location) {
   }
   }
 
 
-  // cleans meta data and other non peristent state
-  getSaveModelClone() {
-    // temp remove stuff
-    var events = this.events;
-    var meta = this.meta;
-    delete this.events;
-    delete this.meta;
-
-    events.emit('prepare-save-model');
-    var copy = $.extend(true, {}, this);
-
-    // restore properties
-    this.events = events;
-    this.meta = meta;
-    return copy;
+  create(dashboard, meta) {
+    return new DashboardModel(dashboard, meta);
   }
   }
 
 
-  private ensureListExist(data) {
-    if (!data) { data = {}; }
-    if (!data.list) { data.list = []; }
-    return data;
+  setCurrent(dashboard) {
+    this.dash = dashboard;
   }
   }
 
 
-  getNextPanelId() {
-    var i, j, row, panel, max = 0;
-    for (i = 0; i < this.rows.length; i++) {
-      row = this.rows[i];
-      for (j = 0; j < row.panels.length; j++) {
-        panel = row.panels[j];
-        if (panel.id > max) { max = panel.id; }
-      }
-    }
-    return max + 1;
+  getCurrent() {
+    return this.dash;
   }
   }
 
 
-  forEachPanel(callback) {
-    var i, j, row;
-    for (i = 0; i < this.rows.length; i++) {
-      row = this.rows[i];
-      for (j = 0; j < row.panels.length; j++) {
-        callback(row.panels[j], j, row, i);
-      }
+  saveDashboard(options) {
+    if (!this.dash.meta.canSave && options.makeEditable !== true) {
+      return Promise.resolve();
     }
     }
-  }
 
 
-  getPanelById(id) {
-    for (var i = 0; i < this.rows.length; i++) {
-      var row = this.rows[i];
-      for (var j = 0; j < row.panels.length; j++) {
-        var panel = row.panels[j];
-        if (panel.id === id) {
-          return panel;
-        }
-      }
-    }
-    return null;
-  }
+    var clone = this.dash.getSaveModelClone();
 
 
-  rowSpan(row) {
-    return _.reduce(row.panels, function(p,v) {
-      return p + v.span;
-    },0);
-  };
+    return this.backendSrv.saveDashboard(clone, options).then(data => {
+      this.dash.version = data.version;
 
 
-  addPanel(panel, row) {
-    var rowSpan = this.rowSpan(row);
-    var panelCount = row.panels.length;
-    var space = (12 - rowSpan) - panel.span;
-    panel.id = this.getNextPanelId();
+      this.$rootScope.appEvent('dashboard-saved', this.dash);
 
 
-    // try to make room of there is no space left
-    if (space <= 0) {
-      if (panelCount === 1) {
-        row.panels[0].span = 6;
-        panel.span = 6;
-      } else if (panelCount === 2) {
-        row.panels[0].span = 4;
-        row.panels[1].span = 4;
-        panel.span = 4;
+      var dashboardUrl = '/dashboard/db/' + data.slug;
+      if (dashboardUrl !== this.$location.path()) {
+        this.$location.url(dashboardUrl);
       }
       }
-    }
 
 
-    row.panels.push(panel);
+      this.$rootScope.appEvent('alert-success', ['Dashboard saved', 'Saved as ' + clone.title]);
+    }).catch(this.handleSaveDashboardError.bind(this));
   }
   }
 
 
-  isSubmenuFeaturesEnabled() {
-    var visableTemplates = _.filter(this.templating.list, function(template) {
-      return template.hideVariable === undefined || template.hideVariable === false;
-    });
+  handleSaveDashboardError(err) {
+    if (err.data && err.data.status === "version-mismatch") {
+      err.isHandled = true;
 
 
-    return visableTemplates.length > 0 || this.annotations.list.length > 0 || this.links.length > 0;
-  }
-
-  getPanelInfoById(panelId) {
-    var result: any = {};
-    _.each(this.rows, function(row) {
-      _.each(row.panels, function(panel, index) {
-        if (panel.id === panelId) {
-          result.panel = panel;
-          result.row = row;
-          result.index = index;
+      this.$rootScope.appEvent('confirm-modal', {
+        title: 'Conflict',
+        text: 'Someone else has updated this dashboard.',
+        text2: 'Would you still like to save this dashboard?',
+        yesText: "Save & Overwrite",
+        icon: "fa-warning",
+        onConfirm: () => {
+          this.saveDashboard({overwrite: true});
         }
         }
       });
       });
-    });
-
-    if (!result.panel) {
-      return null;
-    }
-
-    return result;
-  }
-
-  duplicatePanel(panel, row) {
-    var rowIndex = _.indexOf(this.rows, row);
-    var newPanel = angular.copy(panel);
-    newPanel.id = this.getNextPanelId();
-
-    delete newPanel.repeat;
-    delete newPanel.repeatIteration;
-    delete newPanel.repeatPanelId;
-    delete newPanel.scopedVars;
-
-    var currentRow = this.rows[rowIndex];
-    currentRow.panels.push(newPanel);
-    return newPanel;
-  }
-
-  formatDate(date, format) {
-    date = moment.isMoment(date) ? date : moment(date);
-    format = format || 'YYYY-MM-DD HH:mm:ss';
-    this.timezone = this.getTimezone();
-
-    return this.timezone === 'browser' ?
-      moment(date).format(format) :
-      moment.utc(date).format(format);
-  }
-
-  getRelativeTime(date) {
-    date = moment.isMoment(date) ? date : moment(date);
-
-    return this.timezone === 'browser' ?
-      moment(date).fromNow() :
-      moment.utc(date).fromNow();
-  }
-
-  getNextQueryLetter(panel) {
-    var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
-
-    return _.find(letters, function(refId) {
-      return _.every(panel.targets, function(other) {
-        return other.refId !== refId;
-      });
-    });
-  }
-
-  isTimezoneUtc() {
-    return this.getTimezone() === 'utc';
-  }
-
-  getTimezone() {
-    return this.timezone ? this.timezone : contextSrv.user.timezone;
-  }
-
-  private updateSchema(old) {
-    var i, j, k;
-    var oldVersion = this.schemaVersion;
-    var panelUpgrades = [];
-    this.schemaVersion = 13;
-
-    if (oldVersion === this.schemaVersion) {
-      return;
     }
     }
 
 
-    // version 2 schema changes
-    if (oldVersion < 2) {
-
-      if (old.services) {
-        if (old.services.filter) {
-          this.time = old.services.filter.time;
-          this.templating.list = old.services.filter.list || [];
-        }
-      }
-
-      panelUpgrades.push(function(panel) {
-        // rename panel type
-        if (panel.type === 'graphite') {
-          panel.type = 'graph';
-        }
-
-        if (panel.type !== 'graph') {
-          return;
-        }
-
-        if (_.isBoolean(panel.legend)) { panel.legend = { show: panel.legend }; }
-
-        if (panel.grid) {
-          if (panel.grid.min) {
-            panel.grid.leftMin = panel.grid.min;
-            delete panel.grid.min;
-          }
-
-          if (panel.grid.max) {
-            panel.grid.leftMax = panel.grid.max;
-            delete panel.grid.max;
-          }
-        }
-
-        if (panel.y_format) {
-          panel.y_formats[0] = panel.y_format;
-          delete panel.y_format;
-        }
-
-        if (panel.y2_format) {
-          panel.y_formats[1] = panel.y2_format;
-          delete panel.y2_format;
+    if (err.data && err.data.status === "name-exists") {
+      err.isHandled = true;
+
+      this.$rootScope.appEvent('confirm-modal', {
+        title: 'Conflict',
+        text: 'Dashboard with the same name exists.',
+        text2: 'Would you still like to save this dashboard?',
+        yesText: "Save & Overwrite",
+        icon: "fa-warning",
+        onConfirm: () => {
+          this.saveDashboard({overwrite: true});
         }
         }
       });
       });
     }
     }
 
 
-    // schema version 3 changes
-    if (oldVersion < 3) {
-      // ensure panel ids
-      var maxId = this.getNextPanelId();
-      panelUpgrades.push(function(panel) {
-        if (!panel.id) {
-          panel.id = maxId;
-          maxId += 1;
+    if (err.data && err.data.status === "plugin-dashboard") {
+      err.isHandled = true;
+
+      this.$rootScope.appEvent('confirm-modal', {
+        title: 'Plugin Dashboard',
+        text: err.data.message,
+        text2: 'Your changes will be lost when you update the plugin. Use Save As to create custom version.',
+        yesText: "Overwrite",
+        icon: "fa-warning",
+        altActionText: "Save As",
+        onAltAction: () => {
+          this.saveDashboardAs();
+        },
+        onConfirm: function() {
+          this.saveDashboard({overwrite: true});
         }
         }
       });
       });
     }
     }
-
-    // schema version 4 changes
-    if (oldVersion < 4) {
-      // move aliasYAxis changes
-      panelUpgrades.push(function(panel) {
-        if (panel.type !== 'graph') { return; }
-        _.each(panel.aliasYAxis, function(value, key) {
-          panel.seriesOverrides = [{ alias: key, yaxis: value }];
-        });
-        delete panel.aliasYAxis;
-      });
-    }
-
-    if (oldVersion < 6) {
-      // move pulldowns to new schema
-      var annotations = _.find(old.pulldowns, { type: 'annotations' });
-
-      if (annotations) {
-        this.annotations = {
-          list: annotations.annotations || [],
-        };
-      }
-
-      // update template variables
-      for (i = 0 ; i < this.templating.list.length; i++) {
-        var variable = this.templating.list[i];
-        if (variable.datasource === void 0) { variable.datasource = null; }
-        if (variable.type === 'filter') { variable.type = 'query'; }
-        if (variable.type === void 0) { variable.type = 'query'; }
-        if (variable.allFormat === void 0) { variable.allFormat = 'glob'; }
-      }
-    }
-
-    if (oldVersion < 7) {
-      if (old.nav && old.nav.length) {
-        this.timepicker = old.nav[0];
-      }
-
-      // ensure query refIds
-      panelUpgrades.push(function(panel) {
-        _.each(panel.targets, function(target) {
-          if (!target.refId) {
-            target.refId = this.getNextQueryLetter(panel);
-            }
-          }.bind(this));
-        });
-      }
-
-      if (oldVersion < 8) {
-        panelUpgrades.push(function(panel) {
-          _.each(panel.targets, function(target) {
-            // update old influxdb query schema
-            if (target.fields && target.tags && target.groupBy) {
-              if (target.rawQuery) {
-                delete target.fields;
-                delete target.fill;
-              } else {
-                target.select = _.map(target.fields, function(field) {
-                  var parts = [];
-                  parts.push({type: 'field', params: [field.name]});
-                  parts.push({type: field.func, params: []});
-                  if (field.mathExpr) {
-                    parts.push({type: 'math', params: [field.mathExpr]});
-                  }
-                  if (field.asExpr) {
-                    parts.push({type: 'alias', params: [field.asExpr]});
-                  }
-                  return parts;
-                });
-                delete target.fields;
-                _.each(target.groupBy, function(part) {
-                  if (part.type === 'time' && part.interval)  {
-                    part.params = [part.interval];
-                    delete part.interval;
-                  }
-                  if (part.type === 'tag' && part.key) {
-                    part.params = [part.key];
-                    delete part.key;
-                  }
-                });
-
-                if (target.fill) {
-                  target.groupBy.push({type: 'fill', params: [target.fill]});
-                  delete target.fill;
-                }
-              }
-            }
-          });
-        });
-      }
-
-      // schema version 9 changes
-      if (oldVersion < 9) {
-        // move aliasYAxis changes
-        panelUpgrades.push(function(panel) {
-          if (panel.type !== 'singlestat' && panel.thresholds !== "") { return; }
-
-          if (panel.thresholds) {
-            var k = panel.thresholds.split(",");
-
-            if (k.length >= 3) {
-              k.shift();
-              panel.thresholds = k.join(",");
-            }
-          }
-        });
-      }
-
-      // schema version 10 changes
-      if (oldVersion < 10) {
-        // move aliasYAxis changes
-        panelUpgrades.push(function(panel) {
-          if (panel.type !== 'table') { return; }
-
-          _.each(panel.styles, function(style) {
-            if (style.thresholds && style.thresholds.length >= 3) {
-              var k = style.thresholds;
-              k.shift();
-              style.thresholds = k;
-            }
-          });
-        });
-      }
-
-      if (oldVersion < 12) {
-        // update template variables
-        _.each(this.templating.list, function(templateVariable) {
-          if (templateVariable.refresh) { templateVariable.refresh = 1; }
-          if (!templateVariable.refresh) { templateVariable.refresh = 0; }
-          if (templateVariable.hideVariable) {
-            templateVariable.hide = 2;
-          } else if (templateVariable.hideLabel) {
-            templateVariable.hide = 1;
-          } else {
-            templateVariable.hide = 0;
-          }
-        });
-      }
-
-      if (oldVersion < 12) {
-        // update graph yaxes changes
-        panelUpgrades.push(function(panel) {
-          if (panel.type !== 'graph') { return; }
-          if (!panel.grid) { return; }
-
-          if (!panel.yaxes) {
-            panel.yaxes = [
-              {
-                show: panel['y-axis'],
-                min: panel.grid.leftMin,
-                max: panel.grid.leftMax,
-                logBase: panel.grid.leftLogBase,
-                format: panel.y_formats[0],
-                label: panel.leftYAxisLabel,
-              },
-              {
-                show: panel['y-axis'],
-                min: panel.grid.rightMin,
-                max: panel.grid.rightMax,
-                logBase: panel.grid.rightLogBase,
-                format: panel.y_formats[1],
-                label: panel.rightYAxisLabel,
-              }
-            ];
-
-            panel.xaxis = {
-              show: panel['x-axis'],
-            };
-
-            delete panel.grid.leftMin;
-            delete panel.grid.leftMax;
-            delete panel.grid.leftLogBase;
-            delete panel.grid.rightMin;
-            delete panel.grid.rightMax;
-            delete panel.grid.rightLogBase;
-            delete panel.y_formats;
-            delete panel.leftYAxisLabel;
-            delete panel.rightYAxisLabel;
-            delete panel['y-axis'];
-            delete panel['x-axis'];
-          }
-        });
-      }
-
-      if (oldVersion < 13) {
-        // update graph yaxes changes
-        panelUpgrades.push(function(panel) {
-          if (panel.type !== 'graph') { return; }
-
-          panel.thresholds = [];
-          var t1: any = {}, t2: any = {};
-
-          if (panel.grid.threshold1 !== null) {
-            t1.value = panel.grid.threshold1;
-            if (panel.grid.thresholdLine) {
-              t1.line = true;
-              t1.lineColor = panel.grid.threshold1Color;
-              t1.colorMode = 'custom';
-            } else {
-              t1.fill = true;
-              t1.fillColor = panel.grid.threshold1Color;
-              t1.colorMode = 'custom';
-            }
-          }
-
-          if (panel.grid.threshold2 !== null) {
-            t2.value = panel.grid.threshold2;
-            if (panel.grid.thresholdLine) {
-              t2.line = true;
-              t2.lineColor = panel.grid.threshold2Color;
-              t2.colorMode = 'custom';
-            } else {
-              t2.fill = true;
-              t2.fillColor = panel.grid.threshold2Color;
-              t2.colorMode = 'custom';
-            }
-          }
-
-          if (_.isNumber(t1.value)) {
-            if (_.isNumber(t2.value)) {
-              if (t1.value > t2.value) {
-                t1.op = t2.op = 'lt';
-                panel.thresholds.push(t1);
-                panel.thresholds.push(t2);
-              } else {
-                t1.op = t2.op = 'gt';
-                panel.thresholds.push(t1);
-                panel.thresholds.push(t2);
-              }
-            } else {
-              t1.op = 'gt';
-              panel.thresholds.push(t1);
-            }
-          }
-
-          delete panel.grid.threshold1;
-          delete panel.grid.threshold1Color;
-          delete panel.grid.threshold2;
-          delete panel.grid.threshold2Color;
-          delete panel.grid.thresholdLine;
-        });
-      }
-
-      if (panelUpgrades.length === 0) {
-        return;
-      }
-
-      for (i = 0; i < this.rows.length; i++) {
-        var row = this.rows[i];
-        for (j = 0; j < row.panels.length; j++) {
-          for (k = 0; k < panelUpgrades.length; k++) {
-            panelUpgrades[k].call(this, row.panels[j]);
-          }
-        }
-      }
-    }
-}
-
-
-export class DashboardSrv {
-  currentDashboard: any;
-
-  create(dashboard, meta) {
-    return new DashboardModel(dashboard, meta);
   }
   }
 
 
-  setCurrent(dashboard) {
-    this.currentDashboard = dashboard;
-  }
+  saveDashboardAs() {
+    var newScope = this.$rootScope.$new();
+    newScope.clone = this.dash.getSaveModelClone();
+    newScope.clone.editable = true;
+    newScope.clone.hideControls = false;
 
 
-  getCurrent() {
-    return this.currentDashboard;
+    this.$rootScope.appEvent('show-modal', {
+      src: 'public/app/features/dashboard/partials/saveDashboardAs.html',
+      scope: newScope,
+      modalClass: 'modal--narrow'
+    });
   }
   }
+
 }
 }
 
 
 coreModule.service('dashboardSrv', DashboardSrv);
 coreModule.service('dashboardSrv', DashboardSrv);

+ 5 - 88
public/app/features/dashboard/dashnav/dashnav.ts

@@ -9,7 +9,7 @@ import {DashboardExporter} from '../export/exporter';
 export class DashNavCtrl {
 export class DashNavCtrl {
 
 
   /** @ngInject */
   /** @ngInject */
-  constructor($scope, $rootScope, alertSrv, $location, playlistSrv, backendSrv, $timeout, datasourceSrv) {
+  constructor($scope, $rootScope, dashboardSrv, $location, playlistSrv, backendSrv, $timeout, datasourceSrv) {
 
 
     $scope.init = function() {
     $scope.init = function() {
       $scope.onAppEvent('save-dashboard', $scope.saveDashboard);
       $scope.onAppEvent('save-dashboard', $scope.saveDashboard);
@@ -71,88 +71,14 @@ export class DashNavCtrl {
     $scope.makeEditable = function() {
     $scope.makeEditable = function() {
       $scope.dashboard.editable = true;
       $scope.dashboard.editable = true;
 
 
-      var clone = $scope.dashboard.getSaveModelClone();
-
-      backendSrv.saveDashboard(clone, {overwrite: false}).then(function(data) {
-        $scope.dashboard.version = data.version;
-        $scope.appEvent('dashboard-saved', $scope.dashboard);
-        $scope.appEvent('alert-success', ['Dashboard saved', 'Saved as ' + clone.title]);
-
+      return dashboardSrv.saveDashboard({makeEditable: true, overwrite: false}).then(function() {
         // force refresh whole page
         // force refresh whole page
         window.location.href = window.location.href;
         window.location.href = window.location.href;
-      }, $scope.handleSaveDashError);
+      });
     };
     };
 
 
     $scope.saveDashboard = function(options) {
     $scope.saveDashboard = function(options) {
-      if ($scope.dashboardMeta.canSave === false) {
-        return;
-      }
-
-      var clone = $scope.dashboard.getSaveModelClone();
-
-      backendSrv.saveDashboard(clone, options).then(function(data) {
-        $scope.dashboard.version = data.version;
-        $scope.appEvent('dashboard-saved', $scope.dashboard);
-
-        var dashboardUrl = '/dashboard/db/' + data.slug;
-
-        if (dashboardUrl !== $location.path()) {
-          $location.url(dashboardUrl);
-        }
-
-        $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: 'Conflict',
-          text: 'Someone else has updated this dashboard.',
-          text2: 'Would you still like to save this dashboard?',
-          yesText: "Save & Overwrite",
-          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: 'Conflict',
-          text: 'Dashboard with the same name exists.',
-          text2: 'Would you still like to save this dashboard?',
-          yesText: "Save & Overwrite",
-          icon: "fa-warning",
-          onConfirm: function() {
-            $scope.saveDashboard({overwrite: true});
-          }
-        });
-      }
-
-      if (err.data && err.data.status === "plugin-dashboard") {
-        err.isHandled = true;
-
-        $scope.appEvent('confirm-modal', {
-          title: 'Plugin Dashboard',
-          text: err.data.message,
-          text2: 'Your changes will be lost when you update the plugin. Use Save As to create custom version.',
-          yesText: "Overwrite",
-          icon: "fa-warning",
-          altActionText: "Save As",
-          onAltAction: function() {
-            $scope.saveDashboardAs();
-          },
-          onConfirm: function() {
-            $scope.saveDashboard({overwrite: true});
-          }
-        });
-      }
+      return dashboardSrv.saveDashboard(options);
     };
     };
 
 
     $scope.deleteDashboard = function() {
     $scope.deleteDashboard = function() {
@@ -189,16 +115,7 @@ export class DashNavCtrl {
     };
     };
 
 
     $scope.saveDashboardAs = function() {
     $scope.saveDashboardAs = function() {
-      var newScope = $rootScope.$new();
-      newScope.clone = $scope.dashboard.getSaveModelClone();
-      newScope.clone.editable = true;
-      newScope.clone.hideControls = false;
-
-      $scope.appEvent('show-modal', {
-        src: 'public/app/features/dashboard/partials/saveDashboardAs.html',
-        scope: newScope,
-        modalClass: 'modal--narrow'
-      });
+      return dashboardSrv.saveDashboardAs();
     };
     };
 
 
     $scope.viewJson = function() {
     $scope.viewJson = function() {

+ 576 - 0
public/app/features/dashboard/model.ts

@@ -0,0 +1,576 @@
+///<reference path="../../headers/common.d.ts" />
+
+import config from 'app/core/config';
+import angular from 'angular';
+import moment from 'moment';
+import _ from 'lodash';
+import $ from 'jquery';
+
+import {Emitter} from 'app/core/core';
+import {contextSrv} from 'app/core/services/context_srv';
+
+export class DashboardModel {
+  id: any;
+  title: any;
+  autoUpdate: any;
+  description: any;
+  tags: any;
+  style: any;
+  timezone: any;
+  editable: any;
+  hideControls: any;
+  sharedCrosshair: any;
+  rows: any;
+  time: any;
+  timepicker: any;
+  templating: any;
+  annotations: any;
+  refresh: any;
+  snapshot: any;
+  schemaVersion: number;
+  version: number;
+  revision: number;
+  links: any;
+  gnetId: any;
+  meta: any;
+  events: any;
+
+  constructor(data, meta) {
+    if (!data) {
+      data = {};
+    }
+
+    this.events = new Emitter();
+    this.id = data.id || null;
+    this.revision = data.revision;
+    this.title = data.title || 'No Title';
+    this.autoUpdate = data.autoUpdate;
+    this.description = data.description;
+    this.tags = data.tags || [];
+    this.style = data.style || "dark";
+    this.timezone = data.timezone || '';
+    this.editable = data.editable !== false;
+    this.hideControls = data.hideControls || false;
+    this.sharedCrosshair = data.sharedCrosshair || false;
+    this.rows = data.rows || [];
+    this.time = data.time || { from: 'now-6h', to: 'now' };
+    this.timepicker = data.timepicker || {};
+    this.templating = this.ensureListExist(data.templating);
+    this.annotations = this.ensureListExist(data.annotations);
+    this.refresh = data.refresh;
+    this.snapshot = data.snapshot;
+    this.schemaVersion = data.schemaVersion || 0;
+    this.version = data.version || 0;
+    this.links = data.links || [];
+    this.gnetId = data.gnetId || null;
+
+    this.updateSchema(data);
+    this.initMeta(meta);
+  }
+
+  private initMeta(meta) {
+    meta = meta || {};
+
+    meta.canShare = meta.canShare !== false;
+    meta.canSave = meta.canSave !== false;
+    meta.canStar = meta.canStar !== false;
+    meta.canEdit = meta.canEdit !== false;
+
+    if (!this.editable) {
+      meta.canEdit = false;
+      meta.canDelete = false;
+      meta.canSave = false;
+      this.hideControls = true;
+    }
+
+    this.meta = meta;
+  }
+
+  // cleans meta data and other non peristent state
+  getSaveModelClone() {
+    // temp remove stuff
+    var events = this.events;
+    var meta = this.meta;
+    delete this.events;
+    delete this.meta;
+
+    events.emit('prepare-save-model');
+    var copy = $.extend(true, {}, this);
+
+    // restore properties
+    this.events = events;
+    this.meta = meta;
+    return copy;
+  }
+
+  private ensureListExist(data) {
+    if (!data) { data = {}; }
+    if (!data.list) { data.list = []; }
+    return data;
+  }
+
+  getNextPanelId() {
+    var i, j, row, panel, max = 0;
+    for (i = 0; i < this.rows.length; i++) {
+      row = this.rows[i];
+      for (j = 0; j < row.panels.length; j++) {
+        panel = row.panels[j];
+        if (panel.id > max) { max = panel.id; }
+      }
+    }
+    return max + 1;
+  }
+
+  forEachPanel(callback) {
+    var i, j, row;
+    for (i = 0; i < this.rows.length; i++) {
+      row = this.rows[i];
+      for (j = 0; j < row.panels.length; j++) {
+        callback(row.panels[j], j, row, i);
+      }
+    }
+  }
+
+  getPanelById(id) {
+    for (var i = 0; i < this.rows.length; i++) {
+      var row = this.rows[i];
+      for (var j = 0; j < row.panels.length; j++) {
+        var panel = row.panels[j];
+        if (panel.id === id) {
+          return panel;
+        }
+      }
+    }
+    return null;
+  }
+
+  rowSpan(row) {
+    return _.reduce(row.panels, function(p,v) {
+      return p + v.span;
+    },0);
+  };
+
+  addPanel(panel, row) {
+    var rowSpan = this.rowSpan(row);
+    var panelCount = row.panels.length;
+    var space = (12 - rowSpan) - panel.span;
+    panel.id = this.getNextPanelId();
+
+    // try to make room of there is no space left
+    if (space <= 0) {
+      if (panelCount === 1) {
+        row.panels[0].span = 6;
+        panel.span = 6;
+      } else if (panelCount === 2) {
+        row.panels[0].span = 4;
+        row.panels[1].span = 4;
+        panel.span = 4;
+      }
+    }
+
+    row.panels.push(panel);
+  }
+
+  isSubmenuFeaturesEnabled() {
+    var visableTemplates = _.filter(this.templating.list, function(template) {
+      return template.hideVariable === undefined || template.hideVariable === false;
+    });
+
+    return visableTemplates.length > 0 || this.annotations.list.length > 0 || this.links.length > 0;
+  }
+
+  getPanelInfoById(panelId) {
+    var result: any = {};
+    _.each(this.rows, function(row) {
+      _.each(row.panels, function(panel, index) {
+        if (panel.id === panelId) {
+          result.panel = panel;
+          result.row = row;
+          result.index = index;
+        }
+      });
+    });
+
+    if (!result.panel) {
+      return null;
+    }
+
+    return result;
+  }
+
+  duplicatePanel(panel, row) {
+    var rowIndex = _.indexOf(this.rows, row);
+    var newPanel = angular.copy(panel);
+    newPanel.id = this.getNextPanelId();
+
+    delete newPanel.repeat;
+    delete newPanel.repeatIteration;
+    delete newPanel.repeatPanelId;
+    delete newPanel.scopedVars;
+
+    var currentRow = this.rows[rowIndex];
+    currentRow.panels.push(newPanel);
+    return newPanel;
+  }
+
+  formatDate(date, format) {
+    date = moment.isMoment(date) ? date : moment(date);
+    format = format || 'YYYY-MM-DD HH:mm:ss';
+    this.timezone = this.getTimezone();
+
+    return this.timezone === 'browser' ?
+      moment(date).format(format) :
+      moment.utc(date).format(format);
+  }
+
+  getRelativeTime(date) {
+    date = moment.isMoment(date) ? date : moment(date);
+
+    return this.timezone === 'browser' ?
+      moment(date).fromNow() :
+      moment.utc(date).fromNow();
+  }
+
+  getNextQueryLetter(panel) {
+    var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+    return _.find(letters, function(refId) {
+      return _.every(panel.targets, function(other) {
+        return other.refId !== refId;
+      });
+    });
+  }
+
+  isTimezoneUtc() {
+    return this.getTimezone() === 'utc';
+  }
+
+  getTimezone() {
+    return this.timezone ? this.timezone : contextSrv.user.timezone;
+  }
+
+  private updateSchema(old) {
+    var i, j, k;
+    var oldVersion = this.schemaVersion;
+    var panelUpgrades = [];
+    this.schemaVersion = 13;
+
+    if (oldVersion === this.schemaVersion) {
+      return;
+    }
+
+    // version 2 schema changes
+    if (oldVersion < 2) {
+
+      if (old.services) {
+        if (old.services.filter) {
+          this.time = old.services.filter.time;
+          this.templating.list = old.services.filter.list || [];
+        }
+      }
+
+      panelUpgrades.push(function(panel) {
+        // rename panel type
+        if (panel.type === 'graphite') {
+          panel.type = 'graph';
+        }
+
+        if (panel.type !== 'graph') {
+          return;
+        }
+
+        if (_.isBoolean(panel.legend)) { panel.legend = { show: panel.legend }; }
+
+        if (panel.grid) {
+          if (panel.grid.min) {
+            panel.grid.leftMin = panel.grid.min;
+            delete panel.grid.min;
+          }
+
+          if (panel.grid.max) {
+            panel.grid.leftMax = panel.grid.max;
+            delete panel.grid.max;
+          }
+        }
+
+        if (panel.y_format) {
+          panel.y_formats[0] = panel.y_format;
+          delete panel.y_format;
+        }
+
+        if (panel.y2_format) {
+          panel.y_formats[1] = panel.y2_format;
+          delete panel.y2_format;
+        }
+      });
+    }
+
+    // schema version 3 changes
+    if (oldVersion < 3) {
+      // ensure panel ids
+      var maxId = this.getNextPanelId();
+      panelUpgrades.push(function(panel) {
+        if (!panel.id) {
+          panel.id = maxId;
+          maxId += 1;
+        }
+      });
+    }
+
+    // schema version 4 changes
+    if (oldVersion < 4) {
+      // move aliasYAxis changes
+      panelUpgrades.push(function(panel) {
+        if (panel.type !== 'graph') { return; }
+        _.each(panel.aliasYAxis, function(value, key) {
+          panel.seriesOverrides = [{ alias: key, yaxis: value }];
+        });
+        delete panel.aliasYAxis;
+      });
+    }
+
+    if (oldVersion < 6) {
+      // move pulldowns to new schema
+      var annotations = _.find(old.pulldowns, { type: 'annotations' });
+
+      if (annotations) {
+        this.annotations = {
+          list: annotations.annotations || [],
+        };
+      }
+
+      // update template variables
+      for (i = 0 ; i < this.templating.list.length; i++) {
+        var variable = this.templating.list[i];
+        if (variable.datasource === void 0) { variable.datasource = null; }
+        if (variable.type === 'filter') { variable.type = 'query'; }
+        if (variable.type === void 0) { variable.type = 'query'; }
+        if (variable.allFormat === void 0) { variable.allFormat = 'glob'; }
+      }
+    }
+
+    if (oldVersion < 7) {
+      if (old.nav && old.nav.length) {
+        this.timepicker = old.nav[0];
+      }
+
+      // ensure query refIds
+      panelUpgrades.push(function(panel) {
+        _.each(panel.targets, function(target) {
+          if (!target.refId) {
+            target.refId = this.getNextQueryLetter(panel);
+            }
+          }.bind(this));
+        });
+      }
+
+      if (oldVersion < 8) {
+        panelUpgrades.push(function(panel) {
+          _.each(panel.targets, function(target) {
+            // update old influxdb query schema
+            if (target.fields && target.tags && target.groupBy) {
+              if (target.rawQuery) {
+                delete target.fields;
+                delete target.fill;
+              } else {
+                target.select = _.map(target.fields, function(field) {
+                  var parts = [];
+                  parts.push({type: 'field', params: [field.name]});
+                  parts.push({type: field.func, params: []});
+                  if (field.mathExpr) {
+                    parts.push({type: 'math', params: [field.mathExpr]});
+                  }
+                  if (field.asExpr) {
+                    parts.push({type: 'alias', params: [field.asExpr]});
+                  }
+                  return parts;
+                });
+                delete target.fields;
+                _.each(target.groupBy, function(part) {
+                  if (part.type === 'time' && part.interval)  {
+                    part.params = [part.interval];
+                    delete part.interval;
+                  }
+                  if (part.type === 'tag' && part.key) {
+                    part.params = [part.key];
+                    delete part.key;
+                  }
+                });
+
+                if (target.fill) {
+                  target.groupBy.push({type: 'fill', params: [target.fill]});
+                  delete target.fill;
+                }
+              }
+            }
+          });
+        });
+      }
+
+      // schema version 9 changes
+      if (oldVersion < 9) {
+        // move aliasYAxis changes
+        panelUpgrades.push(function(panel) {
+          if (panel.type !== 'singlestat' && panel.thresholds !== "") { return; }
+
+          if (panel.thresholds) {
+            var k = panel.thresholds.split(",");
+
+            if (k.length >= 3) {
+              k.shift();
+              panel.thresholds = k.join(",");
+            }
+          }
+        });
+      }
+
+      // schema version 10 changes
+      if (oldVersion < 10) {
+        // move aliasYAxis changes
+        panelUpgrades.push(function(panel) {
+          if (panel.type !== 'table') { return; }
+
+          _.each(panel.styles, function(style) {
+            if (style.thresholds && style.thresholds.length >= 3) {
+              var k = style.thresholds;
+              k.shift();
+              style.thresholds = k;
+            }
+          });
+        });
+      }
+
+      if (oldVersion < 12) {
+        // update template variables
+        _.each(this.templating.list, function(templateVariable) {
+          if (templateVariable.refresh) { templateVariable.refresh = 1; }
+          if (!templateVariable.refresh) { templateVariable.refresh = 0; }
+          if (templateVariable.hideVariable) {
+            templateVariable.hide = 2;
+          } else if (templateVariable.hideLabel) {
+            templateVariable.hide = 1;
+          } else {
+            templateVariable.hide = 0;
+          }
+        });
+      }
+
+      if (oldVersion < 12) {
+        // update graph yaxes changes
+        panelUpgrades.push(function(panel) {
+          if (panel.type !== 'graph') { return; }
+          if (!panel.grid) { return; }
+
+          if (!panel.yaxes) {
+            panel.yaxes = [
+              {
+                show: panel['y-axis'],
+                min: panel.grid.leftMin,
+                max: panel.grid.leftMax,
+                logBase: panel.grid.leftLogBase,
+                format: panel.y_formats[0],
+                label: panel.leftYAxisLabel,
+              },
+              {
+                show: panel['y-axis'],
+                min: panel.grid.rightMin,
+                max: panel.grid.rightMax,
+                logBase: panel.grid.rightLogBase,
+                format: panel.y_formats[1],
+                label: panel.rightYAxisLabel,
+              }
+            ];
+
+            panel.xaxis = {
+              show: panel['x-axis'],
+            };
+
+            delete panel.grid.leftMin;
+            delete panel.grid.leftMax;
+            delete panel.grid.leftLogBase;
+            delete panel.grid.rightMin;
+            delete panel.grid.rightMax;
+            delete panel.grid.rightLogBase;
+            delete panel.y_formats;
+            delete panel.leftYAxisLabel;
+            delete panel.rightYAxisLabel;
+            delete panel['y-axis'];
+            delete panel['x-axis'];
+          }
+        });
+      }
+
+      if (oldVersion < 13) {
+        // update graph yaxes changes
+        panelUpgrades.push(function(panel) {
+          if (panel.type !== 'graph') { return; }
+
+          panel.thresholds = [];
+          var t1: any = {}, t2: any = {};
+
+          if (panel.grid.threshold1 !== null) {
+            t1.value = panel.grid.threshold1;
+            if (panel.grid.thresholdLine) {
+              t1.line = true;
+              t1.lineColor = panel.grid.threshold1Color;
+              t1.colorMode = 'custom';
+            } else {
+              t1.fill = true;
+              t1.fillColor = panel.grid.threshold1Color;
+              t1.colorMode = 'custom';
+            }
+          }
+
+          if (panel.grid.threshold2 !== null) {
+            t2.value = panel.grid.threshold2;
+            if (panel.grid.thresholdLine) {
+              t2.line = true;
+              t2.lineColor = panel.grid.threshold2Color;
+              t2.colorMode = 'custom';
+            } else {
+              t2.fill = true;
+              t2.fillColor = panel.grid.threshold2Color;
+              t2.colorMode = 'custom';
+            }
+          }
+
+          if (_.isNumber(t1.value)) {
+            if (_.isNumber(t2.value)) {
+              if (t1.value > t2.value) {
+                t1.op = t2.op = 'lt';
+                panel.thresholds.push(t1);
+                panel.thresholds.push(t2);
+              } else {
+                t1.op = t2.op = 'gt';
+                panel.thresholds.push(t1);
+                panel.thresholds.push(t2);
+              }
+            } else {
+              t1.op = 'gt';
+              panel.thresholds.push(t1);
+            }
+          }
+
+          delete panel.grid.threshold1;
+          delete panel.grid.threshold1Color;
+          delete panel.grid.threshold2;
+          delete panel.grid.threshold2Color;
+          delete panel.grid.thresholdLine;
+        });
+      }
+
+      if (panelUpgrades.length === 0) {
+        return;
+      }
+
+      for (i = 0; i < this.rows.length; i++) {
+        var row = this.rows[i];
+        for (j = 0; j < row.panels.length; j++) {
+          for (k = 0; k < panelUpgrades.length; k++) {
+            panelUpgrades[k].call(this, row.panels[j]);
+          }
+        }
+      }
+    }
+}
+