Browse Source

Merge branch 'master' of github.com:grafana/grafana

bergquist 9 years ago
parent
commit
6c7e227d2f

+ 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,

+ 16 - 0
pkg/api/annotations.go

@@ -44,3 +44,19 @@ func GetAnnotations(c *middleware.Context) Response {
 
 
 	return Json(200, result)
 	return Json(200, result)
 }
 }
+
+func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Response {
+	repo := annotations.GetRepository()
+
+	err := repo.Delete(&annotations.DeleteParams{
+		AlertId:     cmd.PanelId,
+		DashboardId: cmd.DashboardId,
+		PanelId:     cmd.PanelId,
+	})
+
+	if err != nil {
+		return ApiError(500, "Failed to delete annotations", err)
+	}
+
+	return ApiSuccess("Annotations deleted")
+}

+ 2 - 1
pkg/api/api.go

@@ -252,7 +252,7 @@ func Register(r *macaron.Macaron) {
 
 
 		r.Group("/alerts", func() {
 		r.Group("/alerts", func() {
 			r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
 			r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
-			r.Post("/:alertId/pause", ValidateOrgAlert, bind(dtos.PauseAlertCommand{}), wrap(PauseAlert))
+			r.Post("/:alertId/pause", bind(dtos.PauseAlertCommand{}), wrap(PauseAlert))
 			r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
 			r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
 			r.Get("/", wrap(GetAlerts))
 			r.Get("/", wrap(GetAlerts))
 			r.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard))
 			r.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard))
@@ -269,6 +269,7 @@ func Register(r *macaron.Macaron) {
 		}, reqOrgAdmin)
 		}, reqOrgAdmin)
 
 
 		r.Get("/annotations", wrap(GetAnnotations))
 		r.Get("/annotations", wrap(GetAnnotations))
+		r.Post("/annotations/mass-delete", reqOrgAdmin, bind(dtos.DeleteAnnotationsCmd{}), wrap(DeleteAnnotations))
 
 
 		// error test
 		// error test
 		r.Get("/metrics/error", wrap(GenerateError))
 		r.Get("/metrics/error", wrap(GenerateError))

+ 6 - 0
pkg/api/dtos/annotations.go

@@ -15,3 +15,9 @@ type Annotation struct {
 
 
 	Data *simplejson.Json `json:"data"`
 	Data *simplejson.Json `json:"data"`
 }
 }
+
+type DeleteAnnotationsCmd struct {
+	AlertId     int64 `json:"alertId"`
+	DashboardId int64 `json:"dashboardId"`
+	PanelId     int64 `json:"panelId"`
+}

+ 2 - 0
pkg/cmd/grafana-server/main.go

@@ -102,8 +102,10 @@ func writePIDFile() {
 
 
 func listenToSystemSignals(server models.GrafanaServer) {
 func listenToSystemSignals(server models.GrafanaServer) {
 	signalChan := make(chan os.Signal, 1)
 	signalChan := make(chan os.Signal, 1)
+	ignoreChan := make(chan os.Signal, 1)
 	code := 0
 	code := 0
 
 
+	signal.Notify(ignoreChan, syscall.SIGHUP)
 	signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM)
 	signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM)
 
 
 	select {
 	select {

+ 7 - 0
pkg/services/annotations/annotations.go

@@ -5,6 +5,7 @@ import "github.com/grafana/grafana/pkg/components/simplejson"
 type Repository interface {
 type Repository interface {
 	Save(item *Item) error
 	Save(item *Item) error
 	Find(query *ItemQuery) ([]*Item, error)
 	Find(query *ItemQuery) ([]*Item, error)
+	Delete(params *DeleteParams) error
 }
 }
 
 
 type ItemQuery struct {
 type ItemQuery struct {
@@ -20,6 +21,12 @@ type ItemQuery struct {
 	Limit int64 `json:"alertId"`
 	Limit int64 `json:"alertId"`
 }
 }
 
 
+type DeleteParams struct {
+	AlertId     int64 `json:"alertId"`
+	DashboardId int64 `json:"dashboardId"`
+	PanelId     int64 `json:"panelId"`
+}
+
 var repositoryInstance Repository
 var repositoryInstance Repository
 
 
 func GetRepository() Repository {
 func GetRepository() Repository {

+ 20 - 18
pkg/services/sqlstore/alert.go

@@ -46,13 +46,23 @@ func GetAllAlertQueryHandler(query *m.GetAllAlertsQuery) error {
 	return nil
 	return nil
 }
 }
 
 
+func deleteAlertByIdInternal(alertId int64, reason string, sess *xorm.Session) error {
+	sqlog.Debug("Deleting alert", "id", alertId, "reason", reason)
+
+	if _, err := sess.Exec("DELETE FROM alert WHERE id = ?", alertId); err != nil {
+		return err
+	}
+
+	if _, err := sess.Exec("DELETE FROM annotation WHERE alert_id = ?", alertId); err != nil {
+		return err
+	}
+
+	return nil
+}
+
 func DeleteAlertById(cmd *m.DeleteAlertCommand) error {
 func DeleteAlertById(cmd *m.DeleteAlertCommand) error {
 	return inTransaction(func(sess *xorm.Session) error {
 	return inTransaction(func(sess *xorm.Session) error {
-		if _, err := sess.Exec("DELETE FROM alert WHERE id = ?", cmd.AlertId); err != nil {
-			return err
-		}
-
-		return nil
+		return deleteAlertByIdInternal(cmd.AlertId, "DeleteAlertCommand", sess)
 	})
 	})
 }
 }
 
 
@@ -110,12 +120,7 @@ func DeleteAlertDefinition(dashboardId int64, sess *xorm.Session) error {
 	sess.Where("dashboard_id = ?", dashboardId).Find(&alerts)
 	sess.Where("dashboard_id = ?", dashboardId).Find(&alerts)
 
 
 	for _, alert := range alerts {
 	for _, alert := range alerts {
-		_, err := sess.Exec("DELETE FROM alert WHERE id = ? ", alert.Id)
-		if err != nil {
-			return err
-		}
-
-		sqlog.Debug("Alert deleted (due to dashboard deletion)", "name", alert.Name, "id", alert.Id)
+		deleteAlertByIdInternal(alert.Id, "Dashboard deleted", sess)
 	}
 	}
 
 
 	return nil
 	return nil
@@ -195,12 +200,7 @@ func deleteMissingAlerts(alerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *xorm
 		}
 		}
 
 
 		if missing {
 		if missing {
-			_, err := sess.Exec("DELETE FROM alert WHERE id = ?", missingAlert.Id)
-			if err != nil {
-				return err
-			}
-
-			sqlog.Debug("Alert deleted", "name", missingAlert.Name, "id", missingAlert.Id)
+			deleteAlertByIdInternal(missingAlert.Id, "Removed from dashboard", sess)
 		}
 		}
 	}
 	}
 
 
@@ -248,7 +248,9 @@ func PauseAlertRule(cmd *m.PauseAlertCommand) error {
 	return inTransaction(func(sess *xorm.Session) error {
 	return inTransaction(func(sess *xorm.Session) error {
 		alert := m.Alert{}
 		alert := m.Alert{}
 
 
-		if has, err := sess.Id(cmd.AlertId).Get(&alert); err != nil {
+		has, err := x.Where("id = ? AND org_id=?", cmd.AlertId, cmd.OrgId).Get(&alert)
+
+		if err != nil {
 			return err
 			return err
 		} else if !has {
 		} else if !has {
 			return fmt.Errorf("Could not find alert")
 			return fmt.Errorf("Could not find alert")

+ 14 - 0
pkg/services/sqlstore/annotation.go

@@ -84,3 +84,17 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
 
 
 	return items, nil
 	return items, nil
 }
 }
+
+func (r *SqlAnnotationRepo) Delete(params *annotations.DeleteParams) error {
+	return inTransaction(func(sess *xorm.Session) error {
+
+		sql := "DELETE FROM annotation WHERE dashboard_id = ? AND panel_id = ?"
+
+		_, err := sess.Exec(sql, params.DashboardId, params.PanelId)
+		if err != nil {
+			return err
+		}
+
+		return nil
+	})
+}

+ 19 - 1
public/app/features/alerting/alert_tab_ctrl.ts

@@ -59,7 +59,7 @@ export class AlertTabCtrl {
       this.panelCtrl.render();
       this.panelCtrl.render();
     });
     });
 
 
-       // build notification model
+    // build notification model
     this.notifications = [];
     this.notifications = [];
     this.alertNotifications = [];
     this.alertNotifications = [];
     this.alertHistory = [];
     this.alertHistory = [];
@@ -352,6 +352,24 @@ export class AlertTabCtrl {
     this.evaluatorParamsChanged();
     this.evaluatorParamsChanged();
   }
   }
 
 
+  clearHistory() {
+    appEvents.emit('confirm-modal', {
+      title: 'Delete Alert History',
+      text: 'Are you sure you want to remove all history & annotations for this alert?',
+      icon: 'fa-trash',
+      yesText: 'Yes',
+      onConfirm: () => {
+        this.backendSrv.post('/api/annotations/mass-delete', {
+          dashboardId: this.panelCtrl.dashboard.id,
+          panelId: this.panel.id,
+        }).then(res => {
+          this.alertHistory = [];
+          this.panelCtrl.refresh();
+        });
+      }
+    });
+  }
+
   test() {
   test() {
     this.testing = true;
     this.testing = true;
 
 

+ 10 - 1
public/app/features/alerting/partials/alert_tab.html

@@ -125,7 +125,16 @@
 		</div>
 		</div>
 
 
 		<div class="gf-form-group" style="max-width: 720px;" ng-if="ctrl.subTabIndex === 2">
 		<div class="gf-form-group" style="max-width: 720px;" ng-if="ctrl.subTabIndex === 2">
-      <h5 class="section-heading">State history <span class="muted small">(last 50 state changes)</span></h5>
+			<button class="btn btn-mini btn-danger pull-right" ng-click="ctrl.clearHistory()"><i class="fa fa-trash"></i>&nbsp;Clear history</button>
+      <h5 class="section-heading" style="whitespace: nowrap">
+				State history <span class="muted small">(last 50 state changes)</span>
+			</h5>
+
+      <div ng-show="ctrl.alertHistory.length === 0">
+        <br>
+        <i>No state changes recorded</i>
+      </div>
+
 			<section class="card-section card-list-layout-list">
 			<section class="card-section card-list-layout-list">
 				<ol class="card-list" >
 				<ol class="card-list" >
 					<li class="card-item-wrapper" ng-repeat="ah in ctrl.alertHistory">
 					<li class="card-item-wrapper" ng-repeat="ah in ctrl.alertHistory">

+ 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]);
+          }
+        }
+      }
+    }
+}
+

+ 1 - 1
public/app/features/dashboard/specs/dashboard_srv_specs.ts

@@ -6,7 +6,7 @@ describe('dashboardSrv', function() {
   var _dashboardSrv;
   var _dashboardSrv;
 
 
   beforeEach(() => {
   beforeEach(() => {
-    _dashboardSrv = new DashboardSrv();
+    _dashboardSrv = new DashboardSrv({}, {}, {});
   });
   });
 
 
   describe('when creating new dashboard with defaults only', function() {
   describe('when creating new dashboard with defaults only', function() {

+ 1 - 1
public/app/plugins/datasource/prometheus/dashboards/prometheus_stats.json

@@ -478,7 +478,7 @@
           "steppedLine": false,
           "steppedLine": false,
           "targets": [
           "targets": [
             {
             {
-              "expr": "prometheus_evaluator_duration_milliseconds{quantile!=\"0.01\", quantile!=\"0.05\"}",
+              "expr": "prometheus_evaluator_duration_seconds{quantile!=\"0.01\", quantile!=\"0.05\"}",
               "interval": "",
               "interval": "",
               "intervalFactor": 2,
               "intervalFactor": 2,
               "legendFormat": "{{quantile}}",
               "legendFormat": "{{quantile}}",

+ 5 - 1
public/app/plugins/panel/graph/graph.ts

@@ -392,17 +392,21 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
           position: 'BOTTOM',
           position: 'BOTTOM',
           markerSize: 5,
           markerSize: 5,
         };
         };
+
         types['$__ok'] = {
         types['$__ok'] = {
           color: 'rgba(11, 237, 50, 1)',
           color: 'rgba(11, 237, 50, 1)',
           position: 'BOTTOM',
           position: 'BOTTOM',
           markerSize: 5,
           markerSize: 5,
         };
         };
-        types['$__nodata'] = {
+
+        types['$__no_data'] = {
           color: 'rgba(150, 150, 150, 1)',
           color: 'rgba(150, 150, 150, 1)',
           position: 'BOTTOM',
           position: 'BOTTOM',
           markerSize: 5,
           markerSize: 5,
         };
         };
 
 
+        types['$__execution_error'] = ['$__no_data'];
+
         for (var i = 0; i < annotations.length; i++) {
         for (var i = 0; i < annotations.length; i++) {
           var item = annotations[i];
           var item = annotations[i];
           if (item.newState) {
           if (item.newState) {

+ 9 - 4
public/app/plugins/panel/graph/graph_tooltip.js

@@ -149,8 +149,6 @@ function ($, _) {
 
 
         seriesHtml = '';
         seriesHtml = '';
 
 
-        absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat);
-
         // Dynamically reorder the hovercard for the current time point if the
         // Dynamically reorder the hovercard for the current time point if the
         // option is enabled, sort by yaxis by default.
         // option is enabled, sort by yaxis by default.
         if (panel.tooltip.sort === 2) {
         if (panel.tooltip.sort === 2) {
@@ -161,13 +159,14 @@ function ($, _) {
           seriesHoverInfo.sort(function(a, b) {
           seriesHoverInfo.sort(function(a, b) {
             return a.value - b.value;
             return a.value - b.value;
           });
           });
-        }
-        else {
+        } else {
           seriesHoverInfo.sort(function(a, b) {
           seriesHoverInfo.sort(function(a, b) {
             return a.yaxis - b.yaxis;
             return a.yaxis - b.yaxis;
           });
           });
         }
         }
 
 
+        var distance, time;
+
         for (i = 0; i < seriesHoverInfo.length; i++) {
         for (i = 0; i < seriesHoverInfo.length; i++) {
           hoverInfo = seriesHoverInfo[i];
           hoverInfo = seriesHoverInfo[i];
 
 
@@ -175,6 +174,11 @@ function ($, _) {
             continue;
             continue;
           }
           }
 
 
+          if (! distance || hoverInfo.distance < distance) {
+            distance = hoverInfo.distance;
+            time = hoverInfo.time;
+          }
+
           var highlightClass = '';
           var highlightClass = '';
           if (item && i === item.seriesIndex) {
           if (item && i === item.seriesIndex) {
             highlightClass = 'graph-tooltip-list-item--highlight';
             highlightClass = 'graph-tooltip-list-item--highlight';
@@ -190,6 +194,7 @@ function ($, _) {
           plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);
           plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);
         }
         }
 
 
+        absoluteTime = dashboard.formatDate(time, tooltipFormat);
         self.showTooltip(absoluteTime, seriesHtml, pos);
         self.showTooltip(absoluteTime, seriesHtml, pos);
       }
       }
       // single series tooltip
       // single series tooltip

+ 4 - 0
public/sass/components/edit_sidemenu.scss

@@ -4,6 +4,10 @@
   flex-direction: row;
   flex-direction: row;
 }
 }
 
 
+.edit-tab-content {
+  flex-grow: 1;
+}
+
 .edit-sidemenu-aside {
 .edit-sidemenu-aside {
   width: 16rem;
   width: 16rem;
 }
 }