Преглед изворни кода

Merge remote-tracking branch 'grafana/master' into influx-db-query2

ryan пре 8 година
родитељ
комит
8c838cff08
51 измењених фајлова са 824 додато и 230 уклоњено
  1. 1 1
      package.json
  2. 9 7
      packaging/deb/init.d/grafana-server
  3. 9 5
      packaging/rpm/init.d/grafana-server
  4. 40 0
      pkg/api/annotations.go
  5. 4 1
      pkg/api/api.go
  6. 14 0
      pkg/api/dtos/annotations.go
  7. 11 0
      pkg/services/annotations/annotations.go
  8. 11 0
      pkg/services/sqlstore/annotation.go
  9. 4 1
      pkg/services/sqlstore/migrations/annotation_mig.go
  10. 1 1
      pkg/services/sqlstore/sqlstore.go
  11. 1 1
      public/app/core/components/switch.ts
  12. 0 4
      public/app/core/services/keybindingSrv.ts
  13. 49 31
      public/app/core/services/popover_srv.ts
  14. 1 1
      public/app/features/all.js
  15. 12 0
      public/app/features/annotations/all.ts
  16. 17 0
      public/app/features/annotations/annotations_srv.ts
  17. 10 2
      public/app/features/annotations/editor_ctrl.ts
  18. 10 0
      public/app/features/annotations/event.ts
  19. 66 0
      public/app/features/annotations/event_editor.ts
  20. 117 0
      public/app/features/annotations/event_manager.ts
  21. 43 27
      public/app/features/annotations/partials/editor.html
  22. 38 0
      public/app/features/annotations/partials/event_editor.html
  23. 9 19
      public/app/features/dashboard/model.ts
  24. 65 0
      public/app/features/dashboard/partials/addAnnotationModal.html
  25. 81 0
      public/app/features/dashboard/specs/dashboard_model_specs.ts
  26. 2 2
      public/app/features/dashboard/submenu/submenu.html
  27. 1 1
      public/app/partials/dashboard.html
  28. 1 1
      public/app/plugins/datasource/cloudwatch/partials/config.html
  29. 6 4
      public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html
  30. 1 2
      public/app/plugins/datasource/grafana/partials/annotations.editor.html
  31. 7 4
      public/app/plugins/datasource/graphite/partials/annotations.editor.html
  32. 1 1
      public/app/plugins/datasource/graphite/partials/query.editor.html
  33. 47 17
      public/app/plugins/datasource/graphite/query_ctrl.ts
  34. 20 0
      public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts
  35. 1 2
      public/app/plugins/datasource/influxdb/partials/annotations.editor.html
  36. 30 60
      public/app/plugins/panel/graph/graph.ts
  37. 1 0
      public/app/plugins/panel/graph/legend.js
  38. 1 1
      public/app/plugins/panel/graph/specs/threshold_manager_specs.ts
  39. 1 1
      public/app/plugins/panel/graph/threshold_manager.ts
  40. 3 2
      public/sass/_variables.dark.scss
  41. 1 0
      public/sass/_variables.light.scss
  42. 13 9
      public/sass/components/_drop.scss
  43. 0 1
      public/sass/components/_modals.scss
  44. 6 0
      public/sass/components/_submenu.scss
  45. 22 17
      public/sass/mixins/_drop_element.scss
  46. 4 0
      public/vendor/flot/jquery.flot.js
  47. 6 2
      public/vendor/flot/jquery.flot.selection.js
  48. 2 1
      tasks/options/exec.js
  49. 11 0
      tasks/options/tslint.js
  50. 13 1
      tasks/options/watch.js
  51. 0 0
      tasks/tslint.js

+ 1 - 1
package.json

@@ -76,7 +76,7 @@
     "systemjs-builder": "^0.15.34",
     "systemjs-builder": "^0.15.34",
     "tether": "^1.4.0",
     "tether": "^1.4.0",
     "tether-drop": "https://github.com/torkelo/drop",
     "tether-drop": "https://github.com/torkelo/drop",
-    "tslint": "^4.0.2",
+    "tslint": "^4.5.1",
     "typescript": "^2.1.4",
     "typescript": "^2.1.4",
     "virtual-scroll": "^1.1.1"
     "virtual-scroll": "^1.1.1"
   }
   }

+ 9 - 7
packaging/deb/init.d/grafana-server

@@ -37,14 +37,8 @@ MAX_OPEN_FILES=10000
 PID_FILE=/var/run/$NAME.pid
 PID_FILE=/var/run/$NAME.pid
 DAEMON=/usr/sbin/$NAME
 DAEMON=/usr/sbin/$NAME
 
 
-
 umask 0027
 umask 0027
 
 
-if [ `id -u` -ne 0 ]; then
-	echo "You need root privileges to run this script"
-	exit 4
-fi
-
 if [ ! -x $DAEMON ]; then
 if [ ! -x $DAEMON ]; then
   echo "Program not installed or not executable"
   echo "Program not installed or not executable"
   exit 5
   exit 5
@@ -63,9 +57,16 @@ fi
 
 
 DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
 DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
 
 
+function checkUser() {
+  if [ `id -u` -ne 0 ]; then
+  	echo "You need root privileges to run this script"
+  	exit 4
+  fi
+}
+
 case "$1" in
 case "$1" in
   start)
   start)
-
+  checkUser
 	log_daemon_msg "Starting $DESC"
 	log_daemon_msg "Starting $DESC"
 
 
 	pid=`pidofproc -p $PID_FILE grafana`
 	pid=`pidofproc -p $PID_FILE grafana`
@@ -112,6 +113,7 @@ case "$1" in
   log_end_msg $return
   log_end_msg $return
 	;;
 	;;
   stop)
   stop)
+  checkUser
 	log_daemon_msg "Stopping $DESC"
 	log_daemon_msg "Stopping $DESC"
 
 
 	if [ -f "$PID_FILE" ]; then
 	if [ -f "$PID_FILE" ]; then

+ 9 - 5
packaging/rpm/init.d/grafana-server

@@ -36,11 +36,6 @@ MAX_OPEN_FILES=10000
 PID_FILE=/var/run/$NAME.pid
 PID_FILE=/var/run/$NAME.pid
 DAEMON=/usr/sbin/$NAME
 DAEMON=/usr/sbin/$NAME
 
 
-if [ `id -u` -ne 0 ]; then
-  echo "You need root privileges to run this script"
-  exit 4
-fi
-
 if [ ! -x $DAEMON ]; then
 if [ ! -x $DAEMON ]; then
   echo "Program not installed or not executable"
   echo "Program not installed or not executable"
   exit 5
   exit 5
@@ -70,8 +65,16 @@ function isRunning() {
   status -p $PID_FILE $NAME > /dev/null 2>&1
   status -p $PID_FILE $NAME > /dev/null 2>&1
 }
 }
 
 
+function checkUser() {
+  if [ `id -u` -ne 0 ]; then
+    echo "You need root privileges to run this script"
+    exit 4
+  fi
+}
+
 case "$1" in
 case "$1" in
   start)
   start)
+    checkUser
     isRunning
     isRunning
     if [ $? -eq 0 ]; then
     if [ $? -eq 0 ]; then
       echo "Already running."
       echo "Already running."
@@ -115,6 +118,7 @@ case "$1" in
     exit $return
     exit $return
     ;;
     ;;
   stop)
   stop)
+    checkUser
     echo -n "Stopping $DESC: ..."
     echo -n "Stopping $DESC: ..."
 
 
     if [ -f "$PID_FILE" ]; then
     if [ -f "$PID_FILE" ]; then

+ 40 - 0
pkg/api/annotations.go

@@ -39,12 +39,52 @@ func GetAnnotations(c *middleware.Context) Response {
 			Text:      item.Text,
 			Text:      item.Text,
 			Metric:    item.Metric,
 			Metric:    item.Metric,
 			Title:     item.Title,
 			Title:     item.Title,
+			PanelId:   item.PanelId,
+			RegionId:  item.RegionId,
 		})
 		})
 	}
 	}
 
 
 	return Json(200, result)
 	return Json(200, result)
 }
 }
 
 
+func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response {
+	repo := annotations.GetRepository()
+
+	item := annotations.Item{
+		OrgId:       c.OrgId,
+		DashboardId: cmd.DashboardId,
+		PanelId:     cmd.PanelId,
+		Epoch:       cmd.Time / 1000,
+		Title:       cmd.Title,
+		Text:        cmd.Text,
+		CategoryId:  cmd.CategoryId,
+		NewState:    cmd.FillColor,
+		Type:        annotations.EventType,
+	}
+
+	if err := repo.Save(&item); err != nil {
+		return ApiError(500, "Failed to save annotation", err)
+	}
+
+	// handle regions
+	if cmd.IsRegion {
+		item.RegionId = item.Id
+
+		if err := repo.Update(&item); err != nil {
+			return ApiError(500, "Failed set regionId on annotation", err)
+		}
+
+		item.Id = 0
+		item.Epoch = cmd.TimeEnd
+
+		if err := repo.Save(&item); err != nil {
+			return ApiError(500, "Failed save annotation for region end time", err)
+		}
+	}
+
+	return ApiSuccess("Annotation added")
+}
+
 func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Response {
 func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Response {
 	repo := annotations.GetRepository()
 	repo := annotations.GetRepository()
 
 

+ 4 - 1
pkg/api/api.go

@@ -277,7 +277,10 @@ func (hs *HttpServer) registerRoutes() {
 		}, reqEditorRole)
 		}, reqEditorRole)
 
 
 		r.Get("/annotations", wrap(GetAnnotations))
 		r.Get("/annotations", wrap(GetAnnotations))
-		r.Post("/annotations/mass-delete", reqOrgAdmin, bind(dtos.DeleteAnnotationsCmd{}), wrap(DeleteAnnotations))
+
+		r.Group("/annotations", func() {
+			r.Post("/", bind(dtos.PostAnnotationsCmd{}), wrap(PostAnnotation))
+		}, reqEditorRole)
 
 
 		// error test
 		// error test
 		r.Get("/metrics/error", wrap(GenerateError))
 		r.Get("/metrics/error", wrap(GenerateError))

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

@@ -12,10 +12,24 @@ type Annotation struct {
 	Title       string `json:"title"`
 	Title       string `json:"title"`
 	Text        string `json:"text"`
 	Text        string `json:"text"`
 	Metric      string `json:"metric"`
 	Metric      string `json:"metric"`
+	RegionId    int64  `json:"regionId"`
 
 
 	Data *simplejson.Json `json:"data"`
 	Data *simplejson.Json `json:"data"`
 }
 }
 
 
+type PostAnnotationsCmd struct {
+	DashboardId int64  `json:"dashboardId"`
+	PanelId     int64  `json:"panelId"`
+	CategoryId  int64  `json:"categoryId"`
+	Time        int64  `json:"time"`
+	Title       string `json:"title"`
+	Text        string `json:"text"`
+
+	FillColor string `json:"fillColor"`
+	IsRegion  bool   `json:"isRegion"`
+	TimeEnd   int64  `json:"timeEnd"`
+}
+
 type DeleteAnnotationsCmd struct {
 type DeleteAnnotationsCmd struct {
 	AlertId     int64 `json:"alertId"`
 	AlertId     int64 `json:"alertId"`
 	DashboardId int64 `json:"dashboardId"`
 	DashboardId int64 `json:"dashboardId"`

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

@@ -4,6 +4,7 @@ import "github.com/grafana/grafana/pkg/components/simplejson"
 
 
 type Repository interface {
 type Repository interface {
 	Save(item *Item) error
 	Save(item *Item) error
+	Update(item *Item) error
 	Find(query *ItemQuery) ([]*Item, error)
 	Find(query *ItemQuery) ([]*Item, error)
 	Delete(params *DeleteParams) error
 	Delete(params *DeleteParams) error
 }
 }
@@ -21,6 +22,14 @@ type ItemQuery struct {
 	Limit int64 `json:"limit"`
 	Limit int64 `json:"limit"`
 }
 }
 
 
+type PostParams struct {
+	DashboardId int64  `json:"dashboardId"`
+	PanelId     int64  `json:"panelId"`
+	Epoch       int64  `json:"epoch"`
+	Title       string `json:"title"`
+	Text        string `json:"text"`
+}
+
 type DeleteParams struct {
 type DeleteParams struct {
 	AlertId     int64 `json:"alertId"`
 	AlertId     int64 `json:"alertId"`
 	DashboardId int64 `json:"dashboardId"`
 	DashboardId int64 `json:"dashboardId"`
@@ -41,6 +50,7 @@ type ItemType string
 
 
 const (
 const (
 	AlertType ItemType = "alert"
 	AlertType ItemType = "alert"
+	EventType ItemType = "event"
 )
 )
 
 
 type Item struct {
 type Item struct {
@@ -49,6 +59,7 @@ type Item struct {
 	DashboardId int64    `json:"dashboardId"`
 	DashboardId int64    `json:"dashboardId"`
 	PanelId     int64    `json:"panelId"`
 	PanelId     int64    `json:"panelId"`
 	CategoryId  int64    `json:"categoryId"`
 	CategoryId  int64    `json:"categoryId"`
+	RegionId    int64    `json:"regionId"`
 	Type        ItemType `json:"type"`
 	Type        ItemType `json:"type"`
 	Title       string   `json:"title"`
 	Title       string   `json:"title"`
 	Text        string   `json:"text"`
 	Text        string   `json:"text"`

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

@@ -23,6 +23,17 @@ func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
 	})
 	})
 }
 }
 
 
+func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
+	return inTransaction(func(sess *xorm.Session) error {
+
+		if _, err := sess.Table("annotation").Id(item.Id).Update(item); err != nil {
+			return err
+		}
+
+		return nil
+	})
+}
+
 func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.Item, error) {
 func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.Item, error) {
 	var sql bytes.Buffer
 	var sql bytes.Buffer
 	params := make([]interface{}, 0)
 	params := make([]interface{}, 0)

+ 4 - 1
pkg/services/sqlstore/migrations/annotation_mig.go

@@ -35,7 +35,6 @@ func addAnnotationMig(mg *Migrator) {
 	}
 	}
 
 
 	mg.AddMigration("Drop old annotation table v4", NewDropTableMigration("annotation"))
 	mg.AddMigration("Drop old annotation table v4", NewDropTableMigration("annotation"))
-
 	mg.AddMigration("create annotation table v5", NewAddTableMigration(table))
 	mg.AddMigration("create annotation table v5", NewAddTableMigration(table))
 
 
 	// create indices
 	// create indices
@@ -54,4 +53,8 @@ func addAnnotationMig(mg *Migrator) {
 		{Name: "new_state", Type: DB_NVarchar, Length: 25, Nullable: false},
 		{Name: "new_state", Type: DB_NVarchar, Length: 25, Nullable: false},
 		{Name: "data", Type: DB_Text, Nullable: false},
 		{Name: "data", Type: DB_Text, Nullable: false},
 	}))
 	}))
+
+	mg.AddMigration("Add column region_id to annotation table", NewAddColumnMigration(table, &Column{
+		Name: "region_id", Type: DB_BigInt, Nullable: true, Default: "0",
+	}))
 }
 }

+ 1 - 1
pkg/services/sqlstore/sqlstore.go

@@ -98,8 +98,8 @@ func SetEngine(engine *xorm.Engine) (err error) {
 		return fmt.Errorf("Sqlstore::Migration failed err: %v\n", err)
 		return fmt.Errorf("Sqlstore::Migration failed err: %v\n", err)
 	}
 	}
 
 
+	// Init repo instances
 	annotations.SetRepository(&SqlAnnotationRepo{})
 	annotations.SetRepository(&SqlAnnotationRepo{})
-
 	return nil
 	return nil
 }
 }
 
 

+ 1 - 1
public/app/core/components/switch.ts

@@ -9,7 +9,7 @@ import Drop from 'tether-drop';
 var template = `
 var template = `
 <label for="check-{{ctrl.id}}" class="gf-form-label {{ctrl.labelClass}} pointer">
 <label for="check-{{ctrl.id}}" class="gf-form-label {{ctrl.labelClass}} pointer">
   {{ctrl.label}}
   {{ctrl.label}}
-  <info-popover mode="right-normal" ng-if="ctrl.tooltip">
+  <info-popover mode="right-normal" ng-if="ctrl.tooltip" position="top center">
     {{ctrl.tooltip}}
     {{ctrl.tooltip}}
   </info-popover>
   </info-popover>
 </label>
 </label>

+ 0 - 4
public/app/core/services/keybindingSrv.ts

@@ -83,10 +83,6 @@ export class KeybindingSrv {
   }
   }
 
 
   setupDashboardBindings(scope, dashboard) {
   setupDashboardBindings(scope, dashboard) {
-    // this.bind('b', () => {
-    //   dashboard.toggleEditMode();
-    // });
-
     this.bind('mod+o', () => {
     this.bind('mod+o', () => {
       dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3;
       dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3;
       appEvents.emit('graph-hover-clear');
       appEvents.emit('graph-hover-clear');

+ 49 - 31
public/app/core/services/popover_srv.ts

@@ -7,51 +7,69 @@ import coreModule from 'app/core/core_module';
 import Drop from 'tether-drop';
 import Drop from 'tether-drop';
 
 
 /** @ngInject **/
 /** @ngInject **/
-function popoverSrv($compile, $rootScope) {
+function popoverSrv($compile, $rootScope, $timeout) {
+  let openDrop = null;
+
+  this.close = function() {
+    if (openDrop) {
+      openDrop.close();
+    }
+  };
 
 
   this.show = function(options) {
   this.show = function(options) {
-    var popoverScope = _.extend($rootScope.$new(true), options.model);
+    if (openDrop) {
+      openDrop.close();
+      openDrop = null;
+    }
+
+    var scope = _.extend($rootScope.$new(true), options.model);
     var drop;
     var drop;
 
 
-    function destroyDrop() {
-      setTimeout(function() {
+    var cleanUp = () => {
+      setTimeout(() => {
+        scope.$destroy();
+
         if (drop.tether) {
         if (drop.tether) {
           drop.destroy();
           drop.destroy();
         }
         }
+
+        if (options.onClose) {
+          options.onClose();
+        }
       });
       });
-    }
 
 
-    popoverScope.dismiss = function() {
-      popoverScope.$destroy();
-      destroyDrop();
+      openDrop = null;
+    };
+
+    scope.dismiss = () => {
+      drop.close();
     };
     };
 
 
     var contentElement = document.createElement('div');
     var contentElement = document.createElement('div');
     contentElement.innerHTML = options.template;
     contentElement.innerHTML = options.template;
 
 
-    $compile(contentElement)(popoverScope);
-
-    drop = new Drop({
-      target: options.element,
-      content: contentElement,
-      position: options.position,
-      classes: 'drop-popover',
-      openOn: options.openOn || 'hover',
-      hoverCloseDelay: 200,
-      tetherOptions: {
-        constraints: [{to: 'window', pin: true, attachment: "both"}]
-      }
-    });
-
-    drop.on('close', () => {
-      popoverScope.dismiss({fromDropClose: true});
-      destroyDrop();
-      if (options.onClose) {
-        options.onClose();
-      }
-    });
-
-    setTimeout(() => { drop.open(); }, 10);
+    $compile(contentElement)(scope);
+
+    $timeout(() => {
+      drop = new Drop({
+        target: options.element,
+        content: contentElement,
+        position: options.position,
+        classes: options.classNames || 'drop-popover',
+        openOn: options.openOn,
+        hoverCloseDelay: 200,
+        tetherOptions: {
+          constraints: [{to: 'scrollParent', attachment: "none both"}]
+        }
+      });
+
+      drop.on('close', () => {
+        cleanUp();
+      });
+
+      openDrop = drop;
+      openDrop.open();
+    }, 100);
   };
   };
 }
 }
 
 

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

@@ -1,7 +1,7 @@
 define([
 define([
   './panellinks/module',
   './panellinks/module',
   './dashlinks/module',
   './dashlinks/module',
-  './annotations/annotations_srv',
+  './annotations/all',
   './templating/all',
   './templating/all',
   './dashboard/all',
   './dashboard/all',
   './playlist/all',
   './playlist/all',

+ 12 - 0
public/app/features/annotations/all.ts

@@ -0,0 +1,12 @@
+
+import {AnnotationsSrv} from './annotations_srv';
+import {eventEditor} from './event_editor';
+import {EventManager} from './event_manager';
+import {AnnotationEvent} from './event';
+
+export {
+  AnnotationsSrv,
+  eventEditor,
+  EventManager,
+  AnnotationEvent,
+};

+ 17 - 0
public/app/features/annotations/annotations_srv.ts

@@ -36,6 +36,18 @@ export class AnnotationsSrv {
       // combine the annotations and flatten results
       // combine the annotations and flatten results
       var annotations = _.flattenDeep([results[0], results[1]]);
       var annotations = _.flattenDeep([results[0], results[1]]);
 
 
+      // filter out annotations that do not belong to requesting panel
+      annotations = _.filter(annotations, item => {
+        // shownIn === 1 requires annotation matching panel id
+        if (item.source.showIn === 1) {
+          if (item.panelId && options.panel.id === item.panelId) {
+            return true;
+          }
+          return false;
+        }
+        return true;
+      });
+
       // look for alert state for this panel
       // look for alert state for this panel
       var alertState = _.find(results[2], {panelId: options.panel.id});
       var alertState = _.find(results[2], {panelId: options.panel.id});
 
 
@@ -126,6 +138,11 @@ export class AnnotationsSrv {
     return this.globalAnnotationsPromise;
     return this.globalAnnotationsPromise;
   }
   }
 
 
+  saveAnnotationEvent(annotation) {
+    this.globalAnnotationsPromise = null;
+    return this.backendSrv.post('/api/annotations', annotation);
+  }
+
   translateQueryResult(annotation, results) {
   translateQueryResult(annotation, results) {
     for (var item of results) {
     for (var item of results) {
       item.source = annotation;
       item.source = annotation;

+ 10 - 2
public/app/features/annotations/editor_ctrl.ts

@@ -17,9 +17,16 @@ export class AnnotationsEditorCtrl {
     name: '',
     name: '',
     datasource: null,
     datasource: null,
     iconColor: 'rgba(255, 96, 96, 1)',
     iconColor: 'rgba(255, 96, 96, 1)',
-    enable: true
+    enable: true,
+    showIn: 0,
+    hide: false,
   };
   };
 
 
+  showOptions: any = [
+    {text: 'All Panels', value: 0},
+    {text: 'Specific Panels', value: 1},
+  ];
+
   /** @ngInject */
   /** @ngInject */
   constructor(private $scope, private datasourceSrv) {
   constructor(private $scope, private datasourceSrv) {
     $scope.ctrl = this;
     $scope.ctrl = this;
@@ -44,6 +51,7 @@ export class AnnotationsEditorCtrl {
 
 
   edit(annotation) {
   edit(annotation) {
     this.currentAnnotation = annotation;
     this.currentAnnotation = annotation;
+    this.currentAnnotation.showIn = this.currentAnnotation.showIn || 0;
     this.currentIsNew = false;
     this.currentIsNew = false;
     this.datasourceChanged();
     this.datasourceChanged();
     this.mode = 'edit';
     this.mode = 'edit';
@@ -74,7 +82,7 @@ export class AnnotationsEditorCtrl {
   removeAnnotation(annotation) {
   removeAnnotation(annotation) {
     var index = _.indexOf(this.annotations, annotation);
     var index = _.indexOf(this.annotations, annotation);
     this.annotations.splice(index, 1);
     this.annotations.splice(index, 1);
-    this.$scope.updateSubmenuVisibility();
+    this.$scope.dashboard.updateSubmenuVisibility();
     this.$scope.broadcastRefresh();
     this.$scope.broadcastRefresh();
   }
   }
 }
 }

+ 10 - 0
public/app/features/annotations/event.ts

@@ -0,0 +1,10 @@
+
+export class AnnotationEvent {
+  dashboardId: number;
+  panelId: number;
+  time: any;
+  timeEnd: any;
+  isRegion: boolean;
+  title: string;
+  text: string;
+}

+ 66 - 0
public/app/features/annotations/event_editor.ts

@@ -0,0 +1,66 @@
+///<reference path="../../headers/common.d.ts" />
+
+import _ from 'lodash';
+import moment from 'moment';
+import {coreModule} from 'app/core/core';
+import {MetricsPanelCtrl} from 'app/plugins/sdk';
+import {AnnotationEvent} from './event';
+
+export class EventEditorCtrl {
+  panelCtrl: MetricsPanelCtrl;
+  event: AnnotationEvent;
+  timeRange: {from: number, to: number};
+  form: any;
+  close: any;
+
+  /** @ngInject **/
+  constructor(private annotationsSrv) {
+    this.event.panelId = this.panelCtrl.panel.id;
+    this.event.dashboardId = this.panelCtrl.dashboard.id;
+  }
+
+  save() {
+    if (!this.form.$valid) {
+      return;
+    }
+
+    let saveModel = _.cloneDeep(this.event);
+    saveModel.time = saveModel.time.valueOf();
+    saveModel.timeEnd = 0;
+
+    if (saveModel.isRegion) {
+      saveModel.timeEnd = saveModel.timeEnd.valueOf();
+
+      if (saveModel.timeEnd < saveModel.time) {
+        console.log('invalid time');
+        return;
+      }
+    }
+
+    this.annotationsSrv.saveAnnotationEvent(saveModel).then(() => {
+      this.panelCtrl.refresh();
+      this.close();
+    });
+  }
+
+  timeChanged() {
+    this.panelCtrl.render();
+  }
+}
+
+export function eventEditor() {
+  return {
+    restrict: 'E',
+    controller: EventEditorCtrl,
+    bindToController: true,
+    controllerAs: 'ctrl',
+    templateUrl: 'public/app/features/annotations/partials/event_editor.html',
+    scope: {
+      "panelCtrl": "=",
+      "event": "=",
+      "close": "&",
+    }
+  };
+}
+
+coreModule.directive('eventEditor', eventEditor);

+ 117 - 0
public/app/features/annotations/event_manager.ts

@@ -0,0 +1,117 @@
+import _ from 'lodash';
+import moment from 'moment';
+import {MetricsPanelCtrl} from 'app/plugins/sdk';
+import {AnnotationEvent} from './event';
+
+export class EventManager {
+  event: AnnotationEvent;
+
+  constructor(private panelCtrl: MetricsPanelCtrl, private elem, private popoverSrv) {
+  }
+
+  editorClosed() {
+    console.log('editorClosed');
+    this.event = null;
+    this.panelCtrl.render();
+  }
+
+  updateTime(range) {
+    let newEvent = true;
+
+    if (this.event) {
+      newEvent = false;
+    } else {
+      // init new event
+      this.event = new AnnotationEvent();
+      this.event.dashboardId = this.panelCtrl.dashboard.id;
+      this.event.panelId = this.panelCtrl.panel.id;
+    }
+
+    // update time
+    this.event.time = moment(range.from);
+    this.event.isRegion = false;
+    if (range.to) {
+      this.event.timeEnd = moment(range.to);
+      this.event.isRegion = true;
+    }
+
+    // newEvent means the editor is not visible
+    if (!newEvent) {
+      this.panelCtrl.render();
+      return;
+    }
+
+    this.popoverSrv.show({
+      element: this.elem[0],
+      classNames: 'drop-popover drop-popover--form',
+      position: 'bottom center',
+      openOn: null,
+      template: '<event-editor panel-ctrl="panelCtrl" event="event" close="dismiss()"></event-editor>',
+      onClose: this.editorClosed.bind(this),
+      model: {
+        event: this.event,
+        panelCtrl: this.panelCtrl,
+      },
+    });
+
+    this.panelCtrl.render();
+  }
+
+  addFlotEvents(annotations, flotOptions) {
+    if (!this.event || annotations.length === 0) {
+      return;
+    }
+
+    var types = {
+      '$__alerting': {
+        color: 'rgba(237, 46, 24, 1)',
+        position: 'BOTTOM',
+        markerSize: 5,
+      },
+      '$__ok': {
+        color: 'rgba(11, 237, 50, 1)',
+        position: 'BOTTOM',
+        markerSize: 5,
+      },
+      '$__no_data': {
+        color: 'rgba(150, 150, 150, 1)',
+        position: 'BOTTOM',
+        markerSize: 5,
+      },
+    };
+
+    if (this.event) {
+      annotations = [
+        {
+          min: this.event.time.valueOf(),
+          title: this.event.title,
+          text: this.event.text,
+          eventType: '$__alerting',
+        }
+      ];
+    } else {
+      // annotations from query
+      for (var i = 0; i < annotations.length; i++) {
+        var item = annotations[i];
+        if (item.newState) {
+          item.eventType = '$__' + item.newState;
+          continue;
+        }
+
+        if (!types[item.source.name]) {
+          types[item.source.name] = {
+            color: item.source.iconColor,
+            position: 'BOTTOM',
+            markerSize: 5,
+          };
+        }
+      }
+    }
+
+    flotOptions.events = {
+      levels: _.keys(types).length + 1,
+      data: annotations,
+      types: types,
+    };
+  }
+}

+ 43 - 27
public/app/features/annotations/partials/editor.html

@@ -7,16 +7,16 @@
 		<ul class="gf-tabs">
 		<ul class="gf-tabs">
 			<li class="gf-tabs-item" >
 			<li class="gf-tabs-item" >
 				<a class="gf-tabs-link" ng-click="ctrl.mode = 'list';" ng-class="{active: ctrl.mode === 'list'}">
 				<a class="gf-tabs-link" ng-click="ctrl.mode = 'list';" ng-class="{active: ctrl.mode === 'list'}">
-					List
+					Queries
 				</a>
 				</a>
 			</li>
 			</li>
 			<li class="gf-tabs-item" ng-show="ctrl.mode === 'edit'">
 			<li class="gf-tabs-item" ng-show="ctrl.mode === 'edit'">
 				<a class="gf-tabs-link" ng-class="{active: ctrl.mode === 'edit'}">
 				<a class="gf-tabs-link" ng-class="{active: ctrl.mode === 'edit'}">
-					{{currentAnnotation.name}}
+					Edit Query
 				</a>
 				</a>
 			</li>
 			</li>
 			<li class="gf-tabs-item" ng-show="ctrl.mode === 'new'">
 			<li class="gf-tabs-item" ng-show="ctrl.mode === 'new'">
-				<span class="active gf-tabs-link">New</span>
+				<span class="active gf-tabs-link">New Query</span>
 			</li>
 			</li>
 		</ul>
 		</ul>
 
 
@@ -62,37 +62,53 @@
 
 
 		<div class="annotations-basic-settings" ng-if="ctrl.mode === 'edit' || ctrl.mode === 'new'">
 		<div class="annotations-basic-settings" ng-if="ctrl.mode === 'edit' || ctrl.mode === 'new'">
 			<div class="gf-form-group">
 			<div class="gf-form-group">
-				<div class="gf-form-inline">
-					<div class="gf-form gf-size-max-xxl">
-						<span class="gf-form-label">Name</span>
-						<input type="text" class="gf-form-input" ng-model='ctrl.currentAnnotation.name' placeholder="name"></input>
-					</div>
-					<div class="gf-form">
-						<span class="gf-form-label max-width-10">Datasource</span>
-						<div class="gf-form-select-wrapper">
-							<select class="gf-form-input gf-size-auto" ng-model="ctrl.currentAnnotation.datasource" ng-options="f.name as f.name for f in ctrl.datasources" ng-change="ctrl.datasourceChanged()"></select>
+				<h5 class="section-heading">Options</h5>
+					<div class="gf-form-inline">
+						<div class="gf-form">
+							<span class="gf-form-label width-7">Name</span>
+							<input type="text" class="gf-form-input width-12" ng-model='ctrl.currentAnnotation.name' placeholder="name"></input>
+						</div>
+						<div class="gf-form">
+							<span class="gf-form-label width-9">Data source</span>
+							<div class="gf-form-select-wrapper width-12">
+								<select class="gf-form-input" ng-model="ctrl.currentAnnotation.datasource" ng-options="f.name as f.name for f in ctrl.datasources" ng-change="ctrl.datasourceChanged()"></select>
+							</div>
 						</div>
 						</div>
 					</div>
 					</div>
-					<div class="gf-form">
-						<label class="gf-form-label">
-							<span>Color</span>
-						</label>
-						<spectrum-picker class="gf-form-input" ng-model="ctrl.currentAnnotation.iconColor"></spectrum-picker>
+					<div class="gf-form-group">
+						<div class="gf-form-inline">
+							<!-- <div class="gf&#45;form"> -->
+							<!-- 	<span class="gf&#45;form&#45;label width&#45;7">Show in</span> -->
+							<!-- 	<div class="gf&#45;form&#45;select&#45;wrapper width&#45;12"> -->
+							<!-- 		<select class="gf&#45;form&#45;input" ng&#45;model="ctrl.currentAnnotation.showIn" ng&#45;options="f.value as f.text for f in ctrl.showOptions"></select> -->
+							<!-- 	</div> -->
+							<!-- </div> -->
+							<gf-form-switch class="gf-form"
+															label="Hide toggle"
+															tooltip="Hides the annotation query toggle from showing at the top of the dashboard"
+															checked="ctrl.currentAnnotation.hide"
+															label-class="width-7">
+							</gf-form-switch>
+						</div>
+						<div class="gf-form">
+							<label class="gf-form-label width-7">Color</label>
+							<spectrum-picker class="gf-form-input width-3" ng-model="ctrl.currentAnnotation.iconColor"></spectrum-picker>
+						</div>
 					</div>
 					</div>
 				</div>
 				</div>
-			</div>
 
 
-			<rebuild-on-change property="ctrl.currentDatasource">
-				<plugin-component type="annotations-query-ctrl">
-				</plugin-component>
-			</rebuild-on-change>
+				<h5 class="section-heading">Query</h5>
+				<rebuild-on-change property="ctrl.currentDatasource">
+					<plugin-component type="annotations-query-ctrl">
+					</plugin-component>
+				</rebuild-on-change>
 
 
-			<div class="gf-form">
-				<div class="gf-form-button-row p-y-0">
-					<button ng-show="ctrl.mode === 'new'" type="button" class="btn gf-form-button btn-success" ng-click="ctrl.add()">Add</button>
-					<button ng-show="ctrl.mode === 'edit'" type="button" class="btn btn-success pull-left" ng-click="ctrl.update()">Update</button>
+				<div class="gf-form">
+					<div class="gf-form-button-row p-y-0">
+						<button ng-show="ctrl.mode === 'new'" type="button" class="btn gf-form-button btn-success" ng-click="ctrl.add()">Add</button>
+						<button ng-show="ctrl.mode === 'edit'" type="button" class="btn btn-success pull-left" ng-click="ctrl.update()">Update</button>
+					</div>
 				</div>
 				</div>
-			</div>
 		</div>
 		</div>
 
 
 	</div>
 	</div>

+ 38 - 0
public/app/features/annotations/partials/event_editor.html

@@ -0,0 +1,38 @@
+
+<h5 class="section-heading text-center">Add annotation</h5>
+
+<form name="ctrl.form" class="text-center">
+	<div style="display: inline-block">
+		<div class="gf-form">
+			<span class="gf-form-label width-7">Title</span>
+			<input type="text" ng-model="ctrl.event.title" class="gf-form-input max-width-20" required>
+		</div>
+		<!-- single event -->
+		<div ng-if="!ctrl.event.isRegion">
+      <div class="gf-form">
+        <span class="gf-form-label width-7">Time</span>
+        <input type="text" ng-model="ctrl.event.time" class="gf-form-input max-width-20" input-datetime required ng-change="ctrl.timeChanged()">
+      </div>
+    </div>
+    <!-- region event -->
+    <div ng-if="ctrl.event.isRegion">
+      <div class="gf-form">
+        <span class="gf-form-label width-7">Start</span>
+        <input type="text" ng-model="ctrl.event.time" class="gf-form-input max-width-20" input-datetime required ng-change="ctrl.timeChanged()">
+      </div>
+      <div class="gf-form">
+        <span class="gf-form-label width-7">End</span>
+        <input type="text" ng-model="ctrl.event.timeEnd" class="gf-form-input max-width-20" input-datetime required ng-change="ctrl.timeChanged()">
+      </div>
+    </div>
+    <div class="gf-form gf-form--v-stretch">
+      <span class="gf-form-label width-7">Description</span>
+      <textarea class="gf-form-input width-20" rows="3" ng-model="ctrl.event.text"  placeholder="Event description"></textarea>
+    </div>
+
+    <div class="gf-form-button-row">
+      <button type="submit" class="btn gf-form-btn btn-success" ng-click="ctrl.save()">Save</button>
+			<a class="btn-text" ng-click="ctrl.close();">Cancel</a>
+    </div>
+  </div>
+</form>

+ 9 - 19
public/app/features/dashboard/model.ts

@@ -193,32 +193,22 @@ export class DashboardModel {
     });
     });
   }
   }
 
 
-  toggleEditMode() {
-    if (!this.meta.canEdit) {
-      console.log('Not allowed to edit dashboard');
-      return;
-    }
-
-    this.editMode = !this.editMode;
-    this.updateSubmenuVisibility();
-    this.events.emit('edit-mode-changed', this.editMode);
-  }
-
   setPanelFocus(id) {
   setPanelFocus(id) {
     this.meta.focusPanelId = id;
     this.meta.focusPanelId = id;
   }
   }
 
 
   updateSubmenuVisibility() {
   updateSubmenuVisibility() {
-    if (this.editMode) {
-      this.meta.submenuEnabled = true;
-      return;
-    }
+    this.meta.submenuEnabled = (() => {
+      if (this.links.length > 0) { return true; }
 
 
-    var visibleVars = _.filter(this.templating.list, function(template) {
-      return template.hide !== 2;
-    });
+      var visibleVars = _.filter(this.templating.list, variable => variable.hide !== 2);
+      if (visibleVars.length > 0) { return true; }
+
+      var visibleAnnotations = _.filter(this.annotations.list, annotation => annotation.hide !== true);
+      if (visibleAnnotations.length > 0) { return true; }
 
 
-    this.meta.submenuEnabled = visibleVars.length > 0 || this.annotations.list.length > 0 || this.links.length > 0;
+      return false;
+    })();
   }
   }
 
 
   getPanelInfoById(panelId) {
   getPanelInfoById(panelId) {

+ 65 - 0
public/app/features/dashboard/partials/addAnnotationModal.html

@@ -0,0 +1,65 @@
+<div class="modal-body" ng-controller="AddAnnotationModalCtrl">
+
+  <div class="modal-header">
+    <h2 class="modal-header-title">
+      Add Annotation
+    </h2>
+
+    <a class="modal-header-close" ng-click="ctrl.close()">
+      <i class="fa fa-remove"></i>
+    </a>
+  </div>
+
+
+  <div class="modal-content">
+    <div class="share-modal-body">
+      <div class="share-modal-header">
+
+        <div class="share-modal-big-icon">
+          <i class="fa fa-tag"></i>
+        </div>
+
+        <div class="share-modal-content">
+
+          <div class="gf-form-group share-modal-options">
+            <p class="share-modal-info-text">
+              Add annotation details.
+            </p>
+
+            <div class="gf-form">
+              <span class="gf-form-label width-8">Title</span>
+              <input type="text" ng-model="ctrl.annotation.title" class="gf-form-input max-width-20">
+            </div>
+            <div class="gf-form">
+              <span class="gf-form-label width-8" ng-if="!ctrl.annotation.timeTo">Time</span>
+              <span class="gf-form-label width-8" ng-if="ctrl.annotation.timeTo">Time Start</span>
+              <input type="text" ng-model="ctrl.annotation.time" class="gf-form-input max-width-20">
+            </div>
+            <div class="gf-form" ng-if="ctrl.annotation.timeTo">
+              <span class="gf-form-label width-8">Time Stop</span>
+              <input type="text" ng-model="ctrl.annotation.timeTo" class="gf-form-input max-width-20">
+            </div>
+          </div>
+
+          <div>
+            <h6>Description</h6>
+          </div>
+          <div class="gf-form-group share-modal-options">
+            <div class="gf-form">
+              <textarea rows="3" class="gf-form-input width-27" ng-model="ctrl.annotation.text"></textarea>
+            </div>
+          </div>
+
+          <div class="gf-form-button-row">
+            <button class="btn gf-form-btn width-10 btn-success" ng-click="ctrl.addAnnotation()">
+              <i class="fa fa-pencil"></i>
+              Add Annotation
+            </button>
+          </div>
+        </div>
+
+        </div>
+      </div>
+    </div>
+  </div>
+</div>

+ 81 - 0
public/app/features/dashboard/specs/dashboard_model_specs.ts

@@ -364,4 +364,85 @@ describe('DashboardModel', function() {
     });
     });
   });
   });
 
 
+  describe('updateSubmenuVisibility with empty lists', function() {
+    var model;
+
+    beforeEach(function() {
+      model = new DashboardModel({});
+      model.updateSubmenuVisibility();
+    });
+
+    it('should not enable submmenu', function() {
+      expect(model.meta.submenuEnabled).to.be(false);
+    });
+  });
+
+  describe('updateSubmenuVisibility with annotation', function() {
+    var model;
+
+    beforeEach(function() {
+      model = new DashboardModel({
+        annotations: {
+          list: [{}]
+        }
+      });
+      model.updateSubmenuVisibility();
+    });
+
+    it('should enable submmenu', function() {
+      expect(model.meta.submenuEnabled).to.be(true);
+    });
+  });
+
+  describe('updateSubmenuVisibility with template var', function() {
+    var model;
+
+    beforeEach(function() {
+      model = new DashboardModel({
+        templating: {
+          list: [{}]
+        }
+      });
+      model.updateSubmenuVisibility();
+    });
+
+    it('should enable submmenu', function() {
+      expect(model.meta.submenuEnabled).to.be(true);
+    });
+  });
+
+  describe('updateSubmenuVisibility with hidden template var', function() {
+    var model;
+
+    beforeEach(function() {
+      model = new DashboardModel({
+        templating: {
+          list: [{hide: 2}]
+        }
+      });
+      model.updateSubmenuVisibility();
+    });
+
+    it('should not enable submmenu', function() {
+      expect(model.meta.submenuEnabled).to.be(false);
+    });
+  });
+
+  describe('updateSubmenuVisibility with hidden annotation toggle', function() {
+    var model;
+
+    beforeEach(function() {
+      model = new DashboardModel({
+        annotations: {
+          list: [{hide: true}]
+        }
+      });
+      model.updateSubmenuVisibility();
+    });
+
+    it('should not enable submmenu', function() {
+      expect(model.meta.submenuEnabled).to.be(false);
+    });
+  });
+
 });
 });

+ 2 - 2
public/app/features/dashboard/submenu/submenu.html

@@ -1,4 +1,4 @@
-<div class="submenu-controls gf-form-query">
+<div class="submenu-controls">
 
 
   <div ng-repeat="variable in ctrl.variables" ng-hide="variable.hide === 2" class="submenu-item gf-form-inline">
   <div ng-repeat="variable in ctrl.variables" ng-hide="variable.hide === 2" class="submenu-item gf-form-inline">
     <div class="gf-form">
     <div class="gf-form">
@@ -11,7 +11,7 @@
   </div>
   </div>
 
 
   <div ng-if="ctrl.dashboard.annotations.list.length > 0">
   <div ng-if="ctrl.dashboard.annotations.list.length > 0">
-    <div ng-repeat="annotation in ctrl.dashboard.annotations.list" class="submenu-item" ng-class="{'annotation-disabled': !annotation.enable}">
+    <div ng-repeat="annotation in ctrl.dashboard.annotations.list" ng-hide="annotation.hide" class="submenu-item" ng-class="{'annotation-disabled': !annotation.enable}">
       <gf-form-switch class="gf-form" label="{{annotation.name}}" checked="annotation.enable" on-change="ctrl.annotationStateChanged()"></gf-form-switch>
       <gf-form-switch class="gf-form" label="{{annotation.name}}" checked="annotation.enable" on-change="ctrl.annotationStateChanged()"></gf-form-switch>
     </div>
     </div>
   </div>
   </div>

+ 1 - 1
public/app/partials/dashboard.html

@@ -13,7 +13,7 @@
     <dash-row class="dash-row" ng-repeat="row in dashboard.rows" row="row" dashboard="dashboard">
     <dash-row class="dash-row" ng-repeat="row in dashboard.rows" row="row" dashboard="dashboard">
     </dash-row>
     </dash-row>
 
 
-    <div ng-show='dashboardMeta.canEdit' class="add-row-panel-hint">
+    <div ng-show='dashboard.meta.canEdit && !dashboard.meta.fullscreen' class="add-row-panel-hint">
       <div class="span12" style="text-align:left;">
       <div class="span12" style="text-align:left;">
         <span style="margin-left: 12px;" ng-click="addRowDefault()" class="pointer btn btn-inverse btn-small">
         <span style="margin-left: 12px;" ng-click="addRowDefault()" class="pointer btn btn-inverse btn-small">
           <span><i class="fa fa-plus"></i> ADD ROW</span>
           <span><i class="fa fa-plus"></i> ADD ROW</span>

+ 1 - 1
public/app/plugins/datasource/cloudwatch/partials/config.html

@@ -46,7 +46,7 @@
     </div>
     </div>
   </div>
   </div>
   <div class="gf-form">
   <div class="gf-form">
-    <label class="gf-form-label width-13">Custom Metrics namespace</label>
+    <label class="gf-form-label width-13">Custom Metrics</label>
     <input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.jsonData.customMetricsNamespaces' placeholder="Namespace1,Namespace2"></input>
     <input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.jsonData.customMetricsNamespaces' placeholder="Namespace1,Namespace2"></input>
     <info-popover mode="right-absolute">
     <info-popover mode="right-absolute">
       Namespaces of Custom Metrics
       Namespaces of Custom Metrics

+ 6 - 4
public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html

@@ -3,9 +3,11 @@
 		<span class="gf-form-label width-14">Index name</span>
 		<span class="gf-form-label width-14">Index name</span>
 		<input type="text" class="gf-form-input max-width-20" ng-model='ctrl.annotation.index' placeholder="events-*"></input>
 		<input type="text" class="gf-form-input max-width-20" ng-model='ctrl.annotation.index' placeholder="events-*"></input>
 	</div>
 	</div>
-	<div class="gf-form">
-		<span class="gf-form-label width-14">Search query (lucene) <tip>Use [[filterName]] in query to replace part of the query with a filter value</tip></span>
-		<input type="text" class="gf-form-input max-width-20" ng-model='ctrl.annotation.query' placeholder="tags:deploy"></input>
+	<div class="gf-form-group">
+		<div class="gf-form">
+			<input type="text" class="gf-form-input" ng-model='ctrl.annotation.query'
+											placeholder="Elasticsearch lucene query"></input>
+		</div>
 	</div>
 	</div>
 </div>
 </div>
 
 
@@ -33,4 +35,4 @@
 			<input type="text" class="gf-form-input max-width-16" ng-model='ctrl.annotation.textField' placeholder=""></input>
 			<input type="text" class="gf-form-input max-width-16" ng-model='ctrl.annotation.textField' placeholder=""></input>
 		</div>
 		</div>
 	</div>
 	</div>
-</div>
+</div>

+ 1 - 2
public/app/plugins/datasource/grafana/partials/annotations.editor.html

@@ -1,11 +1,10 @@
 
 
 <div class="gf-form-group">
 <div class="gf-form-group">
-	<h6>Filters</h6>
 	<div class="gf-form-inline">
 	<div class="gf-form-inline">
 		<div class="gf-form">
 		<div class="gf-form">
 			<span class="gf-form-label width-7">Type</span>
 			<span class="gf-form-label width-7">Type</span>
 			<div class="gf-form-select-wrapper">
 			<div class="gf-form-select-wrapper">
-				<select class="gf-form-input" ng-model="ctrl.annotation.type" ng-options="f.value as f.text for f in [{text: 'Alert', value: 'alert'}]">
+				<select class="gf-form-input" ng-model="ctrl.annotation.type" ng-options="f.value as f.text for f in [{text: 'Event', value: 'event'}, {text: 'Alert', value: 'alert'}]">
 				</select>
 				</select>
 			</div>
 			</div>
 		</div>
 		</div>

+ 7 - 4
public/app/plugins/datasource/graphite/partials/annotations.editor.html

@@ -1,10 +1,13 @@
 <div class="gf-form-group">
 <div class="gf-form-group">
   <div class="gf-form">
   <div class="gf-form">
-    <span class="gf-form-label width-13">Graphite metrics query</span>
-    <input type="text" class="gf-form-input" ng-model='ctrl.annotation.target' placeholder=""></input>
+    <span class="gf-form-label width-12">Graphite query</span>
+    <input type="text" class="gf-form-input" ng-model='ctrl.annotation.target' placeholder="Example: statsd.application.counters.*.count"></input>
   </div>
   </div>
+
+	<h5 class="section-heading">Or</h5>
+
   <div class="gf-form">
   <div class="gf-form">
-    <span class="gf-form-label width-13">Or Graphite events query</span>
-    <input type="text" class="gf-form-input" ng-model='ctrl.annotation.tags' placeholder=""></input>
+    <span class="gf-form-label width-12">Graphite events tags</span>
+    <input type="text" class="gf-form-input" ng-model='ctrl.annotation.tags' placeholder="Example: event_tag_name"></input>
   </div>
   </div>
 </div>
 </div>

+ 1 - 1
public/app/plugins/datasource/graphite/partials/query.editor.html

@@ -1,7 +1,7 @@
 <query-editor-row query-ctrl="ctrl" has-text-edit-mode="true">
 <query-editor-row query-ctrl="ctrl" has-text-edit-mode="true">
 
 
 	<div class="gf-form" ng-show="ctrl.target.textEditor">
 	<div class="gf-form" ng-show="ctrl.target.textEditor">
-		<input type="text" class="gf-form-input" ng-model="ctrl.target.target" spellcheck="false" ng-blur="ctrl.refresh()"></input>
+		<input type="text" class="gf-form-input" ng-model="ctrl.target.target" spellcheck="false" ng-blur="ctrl.targetTextChanged()"></input>
 	</div>
 	</div>
 
 
   <div ng-hide="ctrl.target.textEditor">
   <div ng-hide="ctrl.target.textEditor">

+ 47 - 17
public/app/plugins/datasource/graphite/query_ctrl.ts

@@ -28,7 +28,6 @@ export class GraphiteQueryCtrl extends QueryCtrl {
   }
   }
 
 
   toggleEditorMode() {
   toggleEditorMode() {
-    this.target.textEditor = !this.target.textEditor;
     this.parseTarget();
     this.parseTarget();
   }
   }
 
 
@@ -55,7 +54,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
     }
     }
 
 
     try {
     try {
-      this.parseTargeRecursive(astNode, null, 0);
+      this.parseTargetRecursive(astNode, null, 0);
     } catch (err) {
     } catch (err) {
       console.log('error parsing target:', err.message);
       console.log('error parsing target:', err.message);
       this.error = err.message;
       this.error = err.message;
@@ -72,7 +71,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
     func.params[index] = value;
     func.params[index] = value;
   }
   }
 
 
-  parseTargeRecursive(astNode, func, index) {
+  parseTargetRecursive(astNode, func, index) {
     if (astNode === null) {
     if (astNode === null) {
       return null;
       return null;
     }
     }
@@ -81,7 +80,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
       case 'function':
       case 'function':
         var innerFunc = gfunc.createFuncInstance(astNode.name, { withDefaultParams: false });
         var innerFunc = gfunc.createFuncInstance(astNode.name, { withDefaultParams: false });
         _.each(astNode.params, (param, index) => {
         _.each(astNode.params, (param, index) => {
-          this.parseTargeRecursive(param, innerFunc, index);
+          this.parseTargetRecursive(param, innerFunc, index);
         });
         });
 
 
         innerFunc.updateText();
         innerFunc.updateText();
@@ -209,30 +208,61 @@ export class GraphiteQueryCtrl extends QueryCtrl {
   }
   }
 
 
   targetTextChanged() {
   targetTextChanged() {
-    this.parseTarget();
-    this.panelCtrl.refresh();
+    this.updateModelTarget();
+    this.refresh();
   }
   }
 
 
   updateModelTarget() {
   updateModelTarget() {
     // render query
     // render query
-    var metricPath = this.getSegmentPathUpTo(this.segments.length);
-    this.target.target = _.reduce(this.functions, this.wrapFunction, metricPath);
+    if (!this.target.textEditor) {
+      var metricPath = this.getSegmentPathUpTo(this.segments.length);
+      this.target.target = _.reduce(this.functions, this.wrapFunction, metricPath);
+    }
 
 
+    this.updateRenderedTarget(this.target);
+
+    // loop through other queries and update targetFull as needed
+    for (const target of this.panelCtrl.panel.targets || []) {
+      if (target.refId !== this.target.refId) {
+        this.updateRenderedTarget(target);
+      }
+    }
+  }
+
+  updateRenderedTarget(target) {
     // render nested query
     // render nested query
     var targetsByRefId = _.keyBy(this.panelCtrl.panel.targets, 'refId');
     var targetsByRefId = _.keyBy(this.panelCtrl.panel.targets, 'refId');
+
+    // no references to self
+    delete targetsByRefId[target.refId];
+
     var nestedSeriesRefRegex = /\#([A-Z])/g;
     var nestedSeriesRefRegex = /\#([A-Z])/g;
-    var targetWithNestedQueries = this.target.target.replace(nestedSeriesRefRegex, (match, g1) => {
-      var target  = targetsByRefId[g1];
-      if (!target) {
-        return match;
+    var targetWithNestedQueries = target.target;
+
+    // Keep interpolating until there are no query references
+    // The reason for the loop is that the referenced query might contain another reference to another query
+    while (targetWithNestedQueries.match(nestedSeriesRefRegex)) {
+      var updated = targetWithNestedQueries.replace(nestedSeriesRefRegex, (match, g1) => {
+        var t = targetsByRefId[g1];
+        if (!t) {
+          return match;
+        }
+
+        // no circular references
+        delete targetsByRefId[g1];
+        return t.target;
+      });
+
+      if (updated === targetWithNestedQueries) {
+        break;
       }
       }
 
 
-      return target.targetFull || target.target;
-    });
+      targetWithNestedQueries = updated;
+    }
 
 
-    delete this.target.targetFull;
-    if (this.target.target !== targetWithNestedQueries) {
-      this.target.targetFull = targetWithNestedQueries;
+    delete target.targetFull;
+    if (target.target !== targetWithNestedQueries) {
+      target.targetFull = targetWithNestedQueries;
     }
     }
   }
   }
 
 

+ 20 - 0
public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts

@@ -186,4 +186,24 @@ describe('GraphiteQueryCtrl', function() {
       expect(ctx.ctrl.target.targetFull).to.be('scaleToSeconds(nested.query.count)');
       expect(ctx.ctrl.target.targetFull).to.be('scaleToSeconds(nested.query.count)');
     });
     });
   });
   });
+
+  describe('when updating target used in other query', function() {
+    beforeEach(function() {
+      ctx.ctrl.target.target = 'metrics.a.count';
+      ctx.ctrl.target.refId = 'A';
+      ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
+      ctx.ctrl.parseTarget();
+
+      ctx.ctrl.panelCtrl.panel.targets = [
+        ctx.ctrl.target, {target: 'sumSeries(#A)', refId: 'B'}
+      ];
+
+      ctx.ctrl.updateModelTarget();
+    });
+
+    it('targetFull of other query should update', function() {
+      expect(ctx.ctrl.panel.targets[1].targetFull).to.be('sumSeries(metrics.a.count)');
+    });
+  });
+
 });
 });

+ 1 - 2
public/app/plugins/datasource/influxdb/partials/annotations.editor.html

@@ -1,12 +1,11 @@
 
 
-<h5 class="section-heading">Query</h5>
 <div class="gf-form-group">
 <div class="gf-form-group">
 	<div class="gf-form">
 	<div class="gf-form">
 		<input type="text" class="gf-form-input" ng-model='ctrl.annotation.query' placeholder="select text from events where $timeFilter"></input>
 		<input type="text" class="gf-form-input" ng-model='ctrl.annotation.query' placeholder="select text from events where $timeFilter"></input>
 	</div>
 	</div>
 </div>
 </div>
 
 
-<h5 class="section-heading">Column mappings <tip>If your influxdb query returns more than one column you need to specify the column names below. An annotation event is composed of a title, tags, and an additional text field.</tip></h5>
+<h5 class="section-heading">Field mappings <tip>If your influxdb query returns more than one field you need to specify the column names below. An annotation event is composed of a title, tags, and an additional text field.</tip></h5>
 <div class="gf-form-group">
 <div class="gf-form-group">
 	<div class="gf-form-inline">
 	<div class="gf-form-inline">
 		<div class="gf-form">
 		<div class="gf-form">

+ 30 - 60
public/app/plugins/panel/graph/graph.ts

@@ -17,9 +17,10 @@ import {tickStep} from 'app/core/utils/ticks';
 import {appEvents, coreModule} from 'app/core/core';
 import {appEvents, coreModule} from 'app/core/core';
 import GraphTooltip from './graph_tooltip';
 import GraphTooltip from './graph_tooltip';
 import {ThresholdManager} from './threshold_manager';
 import {ThresholdManager} from './threshold_manager';
+import {EventManager} from 'app/features/annotations/all';
 import {convertValuesToHistogram, getSeriesValues} from './histogram';
 import {convertValuesToHistogram, getSeriesValues} from './histogram';
 
 
-coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
+coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) {
   return {
   return {
     restrict: 'A',
     restrict: 'A',
     template: '',
     template: '',
@@ -27,13 +28,14 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
       var ctrl = scope.ctrl;
       var ctrl = scope.ctrl;
       var dashboard = ctrl.dashboard;
       var dashboard = ctrl.dashboard;
       var panel = ctrl.panel;
       var panel = ctrl.panel;
+      var annotations = [];
       var data;
       var data;
-      var annotations;
       var plot;
       var plot;
       var sortedSeries;
       var sortedSeries;
       var legendSideLastValue = null;
       var legendSideLastValue = null;
       var rootScope = scope.$root;
       var rootScope = scope.$root;
       var panelWidth = 0;
       var panelWidth = 0;
+      var eventManager = new EventManager(ctrl, elem, popoverSrv);
       var thresholdManager = new ThresholdManager(ctrl);
       var thresholdManager = new ThresholdManager(ctrl);
       var tooltip = new GraphTooltip(elem, dashboard, scope, function() {
       var tooltip = new GraphTooltip(elem, dashboard, scope, function() {
         return sortedSeries;
         return sortedSeries;
@@ -54,7 +56,7 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
         if (!data) {
         if (!data) {
           return;
           return;
         }
         }
-        annotations = ctrl.annotations;
+        annotations = ctrl.annotations || [];
         render_panel();
         render_panel();
       });
       });
 
 
@@ -328,8 +330,8 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
           }
           }
         }
         }
 
 
-        thresholdManager.addPlotOptions(options, panel);
-        addAnnotations(options);
+        thresholdManager.addFlotOptions(options, panel);
+        eventManager.addFlotEvents(annotations, options);
         configureAxisOptions(data, options);
         configureAxisOptions(data, options);
 
 
         sortedSeries = _.sortBy(data, function(series) { return series.zindex; });
         sortedSeries = _.sortBy(data, function(series) { return series.zindex; });
@@ -461,56 +463,6 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
         };
         };
       }
       }
 
 
-      function addAnnotations(options) {
-        if (!annotations || annotations.length === 0) {
-          return;
-        }
-
-        var types = {};
-        types['$__alerting'] = {
-          color: 'rgba(237, 46, 24, 1)',
-          position: 'BOTTOM',
-          markerSize: 5,
-        };
-
-        types['$__ok'] = {
-          color: 'rgba(11, 237, 50, 1)',
-          position: 'BOTTOM',
-          markerSize: 5,
-        };
-
-        types['$__no_data'] = {
-          color: 'rgba(150, 150, 150, 1)',
-          position: 'BOTTOM',
-          markerSize: 5,
-        };
-
-        types['$__execution_error'] = ['$__no_data'];
-
-        for (var i = 0; i < annotations.length; i++) {
-          var item = annotations[i];
-          if (item.newState) {
-            console.log(item.newState);
-            item.eventType = '$__' + item.newState;
-            continue;
-          }
-
-          if (!types[item.source.name]) {
-            types[item.source.name] = {
-              color: item.source.iconColor,
-              position: 'BOTTOM',
-              markerSize: 5,
-            };
-          }
-        }
-
-        options.events = {
-          levels: _.keys(types).length + 1,
-          data: annotations,
-          types: types,
-        };
-      }
-
       function configureAxisOptions(data, options) {
       function configureAxisOptions(data, options) {
         var defaults = {
         var defaults = {
           position: 'left',
           position: 'left',
@@ -639,12 +591,30 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
       }
       }
 
 
       elem.bind("plotselected", function (event, ranges) {
       elem.bind("plotselected", function (event, ranges) {
-        scope.$apply(function() {
-          timeSrv.setTime({
-            from  : moment.utc(ranges.xaxis.from),
-            to    : moment.utc(ranges.xaxis.to),
+        if (ranges.ctrlKey || ranges.metaKey)  {
+          // scope.$apply(() => {
+          //   eventManager.updateTime(ranges.xaxis);
+          // });
+        } else {
+          scope.$apply(function() {
+            timeSrv.setTime({
+              from  : moment.utc(ranges.xaxis.from),
+              to    : moment.utc(ranges.xaxis.to),
+            });
           });
           });
-        });
+        }
+      });
+
+      elem.bind("plotclick", function (event, pos, item) {
+        if (pos.ctrlKey || pos.metaKey || eventManager.event)  {
+          // Skip if range selected (added in "plotselected" event handler)
+          let isRangeSelection = pos.x !== pos.x1;
+          if (!isRangeSelection) {
+            // scope.$apply(() => {
+            //   eventManager.updateTime({from: pos.x, to: null});
+            // });
+          }
+        }
       });
       });
 
 
       scope.$on('$destroy', function() {
       scope.$on('$destroy', function() {

+ 1 - 0
public/app/plugins/panel/graph/legend.js

@@ -48,6 +48,7 @@ function (angular, _, $) {
               element: el[0],
               element: el[0],
               position: 'bottom center',
               position: 'bottom center',
               template: '<gf-color-picker></gf-color-picker>',
               template: '<gf-color-picker></gf-color-picker>',
+              openOn: 'hover',
               model: {
               model: {
                 series: series,
                 series: series,
                 toggleAxis: function() {
                 toggleAxis: function() {

+ 1 - 1
public/app/plugins/panel/graph/specs/threshold_manager_specs.ts

@@ -21,7 +21,7 @@ describe('ThresholdManager', function() {
       ctx.setup = function(thresholds) {
       ctx.setup = function(thresholds) {
         ctx.panel.thresholds = thresholds;
         ctx.panel.thresholds = thresholds;
         var manager = new ThresholdManager(ctx.panelCtrl);
         var manager = new ThresholdManager(ctx.panelCtrl);
-        manager.addPlotOptions(ctx.options, ctx.panel);
+        manager.addFlotOptions(ctx.options, ctx.panel);
       };
       };
 
 
       func(ctx);
       func(ctx);

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

@@ -158,7 +158,7 @@ export class ThresholdManager {
     this.needsCleanup = true;
     this.needsCleanup = true;
   }
   }
 
 
-  addPlotOptions(options, panel) {
+  addFlotOptions(options, panel) {
     if (!panel.thresholds || panel.thresholds.length === 0) {
     if (!panel.thresholds || panel.thresholds.length === 0) {
       return;
       return;
     }
     }

+ 3 - 2
public/sass/_variables.dark.scss

@@ -251,8 +251,9 @@ $infoText:                $blue-dark;
 $infoBackground:          $blue-dark;
 $infoBackground:          $blue-dark;
 
 
 // popover
 // popover
-$popover-bg:         $dark-4;
-$popover-color:      $text-color;
+$popover-bg:              $panel-bg;
+$popover-color:           $text-color;
+$popover-border-color:    $gray-1;
 
 
 $popover-help-bg:         $btn-secondary-bg;
 $popover-help-bg:         $btn-secondary-bg;
 $popover-help-color:      $text-color;
 $popover-help-color:      $text-color;

+ 1 - 0
public/sass/_variables.light.scss

@@ -278,6 +278,7 @@ $infoBorder:              transparent;
 // popover
 // popover
 $popover-bg:              $gray-5;
 $popover-bg:              $gray-5;
 $popover-color:           $text-color;
 $popover-color:           $text-color;
+$popover-border-color:    $gray-3;
 
 
 $popover-help-bg:         $blue-dark;
 $popover-help-bg:         $blue-dark;
 $popover-help-color:      $gray-6;
 $popover-help-color:      $gray-6;

+ 13 - 9
public/sass/components/_drop.scss

@@ -1,11 +1,18 @@
 $popover-arrow-size: 0.7rem;
 $popover-arrow-size: 0.7rem;
 $color: inherit;
 $color: inherit;
-$backgroundColor: $btn-secondary-bg;
 $color: $text-color;
 $color: $text-color;
 $useDropShadow: false;
 $useDropShadow: false;
 $attachmentOffset: 0%;
 $attachmentOffset: 0%;
 $easing: cubic-bezier(0, 0, 0.265, 1.00);
 $easing: cubic-bezier(0, 0, 0.265, 1.00);
 
 
+@include drop-theme("error", $errorBackground, $popover-color);
+@include drop-theme("popover", $popover-bg, $popover-color, $popover-border-color);
+@include drop-theme("help", $popover-help-bg, $popover-help-color);
+
+@include drop-animation-scale("drop", "help", $attachmentOffset: $attachmentOffset, $easing: $easing);
+@include drop-animation-scale("drop", "error", $attachmentOffset: $attachmentOffset, $easing: $easing);
+@include drop-animation-scale("drop", "popover", $attachmentOffset: $attachmentOffset, $easing: $easing);
+
 .drop-element {
 .drop-element {
   z-index: 10000;
   z-index: 10000;
   position: absolute;
   position: absolute;
@@ -44,11 +51,8 @@ $easing: cubic-bezier(0, 0, 0.265, 1.00);
   }
   }
 }
 }
 
 
-@include drop-theme("help", $popover-help-bg, $popover-help-color);
-@include drop-theme("error", $errorBackground, $popover-color);
-@include drop-theme("popover", $popover-bg, $popover-color);
-
-@include drop-animation-scale("drop", "help", $attachmentOffset: $attachmentOffset, $easing: $easing);
-@include drop-animation-scale("drop", "error", $attachmentOffset: $attachmentOffset, $easing: $easing);
-@include drop-animation-scale("drop", "popover", $attachmentOffset: $attachmentOffset, $easing: $easing);
-
+.drop-element.drop-popover--form {
+  .drop-content {
+    max-width: none;
+  }
+}

+ 0 - 1
public/sass/components/_modals.scss

@@ -67,7 +67,6 @@
 
 
 .modal-content {
 .modal-content {
   padding: $spacer*2;
   padding: $spacer*2;
-  min-height: $spacer*15;
 }
 }
 
 
 // Remove bottom margin if need be
 // Remove bottom margin if need be

+ 6 - 0
public/sass/components/_submenu.scss

@@ -1,4 +1,10 @@
 .submenu-controls {
 .submenu-controls {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  align-content: flex-start;
+  align-items: flex-start;
+
   margin: 0 $panel-margin ($panel-margin*2) $panel-margin;
   margin: 0 $panel-margin ($panel-margin*2) $panel-margin;
 }
 }
 
 

+ 22 - 17
public/sass/mixins/_drop_element.scss

@@ -1,5 +1,5 @@
 
 
-@mixin drop-theme($themeName, $theme-bg, $theme-color) {
+@mixin drop-theme($themeName, $theme-bg, $theme-color, $border-color: $theme-bg) {
   .drop-element.drop-#{$themeName} {
   .drop-element.drop-#{$themeName} {
     max-width: 100%;
     max-width: 100%;
     max-height: 100%;
     max-height: 100%;
@@ -14,6 +14,11 @@
       font-size: $font-size-sm;
       font-size: $font-size-sm;
       word-wrap: break-word;
       word-wrap: break-word;
       max-width: 20rem;
       max-width: 20rem;
+      border: 1px solid $border-color;
+
+      @if $theme-bg != $border-color {
+        box-shadow: 0 0 15px $border-color;
+      }
 
 
       &:before {
       &:before {
         content: "";
         content: "";
@@ -43,7 +48,7 @@
         top: 100%;
         top: 100%;
         left: 50%;
         left: 50%;
         margin-left: - $popover-arrow-size;
         margin-left: - $popover-arrow-size;
-        border-top-color: $theme-bg;
+        border-top-color: $border-color;
       }
       }
     }
     }
 
 
@@ -54,7 +59,7 @@
         bottom: 100%;
         bottom: 100%;
         left: 50%;
         left: 50%;
         margin-left: - $popover-arrow-size;
         margin-left: - $popover-arrow-size;
-        border-bottom-color: $theme-bg;
+        border-bottom-color: $border-color;
       }
       }
     }
     }
 
 
@@ -65,7 +70,7 @@
         left: 100%;
         left: 100%;
         top: 50%;
         top: 50%;
         margin-top: - $popover-arrow-size;
         margin-top: - $popover-arrow-size;
-        border-left-color: $theme-bg;
+        border-left-color: $border-color;
       }
       }
     }
     }
 
 
@@ -76,7 +81,7 @@
         right: 100%;
         right: 100%;
         top: 50%;
         top: 50%;
         margin-top: - $popover-arrow-size;
         margin-top: - $popover-arrow-size;
-        border-right-color: $theme-bg;
+        border-right-color: $border-color;
       }
       }
     }
     }
 
 
@@ -95,7 +100,7 @@
       &:before {
       &:before {
         bottom: 100%;
         bottom: 100%;
         left: $popover-arrow-size;
         left: $popover-arrow-size;
-        border-bottom-color: $theme-bg;
+        border-bottom-color: $border-color;
       }
       }
     }
     }
 
 
@@ -105,7 +110,7 @@
       &:before {
       &:before {
         bottom: 100%;
         bottom: 100%;
         right: $popover-arrow-size;
         right: $popover-arrow-size;
-        border-bottom-color: $theme-bg;
+        border-bottom-color: $border-color;
       }
       }
     }
     }
 
 
@@ -115,7 +120,7 @@
       &:before {
       &:before {
         top: 100%;
         top: 100%;
         left: $popover-arrow-size;
         left: $popover-arrow-size;
-        border-top-color: $theme-bg;
+        border-top-color: $border-color;
       }
       }
     }
     }
 
 
@@ -125,7 +130,7 @@
       &:before {
       &:before {
         top: 100%;
         top: 100%;
         right: $popover-arrow-size;
         right: $popover-arrow-size;
-        border-top-color: $theme-bg;
+        border-top-color: $border-color;
       }
       }
     }
     }
 
 
@@ -136,7 +141,7 @@
       &:before {
       &:before {
         bottom: 100%;
         bottom: 100%;
         left: $popover-arrow-size;
         left: $popover-arrow-size;
-        border-bottom-color: $theme-bg;
+        border-bottom-color: $border-color;
       }
       }
     }
     }
 
 
@@ -146,7 +151,7 @@
       &:before {
       &:before {
         bottom: 100%;
         bottom: 100%;
         right: $popover-arrow-size;
         right: $popover-arrow-size;
-        border-bottom-color: $theme-bg;
+        border-bottom-color: $border-color;
       }
       }
     }
     }
 
 
@@ -156,7 +161,7 @@
       &:before {
       &:before {
         top: 100%;
         top: 100%;
         left: $popover-arrow-size;
         left: $popover-arrow-size;
-        border-top-color: $theme-bg;
+        border-top-color: $border-color;
       }
       }
     }
     }
 
 
@@ -166,7 +171,7 @@
       &:before {
       &:before {
         top: 100%;
         top: 100%;
         right: $popover-arrow-size;
         right: $popover-arrow-size;
-        border-top-color: $theme-bg;
+        border-top-color: $border-color;
       }
       }
     }
     }
 
 
@@ -177,7 +182,7 @@
       &:before {
       &:before {
         top: $popover-arrow-size;
         top: $popover-arrow-size;
         left: 100%;
         left: 100%;
-        border-left-color: $theme-bg;
+        border-left-color: $border-color;
       }
       }
     }
     }
 
 
@@ -187,7 +192,7 @@
       &:before {
       &:before {
         top: $popover-arrow-size;
         top: $popover-arrow-size;
         right: 100%;
         right: 100%;
-        border-right-color: $theme-bg;
+        border-right-color: $border-color;
       }
       }
     }
     }
 
 
@@ -197,7 +202,7 @@
       &:before {
       &:before {
         bottom: $popover-arrow-size;
         bottom: $popover-arrow-size;
         left: 100%;
         left: 100%;
-        border-left-color: $theme-bg;
+        border-left-color: $border-color;
       }
       }
     }
     }
 
 
@@ -207,7 +212,7 @@
       &:before {
       &:before {
         bottom: $popover-arrow-size;
         bottom: $popover-arrow-size;
         right: 100%;
         right: 100%;
-        border-right-color: $theme-bg;
+        border-right-color: $border-color;
       }
       }
     }
     }
   }
   }

+ 4 - 0
public/vendor/flot/jquery.flot.js

@@ -2972,6 +2972,10 @@ Licensed under the MIT license.
             pos.pageX = event.pageX;
             pos.pageX = event.pageX;
             pos.pageY = event.pageY;
             pos.pageY = event.pageY;
 
 
+            // Add ctrlKey and metaKey to event
+            pos.ctrlKey = event.ctrlKey;
+            pos.metaKey = event.metaKey;
+
             var item = findNearbyItem(canvasX, canvasY, seriesFilter);
             var item = findNearbyItem(canvasX, canvasY, seriesFilter);
 
 
             if (item) {
             if (item) {

+ 6 - 2
public/vendor/flot/jquery.flot.selection.js

@@ -145,7 +145,7 @@ The plugin allso adds the following methods to the plot object:
             updateSelection(e);
             updateSelection(e);
 
 
             if (selectionIsSane())
             if (selectionIsSane())
-                triggerSelectedEvent();
+                triggerSelectedEvent(e);
             else {
             else {
                 // this counts as a clear
                 // this counts as a clear
                 plot.getPlaceholder().trigger("plotunselected", [ ]);
                 plot.getPlaceholder().trigger("plotunselected", [ ]);
@@ -180,9 +180,13 @@ The plugin allso adds the following methods to the plot object:
             return r;
             return r;
         }
         }
 
 
-        function triggerSelectedEvent() {
+        function triggerSelectedEvent(event) {
             var r = getSelection();
             var r = getSelection();
 
 
+            // Add ctrlKey and metaKey to event
+            r.ctrlKey = event.ctrlKey;
+            r.metaKey = event.metaKey;
+
             plot.getPlaceholder().trigger("plotselected", [ r ]);
             plot.getPlaceholder().trigger("plotselected", [ r ]);
 
 
             // backwards-compat stuff, to be removed in future
             // backwards-compat stuff, to be removed in future

+ 2 - 1
tasks/options/exec.js

@@ -1,7 +1,8 @@
-module.exports = function(config) {
+module.exports = function(config, grunt) {
   'use strict'
   'use strict'
   return {
   return {
     tslint : "node ./node_modules/tslint/lib/tslint-cli.js -c tslint.json --project ./tsconfig.json",
     tslint : "node ./node_modules/tslint/lib/tslint-cli.js -c tslint.json --project ./tsconfig.json",
+    tslintfile : "node ./node_modules/tslint/lib/tslint-cli.js -c tslint.json --project ./tsconfig.json <%= tslint.source.files.src %>",
     tscompile: "node ./node_modules/typescript/lib/tsc.js -p tsconfig.json --diagnostics",
     tscompile: "node ./node_modules/typescript/lib/tsc.js -p tsconfig.json --diagnostics",
     tswatch: "node ./node_modules/typescript/lib/tsc.js -p tsconfig.json --diagnostics --watch",
     tswatch: "node ./node_modules/typescript/lib/tsc.js -p tsconfig.json --diagnostics --watch",
   };
   };

+ 11 - 0
tasks/options/tslint.js

@@ -0,0 +1,11 @@
+module.exports = function(config, grunt) {
+  'use strict'
+  // dummy to avoid template compile error
+  return {
+    source: {
+      files: {
+        src: ""
+      }
+    }
+  };
+};

+ 13 - 1
tasks/options/watch.js

@@ -8,6 +8,10 @@ module.exports = function(config, grunt) {
   var lastTime;
   var lastTime;
 
 
   grunt.registerTask('watch', function() {
   grunt.registerTask('watch', function() {
+    if (!grunt.option('skip-ts-compile')) {
+      grunt.log.writeln('We recommoned starting with: grunt watch --force --skip-ts-compile')
+      grunt.log.writeln('Then do incremental typescript builds with: grunt exec:tswatch')
+    }
 
 
     done = this.async();
     done = this.async();
     lastTime = new Date().getTime();
     lastTime = new Date().getTime();
@@ -58,7 +62,15 @@ module.exports = function(config, grunt) {
           newPath = filepath.replace(/^public/, 'public_gen');
           newPath = filepath.replace(/^public/, 'public_gen');
           grunt.log.writeln('Copying to ' + newPath);
           grunt.log.writeln('Copying to ' + newPath);
           grunt.file.copy(filepath, newPath);
           grunt.file.copy(filepath, newPath);
-          grunt.task.run('exec:tslint');
+
+          if (grunt.option('skip-ts-compile')) {
+            grunt.log.writeln('Skipping ts compile, run grunt exec:tswatch to start typescript watcher')
+          } else {
+            grunt.task.run('exec:tscompile');
+          }
+
+          grunt.config('tslint.source.files.src', filepath);
+          grunt.task.run('exec:tslintfile');
         }
         }
 
 
         done();
         done();

+ 0 - 0
tasks/tslint.js