Просмотр исходного кода

Merge remote-tracking branch 'origin/master' into getting-started-panel-css2

Torkel Ödegaard 9 лет назад
Родитель
Сommit
fd512457d8
93 измененных файлов с 854 добавлено и 1344 удалено
  1. 7 0
      CHANGELOG.md
  2. 1 16
      conf/defaults.ini
  3. 0 15
      conf/sample.ini
  4. 1 1
      docs/sources/alerting/notifications.md
  5. 4 1
      docs/sources/alerting/rules.md
  6. 1 1
      docs/sources/datasources/influxdb.md
  7. 0 6
      docs/sources/datasources/plugin_api.md
  8. 28 0
      docs/sources/http_api/org.md
  9. 1 1
      docs/sources/installation/configuration.md
  10. 12 0
      docs/sources/installation/rpm.md
  11. 2 1
      pkg/api/alerting.go
  12. 1 0
      pkg/api/api.go
  13. 6 5
      pkg/api/dtos/alerting.go
  14. 3 1
      pkg/api/login_oauth.go
  15. 1 0
      pkg/metrics/publish.go
  16. 1 0
      pkg/middleware/middleware.go
  17. 24 50
      pkg/middleware/recovery.go
  18. 2 0
      pkg/models/stats.go
  19. 7 1
      pkg/services/alerting/conditions/query.go
  20. 1 1
      pkg/services/alerting/eval_context.go
  21. 18 5
      pkg/services/alerting/eval_handler.go
  22. 90 5
      pkg/services/alerting/eval_handler_test.go
  23. 1 0
      pkg/services/alerting/interfaces.go
  24. 26 5
      pkg/services/alerting/rule.go
  25. 3 0
      pkg/services/alerting/scheduler.go
  26. 10 2
      pkg/services/sqlstore/stats.go
  27. 5 3
      pkg/tsdb/influxdb/influxdb.go
  28. 2 0
      pkg/tsdb/influxdb/model_parser.go
  29. 1 0
      pkg/tsdb/influxdb/models.go
  30. 27 23
      pkg/tsdb/influxdb/query.go
  31. 32 15
      pkg/tsdb/influxdb/query_test.go
  32. 0 1
      public/app/app.ts
  33. 6 1
      public/app/core/components/grafana_app.ts
  34. 1 2
      public/app/core/core.ts
  35. 0 31
      public/app/core/directives/grafana_version_check.js
  36. 1 1
      public/app/core/directives/ng_model_on_blur.js
  37. 0 0
      public/app/core/directives/password_strength.js
  38. 3 3
      public/app/core/utils/kbn.js
  39. 4 0
      public/app/features/admin/partials/stats.html
  40. 6 0
      public/app/features/alerting/alert_def.ts
  41. 4 0
      public/app/features/alerting/alert_tab_ctrl.ts
  42. 5 5
      public/app/features/alerting/partials/alert_tab.html
  43. 1 1
      public/app/features/dashboard/dashboard_ctrl.ts
  44. 7 3
      public/app/features/dashboard/dynamic_dashboard_srv.ts
  45. 1 3
      public/app/features/dashboard/export/export_modal.ts
  46. 29 8
      public/app/features/dashboard/export/exporter.ts
  47. 8 5
      public/app/features/dashboard/model.ts
  48. 0 282
      public/app/features/dashboard/partials/globalAlerts.html
  49. 1 1
      public/app/features/dashboard/row/add_panel.html
  50. 0 6
      public/app/features/dashboard/row/add_panel.ts
  51. 2 3
      public/app/features/dashboard/row/row_ctrl.ts
  52. 4 0
      public/app/features/dashboard/row/row_model.ts
  53. 6 2
      public/app/features/dashboard/specs/dashboard_srv_specs.ts
  54. 1 3
      public/app/features/dashboard/specs/dynamic_dashboard_srv_specs.ts
  55. 10 1
      public/app/features/dashboard/specs/exporter_specs.ts
  56. 9 0
      public/app/features/panel/panel_ctrl.ts
  57. 8 2
      public/app/features/panel/panel_directive.ts
  58. 0 56
      public/app/features/panel/partials/query_editor_row.html
  59. 1 1
      public/app/features/templating/adhoc_variable.ts
  60. 1 1
      public/app/features/templating/constant_variable.ts
  61. 1 1
      public/app/features/templating/custom_variable.ts
  62. 4 1
      public/app/features/templating/datasource_variable.ts
  63. 1 1
      public/app/features/templating/interval_variable.ts
  64. 1 1
      public/app/features/templating/partials/editor.html
  65. 7 1
      public/app/features/templating/query_variable.ts
  66. 9 2
      public/app/features/templating/specs/query_variable_specs.ts
  67. 1 1
      public/app/features/templating/variable.ts
  68. 1 10
      public/app/features/templating/variable_srv.ts
  69. 0 3
      public/app/partials/bootstrap/tab.html
  70. 0 11
      public/app/partials/bootstrap/tabset.html
  71. 8 7
      public/app/partials/error.html
  72. 234 51
      public/app/plugins/app/testdata/dashboards/graph_last_1h.json
  73. 1 1
      public/app/plugins/app/testdata/plugin.json
  74. 15 6
      public/app/plugins/datasource/cloudwatch/datasource.js
  75. 1 0
      public/app/plugins/panel/alertlist/editor.html
  76. 11 2
      public/app/plugins/panel/alertlist/module.ts
  77. 1 1
      public/app/plugins/panel/graph/graph_tooltip.js
  78. 3 6
      public/app/plugins/panel/singlestat/module.ts
  79. 0 2
      public/sass/_grafana.scss
  80. 0 69
      public/sass/components/_gfbox.scss
  81. 1 1
      public/sass/components/_navs.scss
  82. 79 0
      public/sass/components/_query_editor.scss
  83. 1 2
      public/sass/components/_row.scss
  84. 2 2
      public/sass/components/_shortcuts.scss
  85. 6 5
      public/sass/components/_submenu.scss
  86. 1 1
      public/sass/components/_tabbed_view.scss
  87. 0 235
      public/sass/components/_tightform.scss
  88. 4 2
      public/sass/components/_timepicker.scss
  89. 0 6
      public/sass/layout/_page.scss
  90. 0 1
      public/sass/pages/_alerting.scss
  91. 6 0
      public/sass/pages/_dashboard.scss
  92. 2 320
      public/vendor/angular-ui/ui-bootstrap-tpls.js
  93. 16 12
      public/views/500.html

+ 7 - 0
CHANGELOG.md

@@ -4,6 +4,13 @@
 * **Graph Panel**: Log base scale on right Y-axis had no effect, max value calc was not applied, [#6534](https://github.com/grafana/grafana/issues/6534)
 * **Graph Panel**: Log base scale on right Y-axis had no effect, max value calc was not applied, [#6534](https://github.com/grafana/grafana/issues/6534)
 * **Graph Panel**: Bar width if bars was only used in series override, [#6528](https://github.com/grafana/grafana/issues/6528)
 * **Graph Panel**: Bar width if bars was only used in series override, [#6528](https://github.com/grafana/grafana/issues/6528)
 * **UI/Browser**: Fixed issue with page/view header gradient border not showing in Safari, [#6530](https://github.com/grafana/grafana/issues/6530)
 * **UI/Browser**: Fixed issue with page/view header gradient border not showing in Safari, [#6530](https://github.com/grafana/grafana/issues/6530)
+* **UX**: Panel Drop zone visible after duplicating panel, and when entering fullscreen/edit view, [#6598](https://github.com/grafana/grafana/issues/6598)
+* **Templating**: Newly added variable was not visible directly only after dashboard reload, [#6622](https://github.com/grafana/grafana/issues/6622)
+
+### Enhancements
+* **Singlestat**: Support repeated template variables in prefix/postfix [#6595](https://github.com/grafana/grafana/issues/6595)
+* **Templating**: Don't persist variable options with refresh option [#6586](https://github.com/grafana/grafana/issues/6586)
+* **Alerting**: Add ability to have OR conditions (and mixing AND & OR) [#6579](https://github.com/grafana/grafana/issues/6579)
 
 
 # 4.0-beta1 (2016-11-09)
 # 4.0-beta1 (2016-11-09)
 
 

+ 1 - 16
conf/defaults.ini

@@ -229,7 +229,7 @@ auth_url = https://accounts.google.com/o/oauth2/auth
 token_url = https://accounts.google.com/o/oauth2/token
 token_url = https://accounts.google.com/o/oauth2/token
 api_url = https://www.googleapis.com/oauth2/v1/userinfo
 api_url = https://www.googleapis.com/oauth2/v1/userinfo
 allowed_domains =
 allowed_domains =
-hosted_domain = 
+hosted_domain =
 
 
 #################################### Grafana.net Auth ####################
 #################################### Grafana.net Auth ####################
 [auth.grafananet]
 [auth.grafananet]
@@ -390,21 +390,6 @@ global_api_key = -1
 global_session = -1
 global_session = -1
 
 
 #################################### Alerting ############################
 #################################### Alerting ############################
-# docs about alerting can be found in /docs/sources/alerting/
-#              __.-/|
-#              \`o_O'
-#               =( )=  +----------------------------+
-#                 U|   | Alerting is still in alpha |
-#       /\  /\   / |   +----------------------------+
-#      ) /^\) ^\/ _)\     |
-#      )   /^\/   _) \    |
-#      )   _ /  / _)  \___|_
-#  /\  )/\/ ||  | )_)\___,|))
-# <  >      |(,,) )__)    |
-#  ||      /    \)___)\
-#  | \____(      )___) )____
-#   \______(_______;;;)__;;;)
-
 [alerting]
 [alerting]
 # Makes it possible to turn off alert rule execution.
 # Makes it possible to turn off alert rule execution.
 execute_alerts = true
 execute_alerts = true

+ 0 - 15
conf/sample.ini

@@ -339,21 +339,6 @@
 ;path = /var/lib/grafana/dashboards
 ;path = /var/lib/grafana/dashboards
 
 
 #################################### Alerting ######################################
 #################################### Alerting ######################################
-# docs about alerting can be found in /docs/sources/alerting/
-#              __.-/|
-#              \`o_O'
-#               =( )=  +----------------------------+
-#                 U|   | Alerting is still in alpha |
-#       /\  /\   / |   +----------------------------+
-#      ) /^\) ^\/ _)\     |
-#      )   /^\/   _) \    |
-#      )   _ /  / _)  \___|_
-#  /\  )/\/ ||  | )_)\___,|))
-# <  >      |(,,) )__)    |
-#  ||      /    \)___)\
-#  | \____(      )___) )____
-#   \______(_______;;;)__;;;)
-
 [alerting]
 [alerting]
 # Makes it possible to turn off alert rule execution.
 # Makes it possible to turn off alert rule execution.
 ;execute_alerts = true
 ;execute_alerts = true

+ 1 - 1
docs/sources/alerting/notifications.md

@@ -98,6 +98,6 @@ Amazon S3 for this and Webdav. So to set that up you need to configure the
 [external image uploader](/installation/configuration/#external-image-storage) in your grafana-server ini
 [external image uploader](/installation/configuration/#external-image-storage) in your grafana-server ini
 config file.
 config file.
 
 
-This is not an optional requirement, you can get slack and email notifications without setting this up.
+This is an optional requirement, you can get slack and email notifications without setting this up.
 
 
 
 

+ 4 - 1
docs/sources/alerting/rules.md

@@ -55,7 +55,10 @@ Currently the only condition type that exists is a `Query` condition that allows
 specify a query letter, time range and an aggregation function. The letter refers to
 specify a query letter, time range and an aggregation function. The letter refers to
 a query you already have added in the **Metrics** tab. The result from the query and the aggregation function is
 a query you already have added in the **Metrics** tab. The result from the query and the aggregation function is
 a single value that is then used in the threshold check. The query used in an alert rule cannot
 a single value that is then used in the threshold check. The query used in an alert rule cannot
-contain any template variables. Currently we only support `AND` operator between conditions.
+contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially.
+For example, we have 3 conditions in the following order:
+`condition:A(evaluates to: TRUE) OR condition:B(evaluates to: FALSE) AND condition:C(evaluates to: TRUE)`
+so the result will be calculated as ((TRUE OR FALSE) AND TRUE) = TRUE.
 
 
 We plan to add other condition types in the future, like `Other Alert`, where you can include the state
 We plan to add other condition types in the future, like `Other Alert`, where you can include the state
 of another alert in your conditions, and `Time Of Day`.
 of another alert in your conditions, and `Time Of Day`.

+ 1 - 1
docs/sources/datasources/influxdb.md

@@ -118,7 +118,7 @@ SHOW TAG VALUES WITH KEY = "hostname"  WHERE region =~ /$region/
 
 
 > Always you `regex values` or `regex wildcard` for All format or multi select format.
 > Always you `regex values` or `regex wildcard` for All format or multi select format.
 
 
-![](img/docs/influxdb/templating_simple_ex1.png)
+![](/img/docs/influxdb/templating_simple_ex1.png)
 
 
 ## Annotations
 ## Annotations
 Annotations allows you to overlay rich event information on top of graphs.
 Annotations allows you to overlay rich event information on top of graphs.

+ 0 - 6
docs/sources/datasources/plugin_api.md

@@ -30,11 +30,5 @@ Even though the data source type name is with lowercase `g`, the directive uses
 that is how angular directives needs to be named in order to match an element with name `<metric-query-editor-graphite />`.
 that is how angular directives needs to be named in order to match an element with name `<metric-query-editor-graphite />`.
 You also specify the query controller here instead of in the query.editor.html partial like before.
 You also specify the query controller here instead of in the query.editor.html partial like before.
 
 
-### query.editor.html
-
-This partial needs to be updated, remove the `np-repeat` this is done in the outer partial now,m the query.editor.html
-should only render a single query. Take a look at the Graphite or InfluxDB partials for `query.editor.html` for reference.
-You should also add a `tight-form-item` with `{{target.refId}}`, all queries needs to be assigned a letter (`refId`).
-These query reference letters are going to be utilized in a later feature.
 
 
 
 

+ 28 - 0
docs/sources/http_api/org.md

@@ -85,6 +85,34 @@ page_keywords: grafana, admin, http, api, documentation, orgs, organisation
       }
       }
     }
     }
 
 
+## Create Organisation
+
+`POST /api/org`
+
+**Example Request**:
+
+    POST /api/org HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+    {
+      "name":"New Org."
+    }
+
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {
+      "orgId":"1",
+      "message":"Organization created"
+    }
+
+
+
 ## Update current Organisation
 ## Update current Organisation
 
 
 `PUT /api/org`
 `PUT /api/org`

+ 1 - 1
docs/sources/installation/configuration.md

@@ -413,7 +413,7 @@ Set to `true` to enable LDAP integration (default: `false`)
 ### config_file
 ### config_file
 Path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`)
 Path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`)
 
 
-> For details on LDAP Configuration, go to the [LDAP Integration](ldap.md) page.
+> For details on LDAP Configuration, go to the [LDAP Integration]({{< relref "ldap.md" >}}) page.
 
 
 <hr>
 <hr>
 
 

+ 12 - 0
docs/sources/installation/rpm.md

@@ -141,6 +141,18 @@ those options.
 - [OpenTSDB]({{< relref "datasources/opentsdb.md" >}})
 - [OpenTSDB]({{< relref "datasources/opentsdb.md" >}})
 - [Prometheus]({{< relref "datasources/prometheus.md" >}})
 - [Prometheus]({{< relref "datasources/prometheus.md" >}})
 
 
+### Server side image rendering
+
+Server side image (png) rendering is a feature that is optional but very useful when sharing visualizations,
+for example in alert notifications.
+
+If the image is missing text make sure you have font packages installed.
+
+```
+yum install fontconfig
+yum install freetype*
+yum install urw-fonts
+```
 
 
 ## Installing from binary tar file
 ## Installing from binary tar file
 
 

+ 2 - 1
pkg/api/alerting.go

@@ -119,7 +119,8 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response {
 	res := backendCmd.Result
 	res := backendCmd.Result
 
 
 	dtoRes := &dtos.AlertTestResult{
 	dtoRes := &dtos.AlertTestResult{
-		Firing: res.Firing,
+		Firing:         res.Firing,
+		ConditionEvals: res.ConditionEvals,
 	}
 	}
 
 
 	if res.Error != nil {
 	if res.Error != nil {

+ 1 - 0
pkg/api/api.go

@@ -310,4 +310,5 @@ func Register(r *macaron.Macaron) {
 
 
 	InitAppPluginRoutes(r)
 	InitAppPluginRoutes(r)
 
 
+	r.NotFound(NotFoundHandler)
 }
 }

+ 6 - 5
pkg/api/dtos/alerting.go

@@ -35,11 +35,12 @@ type AlertTestCommand struct {
 }
 }
 
 
 type AlertTestResult struct {
 type AlertTestResult struct {
-	Firing      bool                  `json:"firing"`
-	TimeMs      string                `json:"timeMs"`
-	Error       string                `json:"error,omitempty"`
-	EvalMatches []*EvalMatch          `json:"matches,omitempty"`
-	Logs        []*AlertTestResultLog `json:"logs,omitempty"`
+	Firing         bool                  `json:"firing"`
+	ConditionEvals string                `json:"conditionEvals"`
+	TimeMs         string                `json:"timeMs"`
+	Error          string                `json:"error,omitempty"`
+	EvalMatches    []*EvalMatch          `json:"matches,omitempty"`
+	Logs           []*AlertTestResultLog `json:"logs,omitempty"`
 }
 }
 
 
 type AlertTestResultLog struct {
 type AlertTestResultLog struct {

+ 3 - 1
pkg/api/login_oauth.go

@@ -96,7 +96,7 @@ func OAuthLogin(ctx *middleware.Context) {
 		}
 		}
 		sslcli := &http.Client{Transport: tr}
 		sslcli := &http.Client{Transport: tr}
 
 
-		oauthCtx = context.TODO()
+		oauthCtx = context.Background()
 		oauthCtx = context.WithValue(oauthCtx, oauth2.HTTPClient, sslcli)
 		oauthCtx = context.WithValue(oauthCtx, oauth2.HTTPClient, sslcli)
 	}
 	}
 
 
@@ -106,6 +106,8 @@ func OAuthLogin(ctx *middleware.Context) {
 		ctx.Handle(500, "login.OAuthLogin(NewTransportWithCode)", err)
 		ctx.Handle(500, "login.OAuthLogin(NewTransportWithCode)", err)
 		return
 		return
 	}
 	}
+	// token.TokenType was defaulting to "bearer", which is out of spec, so we explicitly set to "Bearer"
+	token.TokenType = "Bearer"
 
 
 	ctx.Logger.Debug("OAuthLogin Got token")
 	ctx.Logger.Debug("OAuthLogin Got token")
 
 

+ 1 - 0
pkg/metrics/publish.go

@@ -101,6 +101,7 @@ func sendUsageStats() {
 	metrics["stats.plugins.apps.count"] = len(plugins.Apps)
 	metrics["stats.plugins.apps.count"] = len(plugins.Apps)
 	metrics["stats.plugins.panels.count"] = len(plugins.Panels)
 	metrics["stats.plugins.panels.count"] = len(plugins.Panels)
 	metrics["stats.plugins.datasources.count"] = len(plugins.DataSources)
 	metrics["stats.plugins.datasources.count"] = len(plugins.DataSources)
+	metrics["stats.alerts.count"] = statsQuery.Result.AlertCount
 
 
 	dsStats := m.GetDataSourceStatsQuery{}
 	dsStats := m.GetDataSourceStatsQuery{}
 	if err := bus.Dispatch(&dsStats); err != nil {
 	if err := bus.Dispatch(&dsStats); err != nil {

+ 1 - 0
pkg/middleware/middleware.go

@@ -187,6 +187,7 @@ func (ctx *Context) Handle(status int, title string, err error) {
 	}
 	}
 
 
 	ctx.Data["Title"] = title
 	ctx.Data["Title"] = title
+	ctx.Data["AppSubUrl"] = setting.AppSubUrl
 	ctx.HTML(status, strconv.Itoa(status))
 	ctx.HTML(status, strconv.Itoa(status))
 }
 }
 
 

+ 24 - 50
pkg/middleware/recovery.go

@@ -19,53 +19,14 @@ import (
 	"bytes"
 	"bytes"
 	"fmt"
 	"fmt"
 	"io/ioutil"
 	"io/ioutil"
-	"net/http"
 	"runtime"
 	"runtime"
 
 
 	"gopkg.in/macaron.v1"
 	"gopkg.in/macaron.v1"
 
 
-	"github.com/go-macaron/inject"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 )
 )
 
 
-const (
-	panicHtml = `<html>
-<head><title>PANIC: %s</title>
-<meta charset="utf-8" />
-<style type="text/css">
-html, body {
-	font-family: "Roboto", sans-serif;
-	color: #333333;
-	background-color: #ea5343;
-	margin: 0px;
-}
-h1 {
-	color: #d04526;
-	background-color: #ffffff;
-	padding: 20px;
-	border-bottom: 1px dashed #2b3848;
-}
-pre {
-	margin: 20px;
-	padding: 20px;
-	border: 2px solid #2b3848;
-	background-color: #ffffff;
-	white-space: pre-wrap;       /* css-3 */
-	white-space: -moz-pre-wrap;  /* Mozilla, since 1999 */
-	white-space: -pre-wrap;      /* Opera 4-6 */
-	white-space: -o-pre-wrap;    /* Opera 7 */
-	word-wrap: break-word;       /* Internet Explorer 5.5+ */
-}
-</style>
-</head><body>
-<h1>PANIC</h1>
-<pre style="font-weight: bold;">%s</pre>
-<pre>%s</pre>
-</body>
-</html>`
-)
-
 var (
 var (
 	dunno     = []byte("???")
 	dunno     = []byte("???")
 	centerDot = []byte("·")
 	centerDot = []byte("·")
@@ -151,21 +112,34 @@ func Recovery() macaron.Handler {
 
 
 				panicLogger.Error("Request error", "error", err, "stack", string(stack))
 				panicLogger.Error("Request error", "error", err, "stack", string(stack))
 
 
-				// Lookup the current responsewriter
-				val := c.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil)))
-				res := val.Interface().(http.ResponseWriter)
+				c.Data["Title"] = "Server Error"
+				c.Data["AppSubUrl"] = setting.AppSubUrl
 
 
-				// respond with panic message while in development mode
-				var body []byte
-				if setting.Env == setting.DEV {
-					res.Header().Set("Content-Type", "text/html")
-					body = []byte(fmt.Sprintf(panicHtml, err, err, stack))
+				if theErr, ok := err.(error); ok {
+					c.Data["Title"] = theErr.Error()
 				}
 				}
 
 
-				res.WriteHeader(http.StatusInternalServerError)
-				if nil != body {
-					res.Write(body)
+				if setting.Env == setting.DEV {
+					c.Data["ErrorMsg"] = string(stack)
 				}
 				}
+
+				c.HTML(500, "500")
+
+				// // Lookup the current responsewriter
+				// val := c.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil)))
+				// res := val.Interface().(http.ResponseWriter)
+				//
+				// // respond with panic message while in development mode
+				// var body []byte
+				// if setting.Env == setting.DEV {
+				// 	res.Header().Set("Content-Type", "text/html")
+				// 	body = []byte(fmt.Sprintf(panicHtml, err, err, stack))
+				// }
+				//
+				// res.WriteHeader(http.StatusInternalServerError)
+				// if nil != body {
+				// 	res.Write(body)
+				// }
 			}
 			}
 		}()
 		}()
 
 

+ 2 - 0
pkg/models/stats.go

@@ -5,6 +5,7 @@ type SystemStats struct {
 	UserCount      int64
 	UserCount      int64
 	OrgCount       int64
 	OrgCount       int64
 	PlaylistCount  int64
 	PlaylistCount  int64
+	AlertCount     int64
 }
 }
 
 
 type DataSourceStats struct {
 type DataSourceStats struct {
@@ -29,6 +30,7 @@ type AdminStats struct {
 	DataSourceCount int `json:"data_source_count"`
 	DataSourceCount int `json:"data_source_count"`
 	PlaylistCount   int `json:"playlist_count"`
 	PlaylistCount   int `json:"playlist_count"`
 	StarredDbCount  int `json:"starred_db_count"`
 	StarredDbCount  int `json:"starred_db_count"`
+	AlertCount      int `json:"alert_count"`
 }
 }
 
 
 type GetAdminStatsQuery struct {
 type GetAdminStatsQuery struct {

+ 7 - 1
pkg/services/alerting/conditions/query.go

@@ -23,6 +23,7 @@ type QueryCondition struct {
 	Query         AlertQuery
 	Query         AlertQuery
 	Reducer       QueryReducer
 	Reducer       QueryReducer
 	Evaluator     AlertEvaluator
 	Evaluator     AlertEvaluator
+	Operator      string
 	HandleRequest tsdb.HandleRequestFunc
 	HandleRequest tsdb.HandleRequestFunc
 }
 }
 
 
@@ -72,6 +73,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
 	return &alerting.ConditionResult{
 	return &alerting.ConditionResult{
 		Firing:      evalMatchCount > 0,
 		Firing:      evalMatchCount > 0,
 		NoDataFound: emptySerieCount == len(seriesList),
 		NoDataFound: emptySerieCount == len(seriesList),
+		Operator:    c.Operator,
 		EvalMatches: matches,
 		EvalMatches: matches,
 	}, nil
 	}, nil
 }
 }
@@ -168,8 +170,12 @@ func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, erro
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
-
 	condition.Evaluator = evaluator
 	condition.Evaluator = evaluator
+
+	operatorJson := model.Get("operator")
+	operator := operatorJson.Get("type").MustString("and")
+	condition.Operator = operator
+
 	return &condition, nil
 	return &condition, nil
 }
 }
 
 

+ 1 - 1
pkg/services/alerting/eval_context.go

@@ -17,7 +17,7 @@ type EvalContext struct {
 	EvalMatches     []*EvalMatch
 	EvalMatches     []*EvalMatch
 	Logs            []*ResultLogEntry
 	Logs            []*ResultLogEntry
 	Error           error
 	Error           error
-	Description     string
+	ConditionEvals  string
 	StartTime       time.Time
 	StartTime       time.Time
 	EndTime         time.Time
 	EndTime         time.Time
 	Rule            *Rule
 	Rule            *Rule

+ 18 - 5
pkg/services/alerting/eval_handler.go

@@ -1,6 +1,8 @@
 package alerting
 package alerting
 
 
 import (
 import (
+	"strconv"
+	"strings"
 	"time"
 	"time"
 
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
@@ -21,7 +23,10 @@ func NewEvalHandler() *DefaultEvalHandler {
 
 
 func (e *DefaultEvalHandler) Eval(context *EvalContext) {
 func (e *DefaultEvalHandler) Eval(context *EvalContext) {
 	firing := true
 	firing := true
-	for _, condition := range context.Rule.Conditions {
+	conditionEvals := ""
+
+	for i := 0; i < len(context.Rule.Conditions); i++ {
+		condition := context.Rule.Conditions[i]
 		cr, err := condition.Eval(context)
 		cr, err := condition.Eval(context)
 		if err != nil {
 		if err != nil {
 			context.Error = err
 			context.Error = err
@@ -32,15 +37,23 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
 			break
 			break
 		}
 		}
 
 
-		// break if result has not triggered yet
-		if cr.Firing == false {
-			firing = false
-			break
+		// calculating Firing based on operator
+		if cr.Operator == "or" {
+			firing = firing || cr.Firing
+		} else {
+			firing = firing && cr.Firing
+		}
+
+		if i > 0 {
+			conditionEvals = "[" + conditionEvals + " " + strings.ToUpper(cr.Operator) + " " + strconv.FormatBool(cr.Firing) + "]"
+		} else {
+			conditionEvals = strconv.FormatBool(firing)
 		}
 		}
 
 
 		context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...)
 		context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...)
 	}
 	}
 
 
+	context.ConditionEvals = conditionEvals + " = " + strconv.FormatBool(firing)
 	context.Firing = firing
 	context.Firing = firing
 	context.EndTime = time.Now()
 	context.EndTime = time.Now()
 	elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond
 	elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond

+ 90 - 5
pkg/services/alerting/eval_handler_test.go

@@ -8,12 +8,13 @@ import (
 )
 )
 
 
 type conditionStub struct {
 type conditionStub struct {
-	firing  bool
-	matches []*EvalMatch
+	firing   bool
+	operator string
+	matches  []*EvalMatch
 }
 }
 
 
 func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) {
 func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) {
-	return &ConditionResult{Firing: c.firing, EvalMatches: c.matches}, nil
+	return &ConditionResult{Firing: c.firing, EvalMatches: c.matches, Operator: c.operator}, nil
 }
 }
 
 
 func TestAlertingExecutor(t *testing.T) {
 func TestAlertingExecutor(t *testing.T) {
@@ -29,18 +30,102 @@ func TestAlertingExecutor(t *testing.T) {
 
 
 			handler.Eval(context)
 			handler.Eval(context)
 			So(context.Firing, ShouldEqual, true)
 			So(context.Firing, ShouldEqual, true)
+			So(context.ConditionEvals, ShouldEqual, "true = true")
 		})
 		})
 
 
 		Convey("Show return false with not passing asdf", func() {
 		Convey("Show return false with not passing asdf", func() {
 			context := NewEvalContext(context.TODO(), &Rule{
 			context := NewEvalContext(context.TODO(), &Rule{
 				Conditions: []Condition{
 				Conditions: []Condition{
-					&conditionStub{firing: true, matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}},
-					&conditionStub{firing: false},
+					&conditionStub{firing: true, operator: "and", matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}},
+					&conditionStub{firing: false, operator: "and"},
 				},
 				},
 			})
 			})
 
 
 			handler.Eval(context)
 			handler.Eval(context)
 			So(context.Firing, ShouldEqual, false)
 			So(context.Firing, ShouldEqual, false)
+			So(context.ConditionEvals, ShouldEqual, "[true AND false] = false")
+		})
+
+		Convey("Show return true if any of the condition is passing with OR operator", func() {
+			context := NewEvalContext(context.TODO(), &Rule{
+				Conditions: []Condition{
+					&conditionStub{firing: true, operator: "and"},
+					&conditionStub{firing: false, operator: "or"},
+				},
+			})
+
+			handler.Eval(context)
+			So(context.Firing, ShouldEqual, true)
+			So(context.ConditionEvals, ShouldEqual, "[true OR false] = true")
+		})
+
+		Convey("Show return false if any of the condition is failing with AND operator", func() {
+			context := NewEvalContext(context.TODO(), &Rule{
+				Conditions: []Condition{
+					&conditionStub{firing: true, operator: "and"},
+					&conditionStub{firing: false, operator: "and"},
+				},
+			})
+
+			handler.Eval(context)
+			So(context.Firing, ShouldEqual, false)
+			So(context.ConditionEvals, ShouldEqual, "[true AND false] = false")
+		})
+
+		Convey("Show return true if one condition is failing with nested OR operator", func() {
+			context := NewEvalContext(context.TODO(), &Rule{
+				Conditions: []Condition{
+					&conditionStub{firing: true, operator: "and"},
+					&conditionStub{firing: true, operator: "and"},
+					&conditionStub{firing: false, operator: "or"},
+				},
+			})
+
+			handler.Eval(context)
+			So(context.Firing, ShouldEqual, true)
+			So(context.ConditionEvals, ShouldEqual, "[[true AND true] OR false] = true")
+		})
+
+		Convey("Show return false if one condition is passing with nested OR operator", func() {
+			context := NewEvalContext(context.TODO(), &Rule{
+				Conditions: []Condition{
+					&conditionStub{firing: true, operator: "and"},
+					&conditionStub{firing: false, operator: "and"},
+					&conditionStub{firing: false, operator: "or"},
+				},
+			})
+
+			handler.Eval(context)
+			So(context.Firing, ShouldEqual, false)
+			So(context.ConditionEvals, ShouldEqual, "[[true AND false] OR false] = false")
+		})
+
+		Convey("Show return false if a condition is failing with nested AND operator", func() {
+			context := NewEvalContext(context.TODO(), &Rule{
+				Conditions: []Condition{
+					&conditionStub{firing: true, operator: "and"},
+					&conditionStub{firing: false, operator: "and"},
+					&conditionStub{firing: true, operator: "and"},
+				},
+			})
+
+			handler.Eval(context)
+			So(context.Firing, ShouldEqual, false)
+			So(context.ConditionEvals, ShouldEqual, "[[true AND false] AND true] = false")
+		})
+
+		Convey("Show return true if a condition is passing with nested OR operator", func() {
+			context := NewEvalContext(context.TODO(), &Rule{
+				Conditions: []Condition{
+					&conditionStub{firing: true, operator: "and"},
+					&conditionStub{firing: false, operator: "or"},
+					&conditionStub{firing: true, operator: "or"},
+				},
+			})
+
+			handler.Eval(context)
+			So(context.Firing, ShouldEqual, true)
+			So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true")
 		})
 		})
 	})
 	})
 }
 }

+ 1 - 0
pkg/services/alerting/interfaces.go

@@ -24,6 +24,7 @@ type Notifier interface {
 type ConditionResult struct {
 type ConditionResult struct {
 	Firing      bool
 	Firing      bool
 	NoDataFound bool
 	NoDataFound bool
+	Operator    string
 	EvalMatches []*EvalMatch
 	EvalMatches []*EvalMatch
 }
 }
 
 

+ 26 - 5
pkg/services/alerting/rule.go

@@ -26,11 +26,32 @@ type Rule struct {
 }
 }
 
 
 type ValidationError struct {
 type ValidationError struct {
-	Reason string
+	Reason      string
+	Err         error
+	Alertid     int64
+	DashboardId int64
+	PanelId     int64
 }
 }
 
 
 func (e ValidationError) Error() string {
 func (e ValidationError) Error() string {
-	return e.Reason
+	extraInfo := ""
+	if e.Alertid != 0 {
+		extraInfo = fmt.Sprintf("%s AlertId: %v", extraInfo, e.Alertid)
+	}
+
+	if e.PanelId != 0 {
+		extraInfo = fmt.Sprintf("%s PanelId: %v ", extraInfo, e.PanelId)
+	}
+
+	if e.DashboardId != 0 {
+		extraInfo = fmt.Sprintf("%s DashboardId: %v", extraInfo, e.DashboardId)
+	}
+
+	if e.Err != nil {
+		return fmt.Sprintf("%s %s%s", e.Err.Error(), e.Reason, extraInfo)
+	}
+
+	return fmt.Sprintf("Failed to extract alert.Reason: %s %s", e.Reason, extraInfo)
 }
 }
 
 
 var (
 var (
@@ -83,7 +104,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
 	for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
 	for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
 		jsonModel := simplejson.NewFromAny(v)
 		jsonModel := simplejson.NewFromAny(v)
 		if id, err := jsonModel.Get("id").Int64(); err != nil {
 		if id, err := jsonModel.Get("id").Int64(); err != nil {
-			return nil, ValidationError{Reason: "Invalid notification schema"}
+			return nil, ValidationError{Reason: "Invalid notification schema", DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
 		} else {
 		} else {
 			model.Notifications = append(model.Notifications, id)
 			model.Notifications = append(model.Notifications, id)
 		}
 		}
@@ -93,10 +114,10 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
 		conditionModel := simplejson.NewFromAny(condition)
 		conditionModel := simplejson.NewFromAny(condition)
 		conditionType := conditionModel.Get("type").MustString()
 		conditionType := conditionModel.Get("type").MustString()
 		if factory, exist := conditionFactories[conditionType]; !exist {
 		if factory, exist := conditionFactories[conditionType]; !exist {
-			return nil, ValidationError{Reason: "Unknown alert condition: " + conditionType}
+			return nil, ValidationError{Reason: "Unknown alert condition: " + conditionType, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
 		} else {
 		} else {
 			if queryCondition, err := factory(conditionModel, index); err != nil {
 			if queryCondition, err := factory(conditionModel, index); err != nil {
-				return nil, err
+				return nil, ValidationError{Err: err, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
 			} else {
 			} else {
 				model.Conditions = append(model.Conditions, queryCondition)
 				model.Conditions = append(model.Conditions, queryCondition)
 			}
 			}

+ 3 - 0
pkg/services/alerting/scheduler.go

@@ -39,6 +39,9 @@ func (s *SchedulerImpl) Update(rules []*Rule) {
 
 
 		offset := ((rule.Frequency * 1000) / int64(len(rules))) * int64(i)
 		offset := ((rule.Frequency * 1000) / int64(len(rules))) * int64(i)
 		job.Offset = int64(math.Floor(float64(offset) / 1000))
 		job.Offset = int64(math.Floor(float64(offset) / 1000))
+		if job.Offset == 0 { //zero offset causes division with 0 panics.
+			job.Offset = 1
+		}
 		jobs[rule.Id] = job
 		jobs[rule.Id] = job
 	}
 	}
 
 

+ 10 - 2
pkg/services/sqlstore/stats.go

@@ -39,7 +39,11 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error {
       (
       (
         SELECT COUNT(*)
         SELECT COUNT(*)
         FROM ` + dialect.Quote("playlist") + `
         FROM ` + dialect.Quote("playlist") + `
-      ) AS playlist_count
+      ) AS playlist_count,
+      (
+        SELECT COUNT(*)
+        FROM ` + dialect.Quote("alert") + `
+      ) AS alert_count
 			`
 			`
 
 
 	var stats m.SystemStats
 	var stats m.SystemStats
@@ -85,7 +89,11 @@ func GetAdminStats(query *m.GetAdminStatsQuery) error {
       (
       (
         SELECT COUNT(DISTINCT ` + dialect.Quote("dashboard_id") + ` )
         SELECT COUNT(DISTINCT ` + dialect.Quote("dashboard_id") + ` )
         FROM ` + dialect.Quote("star") + `
         FROM ` + dialect.Quote("star") + `
-      ) AS starred_db_count
+      ) AS starred_db_count,
+      (
+        SELECT COUNT(*)
+        FROM ` + dialect.Quote("alert") + `
+      ) AS alert_count
       `
       `
 
 
 	var stats m.AdminStats
 	var stats m.AdminStats

+ 5 - 3
pkg/tsdb/influxdb/influxdb.go

@@ -18,7 +18,6 @@ import (
 type InfluxDBExecutor struct {
 type InfluxDBExecutor struct {
 	*tsdb.DataSourceInfo
 	*tsdb.DataSourceInfo
 	QueryParser    *InfluxdbQueryParser
 	QueryParser    *InfluxdbQueryParser
-	QueryBuilder   *QueryBuilder
 	ResponseParser *ResponseParser
 	ResponseParser *ResponseParser
 }
 }
 
 
@@ -26,7 +25,6 @@ func NewInfluxDBExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
 	return &InfluxDBExecutor{
 	return &InfluxDBExecutor{
 		DataSourceInfo: dsInfo,
 		DataSourceInfo: dsInfo,
 		QueryParser:    &InfluxdbQueryParser{},
 		QueryParser:    &InfluxdbQueryParser{},
-		QueryBuilder:   &QueryBuilder{},
 		ResponseParser: &ResponseParser{},
 		ResponseParser: &ResponseParser{},
 	}
 	}
 }
 }
@@ -51,7 +49,7 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice,
 		return result.WithError(err)
 		return result.WithError(err)
 	}
 	}
 
 
-	rawQuery, err := e.QueryBuilder.Build(query, context)
+	rawQuery, err := query.Build(context)
 	if err != nil {
 	if err != nil {
 		return result.WithError(err)
 		return result.WithError(err)
 	}
 	}
@@ -84,6 +82,10 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice,
 		return result.WithError(err)
 		return result.WithError(err)
 	}
 	}
 
 
+	if response.Err != nil {
+		return result.WithError(response.Err)
+	}
+
 	result.QueryResults = make(map[string]*tsdb.QueryResult)
 	result.QueryResults = make(map[string]*tsdb.QueryResult)
 	result.QueryResults["A"] = e.ResponseParser.Parse(&response, query)
 	result.QueryResults["A"] = e.ResponseParser.Parse(&response, query)
 
 

+ 2 - 0
pkg/tsdb/influxdb/model_parser.go

@@ -12,6 +12,7 @@ type InfluxdbQueryParser struct{}
 func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSourceInfo) (*Query, error) {
 func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSourceInfo) (*Query, error) {
 	policy := model.Get("policy").MustString("default")
 	policy := model.Get("policy").MustString("default")
 	rawQuery := model.Get("query").MustString("")
 	rawQuery := model.Get("query").MustString("")
+	useRawQuery := model.Get("rawQuery").MustBool(false)
 	alias := model.Get("alias").MustString("")
 	alias := model.Get("alias").MustString("")
 
 
 	measurement := model.Get("measurement").MustString("")
 	measurement := model.Get("measurement").MustString("")
@@ -54,6 +55,7 @@ func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSo
 		RawQuery:     rawQuery,
 		RawQuery:     rawQuery,
 		Interval:     interval,
 		Interval:     interval,
 		Alias:        alias,
 		Alias:        alias,
+		UseRawQuery:  useRawQuery,
 	}, nil
 	}, nil
 }
 }
 
 

+ 1 - 0
pkg/tsdb/influxdb/models.go

@@ -8,6 +8,7 @@ type Query struct {
 	GroupBy      []*QueryPart
 	GroupBy      []*QueryPart
 	Selects      []*Select
 	Selects      []*Select
 	RawQuery     string
 	RawQuery     string
+	UseRawQuery  bool
 	Alias        string
 	Alias        string
 
 
 	Interval string
 	Interval string

+ 27 - 23
pkg/tsdb/influxdb/query_builder.go → pkg/tsdb/influxdb/query.go

@@ -2,7 +2,6 @@ package influxdb
 
 
 import (
 import (
 	"fmt"
 	"fmt"
-	"strconv"
 	"strings"
 	"strings"
 
 
 	"regexp"
 	"regexp"
@@ -11,31 +10,30 @@ import (
 )
 )
 
 
 var (
 var (
-	regexpOperatorPattern *regexp.Regexp = regexp.MustCompile(`^\/.*\/$`)
+	regexpOperatorPattern    *regexp.Regexp = regexp.MustCompile(`^\/.*\/$`)
+	regexpMeasurementPattern *regexp.Regexp = regexp.MustCompile(`^\/.*\/$`)
 )
 )
 
 
-type QueryBuilder struct{}
-
-func (qb *QueryBuilder) Build(query *Query, queryContext *tsdb.QueryContext) (string, error) {
-	if query.RawQuery != "" {
+func (query *Query) Build(queryContext *tsdb.QueryContext) (string, error) {
+	if query.UseRawQuery && query.RawQuery != "" {
 		q := query.RawQuery
 		q := query.RawQuery
 
 
-		q = strings.Replace(q, "$timeFilter", qb.renderTimeFilter(query, queryContext), 1)
+		q = strings.Replace(q, "$timeFilter", query.renderTimeFilter(queryContext), 1)
 		q = strings.Replace(q, "$interval", tsdb.CalculateInterval(queryContext.TimeRange), 1)
 		q = strings.Replace(q, "$interval", tsdb.CalculateInterval(queryContext.TimeRange), 1)
 
 
 		return q, nil
 		return q, nil
 	}
 	}
 
 
-	res := qb.renderSelectors(query, queryContext)
-	res += qb.renderMeasurement(query)
-	res += qb.renderWhereClause(query)
-	res += qb.renderTimeFilter(query, queryContext)
-	res += qb.renderGroupBy(query, queryContext)
+	res := query.renderSelectors(queryContext)
+	res += query.renderMeasurement()
+	res += query.renderWhereClause()
+	res += query.renderTimeFilter(queryContext)
+	res += query.renderGroupBy(queryContext)
 
 
 	return res, nil
 	return res, nil
 }
 }
 
 
-func (qb *QueryBuilder) renderTags(query *Query) []string {
+func (query *Query) renderTags() []string {
 	var res []string
 	var res []string
 	for i, tag := range query.Tags {
 	for i, tag := range query.Tags {
 		str := ""
 		str := ""
@@ -59,13 +57,12 @@ func (qb *QueryBuilder) renderTags(query *Query) []string {
 		}
 		}
 
 
 		textValue := ""
 		textValue := ""
-		numericValue, err := strconv.ParseFloat(tag.Value, 64)
 
 
 		// quote value unless regex or number
 		// quote value unless regex or number
 		if tag.Operator == "=~" || tag.Operator == "!~" {
 		if tag.Operator == "=~" || tag.Operator == "!~" {
 			textValue = tag.Value
 			textValue = tag.Value
-		} else if err == nil {
-			textValue = fmt.Sprintf("%v", numericValue)
+		} else if tag.Operator == "<" || tag.Operator == ">" {
+			textValue = tag.Value
 		} else {
 		} else {
 			textValue = fmt.Sprintf("'%s'", tag.Value)
 			textValue = fmt.Sprintf("'%s'", tag.Value)
 		}
 		}
@@ -76,7 +73,7 @@ func (qb *QueryBuilder) renderTags(query *Query) []string {
 	return res
 	return res
 }
 }
 
 
-func (qb *QueryBuilder) renderTimeFilter(query *Query, queryContext *tsdb.QueryContext) string {
+func (query *Query) renderTimeFilter(queryContext *tsdb.QueryContext) string {
 	from := "now() - " + queryContext.TimeRange.From
 	from := "now() - " + queryContext.TimeRange.From
 	to := ""
 	to := ""
 
 
@@ -87,7 +84,7 @@ func (qb *QueryBuilder) renderTimeFilter(query *Query, queryContext *tsdb.QueryC
 	return fmt.Sprintf("time > %s%s", from, to)
 	return fmt.Sprintf("time > %s%s", from, to)
 }
 }
 
 
-func (qb *QueryBuilder) renderSelectors(query *Query, queryContext *tsdb.QueryContext) string {
+func (query *Query) renderSelectors(queryContext *tsdb.QueryContext) string {
 	res := "SELECT "
 	res := "SELECT "
 
 
 	var selectors []string
 	var selectors []string
@@ -103,19 +100,26 @@ func (qb *QueryBuilder) renderSelectors(query *Query, queryContext *tsdb.QueryCo
 	return res + strings.Join(selectors, ", ")
 	return res + strings.Join(selectors, ", ")
 }
 }
 
 
-func (qb *QueryBuilder) renderMeasurement(query *Query) string {
+func (query *Query) renderMeasurement() string {
 	policy := ""
 	policy := ""
 	if query.Policy == "" || query.Policy == "default" {
 	if query.Policy == "" || query.Policy == "default" {
 		policy = ""
 		policy = ""
 	} else {
 	} else {
 		policy = `"` + query.Policy + `".`
 		policy = `"` + query.Policy + `".`
 	}
 	}
-	return fmt.Sprintf(` FROM %s"%s"`, policy, query.Measurement)
+
+	measurement := query.Measurement
+
+	if !regexpMeasurementPattern.Match([]byte(measurement)) {
+		measurement = fmt.Sprintf(`"%s"`, measurement)
+	}
+
+	return fmt.Sprintf(` FROM %s%s`, policy, measurement)
 }
 }
 
 
-func (qb *QueryBuilder) renderWhereClause(query *Query) string {
+func (query *Query) renderWhereClause() string {
 	res := " WHERE "
 	res := " WHERE "
-	conditions := qb.renderTags(query)
+	conditions := query.renderTags()
 	res += strings.Join(conditions, " ")
 	res += strings.Join(conditions, " ")
 	if len(conditions) > 0 {
 	if len(conditions) > 0 {
 		res += " AND "
 		res += " AND "
@@ -124,7 +128,7 @@ func (qb *QueryBuilder) renderWhereClause(query *Query) string {
 	return res
 	return res
 }
 }
 
 
-func (qb *QueryBuilder) renderGroupBy(query *Query, queryContext *tsdb.QueryContext) string {
+func (query *Query) renderGroupBy(queryContext *tsdb.QueryContext) string {
 	groupBy := ""
 	groupBy := ""
 	for i, group := range query.GroupBy {
 	for i, group := range query.GroupBy {
 		if i == 0 {
 		if i == 0 {

+ 32 - 15
pkg/tsdb/influxdb/query_builder_test.go → pkg/tsdb/influxdb/query_test.go

@@ -12,7 +12,6 @@ import (
 func TestInfluxdbQueryBuilder(t *testing.T) {
 func TestInfluxdbQueryBuilder(t *testing.T) {
 
 
 	Convey("Influxdb query builder", t, func() {
 	Convey("Influxdb query builder", t, func() {
-		builder := QueryBuilder{}
 
 
 		qp1, _ := NewQueryPart("field", []string{"value"})
 		qp1, _ := NewQueryPart("field", []string{"value"})
 		qp2, _ := NewQueryPart("mean", []string{})
 		qp2, _ := NewQueryPart("mean", []string{})
@@ -37,7 +36,7 @@ func TestInfluxdbQueryBuilder(t *testing.T) {
 				Interval:    "10s",
 				Interval:    "10s",
 			}
 			}
 
 
-			rawQuery, err := builder.Build(query, queryContext)
+			rawQuery, err := query.Build(queryContext)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 			So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "policy"."cpu" WHERE time > now() - 5m GROUP BY time(10s) fill(null)`)
 			So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "policy"."cpu" WHERE time > now() - 5m GROUP BY time(10s) fill(null)`)
 		})
 		})
@@ -51,23 +50,22 @@ func TestInfluxdbQueryBuilder(t *testing.T) {
 				Interval:    "5s",
 				Interval:    "5s",
 			}
 			}
 
 
-			rawQuery, err := builder.Build(query, queryContext)
+			rawQuery, err := query.Build(queryContext)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 			So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE "hostname" = 'server1' OR "hostname" = 'server2' AND time > now() - 5m GROUP BY time(5s), "datacenter" fill(null)`)
 			So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE "hostname" = 'server1' OR "hostname" = 'server2' AND time > now() - 5m GROUP BY time(5s), "datacenter" fill(null)`)
 		})
 		})
 
 
 		Convey("can render time range", func() {
 		Convey("can render time range", func() {
 			query := Query{}
 			query := Query{}
-			builder := &QueryBuilder{}
 			Convey("render from: 2h to now-1h", func() {
 			Convey("render from: 2h to now-1h", func() {
 				query := Query{}
 				query := Query{}
 				queryContext := &tsdb.QueryContext{TimeRange: tsdb.NewTimeRange("2h", "now-1h")}
 				queryContext := &tsdb.QueryContext{TimeRange: tsdb.NewTimeRange("2h", "now-1h")}
-				So(builder.renderTimeFilter(&query, queryContext), ShouldEqual, "time > now() - 2h and time < now() - 1h")
+				So(query.renderTimeFilter(queryContext), ShouldEqual, "time > now() - 2h and time < now() - 1h")
 			})
 			})
 
 
 			Convey("render from: 10m", func() {
 			Convey("render from: 10m", func() {
 				queryContext := &tsdb.QueryContext{TimeRange: tsdb.NewTimeRange("10m", "now")}
 				queryContext := &tsdb.QueryContext{TimeRange: tsdb.NewTimeRange("10m", "now")}
-				So(builder.renderTimeFilter(&query, queryContext), ShouldEqual, "time > now() - 10m")
+				So(query.renderTimeFilter(queryContext), ShouldEqual, "time > now() - 10m")
 			})
 			})
 		})
 		})
 
 
@@ -79,9 +77,10 @@ func TestInfluxdbQueryBuilder(t *testing.T) {
 				GroupBy:     []*QueryPart{groupBy1, groupBy3},
 				GroupBy:     []*QueryPart{groupBy1, groupBy3},
 				Interval:    "10s",
 				Interval:    "10s",
 				RawQuery:    "Raw query",
 				RawQuery:    "Raw query",
+				UseRawQuery: true,
 			}
 			}
 
 
-			rawQuery, err := builder.Build(query, queryContext)
+			rawQuery, err := query.Build(queryContext)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 			So(rawQuery, ShouldEqual, `Raw query`)
 			So(rawQuery, ShouldEqual, `Raw query`)
 		})
 		})
@@ -89,37 +88,55 @@ func TestInfluxdbQueryBuilder(t *testing.T) {
 		Convey("can render normal tags without operator", func() {
 		Convey("can render normal tags without operator", func() {
 			query := &Query{Tags: []*Tag{&Tag{Operator: "", Value: `value`, Key: "key"}}}
 			query := &Query{Tags: []*Tag{&Tag{Operator: "", Value: `value`, Key: "key"}}}
 
 
-			So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 'value'`)
+			So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = 'value'`)
 		})
 		})
 
 
 		Convey("can render regex tags without operator", func() {
 		Convey("can render regex tags without operator", func() {
 			query := &Query{Tags: []*Tag{&Tag{Operator: "", Value: `/value/`, Key: "key"}}}
 			query := &Query{Tags: []*Tag{&Tag{Operator: "", Value: `/value/`, Key: "key"}}}
 
 
-			So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" =~ /value/`)
+			So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" =~ /value/`)
 		})
 		})
 
 
 		Convey("can render regex tags", func() {
 		Convey("can render regex tags", func() {
 			query := &Query{Tags: []*Tag{&Tag{Operator: "=~", Value: `/value/`, Key: "key"}}}
 			query := &Query{Tags: []*Tag{&Tag{Operator: "=~", Value: `/value/`, Key: "key"}}}
 
 
-			So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" =~ /value/`)
+			So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" =~ /value/`)
 		})
 		})
 
 
 		Convey("can render number tags", func() {
 		Convey("can render number tags", func() {
 			query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001", Key: "key"}}}
 			query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001", Key: "key"}}}
 
 
-			So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 10001`)
+			So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = '10001'`)
 		})
 		})
 
 
-		Convey("can render number tags with decimals", func() {
-			query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001.1", Key: "key"}}}
+		Convey("can render numbers less then condition tags", func() {
+			query := &Query{Tags: []*Tag{&Tag{Operator: "<", Value: "10001", Key: "key"}}}
 
 
-			So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 10001.1`)
+			So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" < 10001`)
+		})
+
+		Convey("can render number greather then condition tags", func() {
+			query := &Query{Tags: []*Tag{&Tag{Operator: ">", Value: "10001", Key: "key"}}}
+
+			So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" > 10001`)
 		})
 		})
 
 
 		Convey("can render string tags", func() {
 		Convey("can render string tags", func() {
 			query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "value", Key: "key"}}}
 			query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "value", Key: "key"}}}
 
 
-			So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 'value'`)
+			So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = 'value'`)
+		})
+
+		Convey("can render regular measurement", func() {
+			query := &Query{Measurement: `apa`, Policy: "policy"}
+
+			So(query.renderMeasurement(), ShouldEqual, ` FROM "policy"."apa"`)
+		})
+
+		Convey("can render regexp measurement", func() {
+			query := &Query{Measurement: `/apa/`, Policy: "policy"}
+
+			So(query.renderMeasurement(), ShouldEqual, ` FROM "policy"./apa/`)
 		})
 		})
 	})
 	})
 }
 }

+ 0 - 1
public/app/app.ts

@@ -40,7 +40,6 @@ export class GrafanaApp {
 
 
   init() {
   init() {
     var app = angular.module('grafana', []);
     var app = angular.module('grafana', []);
-    app.constant('grafanaVersion', "@grafanaVersion@");
 
 
     moment.locale(config.bootData.user.locale);
     moment.locale(config.bootData.user.locale);
 
 

+ 6 - 1
public/app/core/components/grafana_app.ts

@@ -147,9 +147,14 @@ export function grafanaAppDirective(playlistSrv, contextSrv) {
         }
         }
       }
       }
 
 
+      // mouse and keyboard is user activity
       body.mousemove(userActivityDetected);
       body.mousemove(userActivityDetected);
       body.keydown(userActivityDetected);
       body.keydown(userActivityDetected);
-      setInterval(checkForInActiveUser, 1000);
+      // treat tab change as activity
+      document.addEventListener('visibilitychange', userActivityDetected);
+
+      // check every 2 seconds
+      setInterval(checkForInActiveUser, 2000);
 
 
       appEvents.on('toggle-view-mode', () => {
       appEvents.on('toggle-view-mode', () => {
         lastActivity = 0;
         lastActivity = 0;

+ 1 - 2
public/app/core/core.ts

@@ -6,11 +6,10 @@ import "./directives/dash_class";
 import "./directives/confirm_click";
 import "./directives/confirm_click";
 import "./directives/dash_edit_link";
 import "./directives/dash_edit_link";
 import "./directives/dropdown_typeahead";
 import "./directives/dropdown_typeahead";
-import "./directives/grafana_version_check";
 import "./directives/metric_segment";
 import "./directives/metric_segment";
 import "./directives/misc";
 import "./directives/misc";
 import "./directives/ng_model_on_blur";
 import "./directives/ng_model_on_blur";
-import "./directives/password_strenght";
+import "./directives/password_strength";
 import "./directives/spectrum_picker";
 import "./directives/spectrum_picker";
 import "./directives/tags";
 import "./directives/tags";
 import "./directives/value_select_dropdown";
 import "./directives/value_select_dropdown";

+ 0 - 31
public/app/core/directives/grafana_version_check.js

@@ -1,31 +0,0 @@
-define([
-  '../core_module',
-],
-function (coreModule) {
-  'use strict';
-
-  coreModule.default.directive('grafanaVersionCheck', function($http, contextSrv) {
-    return {
-      restrict: 'A',
-      link: function(scope, elem) {
-        if (contextSrv.version === 'master') {
-          return;
-        }
-
-        $http({ method: 'GET', url: 'https://grafanarel.s3.amazonaws.com/latest.json' })
-        .then(function(response) {
-          if (!response.data || !response.data.version) {
-            return;
-          }
-
-          if (contextSrv.version !== response.data.version) {
-            elem.append('<i class="icon-info-sign"></i> ' +
-                        '<a href="http://grafana.org/download" target="_blank"> ' +
-            'New version available: ' + response.data.version +
-              '</a>');
-          }
-        });
-      }
-    };
-  });
-});

+ 1 - 1
public/app/core/directives/ng_model_on_blur.js

@@ -47,7 +47,7 @@ function (coreModule, kbn, rangeUtil) {
           if (ctrl.$isEmpty(modelValue)) {
           if (ctrl.$isEmpty(modelValue)) {
             return true;
             return true;
           }
           }
-          if (viewValue.indexOf('$') === 0) {
+          if (viewValue.indexOf('$') === 0 || viewValue.indexOf('+$') === 0) {
             return true; // allow template variable
             return true; // allow template variable
           }
           }
           var info = rangeUtil.describeTextRange(viewValue);
           var info = rangeUtil.describeTextRange(viewValue);

+ 0 - 0
public/app/core/directives/password_strenght.js → public/app/core/directives/password_strength.js


+ 3 - 3
public/app/core/utils/kbn.js

@@ -420,11 +420,11 @@ function($, _, moment) {
   kbn.valueFormats.bps    = kbn.formatBuilders.decimalSIPrefix('bps');
   kbn.valueFormats.bps    = kbn.formatBuilders.decimalSIPrefix('bps');
   kbn.valueFormats.Bps    = kbn.formatBuilders.decimalSIPrefix('Bps');
   kbn.valueFormats.Bps    = kbn.formatBuilders.decimalSIPrefix('Bps');
   kbn.valueFormats.KBs    = kbn.formatBuilders.decimalSIPrefix('Bs', 1);
   kbn.valueFormats.KBs    = kbn.formatBuilders.decimalSIPrefix('Bs', 1);
-  kbn.valueFormats.Kbits  = kbn.formatBuilders.decimalSIPrefix('bits', 1);
+  kbn.valueFormats.Kbits  = kbn.formatBuilders.decimalSIPrefix('bps', 1);
   kbn.valueFormats.MBs    = kbn.formatBuilders.decimalSIPrefix('Bs', 2);
   kbn.valueFormats.MBs    = kbn.formatBuilders.decimalSIPrefix('Bs', 2);
-  kbn.valueFormats.Mbits  = kbn.formatBuilders.decimalSIPrefix('bits', 2);
+  kbn.valueFormats.Mbits  = kbn.formatBuilders.decimalSIPrefix('bps', 2);
   kbn.valueFormats.GBs    = kbn.formatBuilders.decimalSIPrefix('Bs', 3);
   kbn.valueFormats.GBs    = kbn.formatBuilders.decimalSIPrefix('Bs', 3);
-  kbn.valueFormats.Gbits  = kbn.formatBuilders.decimalSIPrefix('bits', 3);
+  kbn.valueFormats.Gbits  = kbn.formatBuilders.decimalSIPrefix('bps', 3);
 
 
   // Throughput
   // Throughput
   kbn.valueFormats.ops  = kbn.formatBuilders.simpleCountUnit('ops');
   kbn.valueFormats.ops  = kbn.formatBuilders.simpleCountUnit('ops');

+ 4 - 0
public/app/features/admin/partials/stats.html

@@ -46,6 +46,10 @@
 				<td>Total starred dashboards</td>
 				<td>Total starred dashboards</td>
 				<td>{{ctrl.stats.starred_db_count}}</td>
 				<td>{{ctrl.stats.starred_db_count}}</td>
 			</tr>
 			</tr>
+      <tr>
+				<td>Total alerts</td>
+				<td>{{ctrl.stats.alert_count}}</td>
+			</tr>
 		</tbody>
 		</tbody>
 	</table>
 	</table>
 </div>
 </div>

+ 6 - 0
public/app/features/alerting/alert_def.ts

@@ -28,6 +28,11 @@ var evalFunctions = [
   {text: 'HAS NO VALUE' , value: 'no_value'}
   {text: 'HAS NO VALUE' , value: 'no_value'}
 ];
 ];
 
 
+var evalOperators = [
+  {text: 'OR', value: 'or'},
+  {text: 'AND', value: 'and'},
+];
+
 var reducerTypes = [
 var reducerTypes = [
   {text: 'avg()', value: 'avg'},
   {text: 'avg()', value: 'avg'},
   {text: 'min()', value: 'min'},
   {text: 'min()', value: 'min'},
@@ -116,6 +121,7 @@ export default {
   getStateDisplayModel: getStateDisplayModel,
   getStateDisplayModel: getStateDisplayModel,
   conditionTypes: conditionTypes,
   conditionTypes: conditionTypes,
   evalFunctions: evalFunctions,
   evalFunctions: evalFunctions,
+  evalOperators: evalOperators,
   noDataModes: noDataModes,
   noDataModes: noDataModes,
   executionErrorModes: executionErrorModes,
   executionErrorModes: executionErrorModes,
   reducerTypes: reducerTypes,
   reducerTypes: reducerTypes,

+ 4 - 0
public/app/features/alerting/alert_tab_ctrl.ts

@@ -18,6 +18,7 @@ export class AlertTabCtrl {
   alert: any;
   alert: any;
   conditionModels: any;
   conditionModels: any;
   evalFunctions: any;
   evalFunctions: any;
+  evalOperators: any;
   noDataModes: any;
   noDataModes: any;
   executionErrorModes: any;
   executionErrorModes: any;
   addNotificationSegment;
   addNotificationSegment;
@@ -41,6 +42,7 @@ export class AlertTabCtrl {
     this.$scope.ctrl = this;
     this.$scope.ctrl = this;
     this.subTabIndex = 0;
     this.subTabIndex = 0;
     this.evalFunctions = alertDef.evalFunctions;
     this.evalFunctions = alertDef.evalFunctions;
+    this.evalOperators = alertDef.evalOperators;
     this.conditionTypes = alertDef.conditionTypes;
     this.conditionTypes = alertDef.conditionTypes;
     this.noDataModes = alertDef.noDataModes;
     this.noDataModes = alertDef.noDataModes;
     this.executionErrorModes = alertDef.executionErrorModes;
     this.executionErrorModes = alertDef.executionErrorModes;
@@ -194,6 +196,7 @@ export class AlertTabCtrl {
       query: {params: ['A', '5m', 'now']},
       query: {params: ['A', '5m', 'now']},
       reducer: {type: 'avg', params: []},
       reducer: {type: 'avg', params: []},
       evaluator: {type: 'gt', params: [null]},
       evaluator: {type: 'gt', params: [null]},
+      operator: {type: 'and'},
     };
     };
   }
   }
 
 
@@ -250,6 +253,7 @@ export class AlertTabCtrl {
     cm.queryPart = new QueryPart(source.query, alertDef.alertQueryDef);
     cm.queryPart = new QueryPart(source.query, alertDef.alertQueryDef);
     cm.reducerPart = alertDef.createReducerPart(source.reducer);
     cm.reducerPart = alertDef.createReducerPart(source.reducer);
     cm.evaluator = source.evaluator;
     cm.evaluator = source.evaluator;
+    cm.operator = source.operator;
 
 
     return cm;
     return cm;
   }
   }

+ 5 - 5
public/app/features/alerting/partials/alert_tab.html

@@ -38,23 +38,23 @@
 				<h5 class="section-heading">Conditions</h5>
 				<h5 class="section-heading">Conditions</h5>
 				<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
 				<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
 					<div class="gf-form">
 					<div class="gf-form">
-						<span class="gf-form-label query-keyword width-5" ng-if="$index">AND</span>
+						<metric-segment-model css-class="query-keyword width-5" ng-if="$index" property="conditionModel.operator.type" options="ctrl.evalOperators" custom="false"></metric-segment-model>
 						<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
 						<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
 					</div>
 					</div>
           <div class="gf-form">
           <div class="gf-form">
-						<query-part-editor class="gf-form-label query-part" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
+						<query-part-editor class="gf-form-label query-part width-5" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
 						</query-part-editor>
 						</query-part-editor>
             <span class="gf-form-label query-keyword">OF</span>
             <span class="gf-form-label query-keyword">OF</span>
 					</div>
 					</div>
 					<div class="gf-form">
 					<div class="gf-form">
-						<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
+						<query-part-editor class="gf-form-label query-part width-10" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
 						</query-part-editor>
 						</query-part-editor>
 					</div>
 					</div>
 					<div class="gf-form">
 					<div class="gf-form">
 						<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
 						<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
-						<input class="gf-form-input max-width-7" type="number" step="any" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input>
+						<input class="gf-form-input max-width-9" type="number" step="any" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input>
             <label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
             <label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
-            <input class="gf-form-input max-width-7" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input>
+            <input class="gf-form-input max-width-9" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input>
 					</div>
 					</div>
 					<div class="gf-form">
 					<div class="gf-form">
 						<label class="gf-form-label">
 						<label class="gf-form-label">

+ 1 - 1
public/app/features/dashboard/dashboard_ctrl.ts

@@ -52,7 +52,7 @@ export class DashboardCtrl {
         .catch($scope.onInitFailed.bind(this, 'Templating init failed', false))
         .catch($scope.onInitFailed.bind(this, 'Templating init failed', false))
         // continue
         // continue
         .finally(function() {
         .finally(function() {
-          dynamicDashboardSrv.init(dashboard, variableSrv);
+          dynamicDashboardSrv.init(dashboard);
           dynamicDashboardSrv.process();
           dynamicDashboardSrv.process();
 
 
           unsavedChangesSrv.init(dashboard, $scope);
           unsavedChangesSrv.init(dashboard, $scope);

+ 7 - 3
public/app/features/dashboard/dynamic_dashboard_srv.ts

@@ -12,12 +12,12 @@ export class DynamicDashboardSrv {
   dashboard: any;
   dashboard: any;
   variables: any;
   variables: any;
 
 
-  init(dashboard, variableSrv) {
+  init(dashboard) {
     this.dashboard = dashboard;
     this.dashboard = dashboard;
-    this.variables = variableSrv.variables;
+    this.variables = dashboard.templating.list;
   }
   }
 
 
-  process(options) {
+  process(options?) {
     if (this.dashboard.snapshot || this.variables.length === 0) {
     if (this.dashboard.snapshot || this.variables.length === 0) {
       return;
       return;
     }
     }
@@ -31,6 +31,8 @@ export class DynamicDashboardSrv {
     // cleanup scopedVars
     // cleanup scopedVars
     for (i = 0; i < this.dashboard.rows.length; i++) {
     for (i = 0; i < this.dashboard.rows.length; i++) {
       row = this.dashboard.rows[i];
       row = this.dashboard.rows[i];
+      delete row.scopedVars;
+
       for (j = 0; j < row.panels.length; j++) {
       for (j = 0; j < row.panels.length; j++) {
         delete row.panels[j].scopedVars;
         delete row.panels[j].scopedVars;
       }
       }
@@ -64,6 +66,8 @@ export class DynamicDashboardSrv {
           j = j - 1;
           j = j - 1;
         }
         }
       }
       }
+
+      row.panelSpanChanged();
     }
     }
   }
   }
 
 

+ 1 - 3
public/app/features/dashboard/export/export_modal.ts

@@ -17,9 +17,7 @@ export class DashExportCtrl {
   constructor(private backendSrv, dashboardSrv, datasourceSrv, $scope) {
   constructor(private backendSrv, dashboardSrv, datasourceSrv, $scope) {
     this.exporter = new DashboardExporter(datasourceSrv);
     this.exporter = new DashboardExporter(datasourceSrv);
 
 
-    var current = dashboardSrv.getCurrent().getSaveModelClone();
-
-    this.exporter.makeExportable(current).then(dash => {
+    this.exporter.makeExportable(dashboardSrv.getCurrent()).then(dash => {
       $scope.$apply(() => {
       $scope.$apply(() => {
         this.dash = dash;
         this.dash = dash;
       });
       });

+ 29 - 8
public/app/features/dashboard/export/exporter.ts

@@ -11,19 +11,40 @@ export class DashboardExporter {
   constructor(private datasourceSrv) {
   constructor(private datasourceSrv) {
   }
   }
 
 
-  makeExportable(dash) {
+  makeExportable(dashboard) {
     var dynSrv = new DynamicDashboardSrv();
     var dynSrv = new DynamicDashboardSrv();
-    dynSrv.init(dash, {variables: dash.templating.list});
+
+    // clean up repeated rows and panels,
+    // this is done on the live real dashboard instance, not on a clone
+    // so we need to undo this
+    // this is pretty hacky and needs to be changed
+    dynSrv.init(dashboard);
     dynSrv.process({cleanUpOnly: true});
     dynSrv.process({cleanUpOnly: true});
 
 
-    dash.id = null;
+    var saveModel = dashboard.getSaveModelClone();
+    saveModel.id = null;
+
+    // undo repeat cleanup
+    dynSrv.process();
 
 
     var inputs = [];
     var inputs = [];
     var requires = {};
     var requires = {};
     var datasources = {};
     var datasources = {};
     var promises = [];
     var promises = [];
+    var variableLookup: any = {};
+
+    for (let variable of saveModel.templating.list) {
+      variableLookup[variable.name] = variable;
+    }
 
 
     var templateizeDatasourceUsage = obj => {
     var templateizeDatasourceUsage = obj => {
+      // ignore data source properties that contain a variable
+      if (obj.datasource && obj.datasource.indexOf('$') === 0) {
+        if (variableLookup[obj.datasource.substring(1)]){
+          return;
+        }
+      }
+
       promises.push(this.datasourceSrv.get(obj.datasource).then(ds => {
       promises.push(this.datasourceSrv.get(obj.datasource).then(ds => {
         if (ds.meta.builtIn) {
         if (ds.meta.builtIn) {
           return;
           return;
@@ -50,7 +71,7 @@ export class DashboardExporter {
     };
     };
 
 
     // check up panel data sources
     // check up panel data sources
-    for (let row of dash.rows) {
+    for (let row of saveModel.rows) {
       for (let panel of row.panels) {
       for (let panel of row.panels) {
         if (panel.datasource !== undefined) {
         if (panel.datasource !== undefined) {
           templateizeDatasourceUsage(panel);
           templateizeDatasourceUsage(panel);
@@ -77,7 +98,7 @@ export class DashboardExporter {
     }
     }
 
 
     // templatize template vars
     // templatize template vars
-    for (let variable of dash.templating.list) {
+    for (let variable of saveModel.templating.list) {
       if (variable.type === 'query') {
       if (variable.type === 'query') {
         templateizeDatasourceUsage(variable);
         templateizeDatasourceUsage(variable);
         variable.options = [];
         variable.options = [];
@@ -87,7 +108,7 @@ export class DashboardExporter {
     }
     }
 
 
     // templatize annotations vars
     // templatize annotations vars
-    for (let annotationDef of dash.annotations.list) {
+    for (let annotationDef of saveModel.annotations.list) {
       templateizeDatasourceUsage(annotationDef);
       templateizeDatasourceUsage(annotationDef);
     }
     }
 
 
@@ -105,7 +126,7 @@ export class DashboardExporter {
       });
       });
 
 
       // templatize constants
       // templatize constants
-      for (let variable of dash.templating.list) {
+      for (let variable of saveModel.templating.list) {
         if (variable.type === 'constant') {
         if (variable.type === 'constant') {
           var refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase();
           var refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase();
           inputs.push({
           inputs.push({
@@ -133,7 +154,7 @@ export class DashboardExporter {
       newObj["__inputs"] = inputs;
       newObj["__inputs"] = inputs;
       newObj["__requires"] = requires;
       newObj["__requires"] = requires;
 
 
-      _.defaults(newObj, dash);
+      _.defaults(newObj, saveModel);
 
 
       return newObj;
       return newObj;
     }).catch(err => {
     }).catch(err => {

+ 8 - 5
public/app/features/dashboard/model.ts

@@ -98,12 +98,14 @@ export class DashboardModel {
     var events = this.events;
     var events = this.events;
     var meta = this.meta;
     var meta = this.meta;
     var rows = this.rows;
     var rows = this.rows;
+    var variables = this.templating.list;
+
     delete this.events;
     delete this.events;
     delete this.meta;
     delete this.meta;
 
 
     // prepare save model
     // prepare save model
-    this.rows = _.map(this.rows, row => row.getSaveModel());
-    events.emit('prepare-save-model');
+    this.rows = _.map(rows, row => row.getSaveModel());
+    this.templating.list = _.map(variables, variable => variable.getSaveModel ? variable.getSaveModel() : variable);
 
 
     var copy = $.extend(true, {}, this);
     var copy = $.extend(true, {}, this);
 
 
@@ -111,6 +113,8 @@ export class DashboardModel {
     this.events = events;
     this.events = events;
     this.meta = meta;
     this.meta = meta;
     this.rows = rows;
     this.rows = rows;
+    this.templating.list = variables;
+
     return copy;
     return copy;
   }
   }
 
 
@@ -233,7 +237,6 @@ export class DashboardModel {
   }
   }
 
 
   duplicatePanel(panel, row) {
   duplicatePanel(panel, row) {
-    var rowIndex = _.indexOf(this.rows, row);
     var newPanel = angular.copy(panel);
     var newPanel = angular.copy(panel);
     newPanel.id = this.getNextPanelId();
     newPanel.id = this.getNextPanelId();
 
 
@@ -241,9 +244,9 @@ export class DashboardModel {
     delete newPanel.repeatIteration;
     delete newPanel.repeatIteration;
     delete newPanel.repeatPanelId;
     delete newPanel.repeatPanelId;
     delete newPanel.scopedVars;
     delete newPanel.scopedVars;
+    delete newPanel.alert;
 
 
-    var currentRow = this.rows[rowIndex];
-    currentRow.panels.push(newPanel);
+    row.addPanel(newPanel);
     return newPanel;
     return newPanel;
   }
   }
 
 

+ 0 - 282
public/app/features/dashboard/partials/globalAlerts.html

@@ -1,282 +0,0 @@
-<topnav title="Alerting" subnav="false">
-  <ul class="nav">
-    <li class="active" ><a href="global-alerts">Global Alerts</a></li>
-  </ul>
-</topnav>
-
-<div class="page-container">
-  <div class="page-wide">
-    <h1>Global alerts</h1>
-
-    <div class="filter-controls-filters">
-      <div class="tight-form last">
-        <ul class="tight-form-list">
-          <li class="tight-form-item">Filters:</li>
-          <li class="tight-form-item">Alert State</li>
-          <li><!-- <value-select-dropdown></value-select-dropdown> --></li>
-          <li class="tight-form-item">Dashboards</li>
-          <li><!-- <value-select-dropdown></value-select-dropdown> --></li>
-          <li class="tight-form-item">
-            <a class="pointer">
-              <i class="fa fa-pencil"></i>
-            </a>
-          </li>
-        </ul>
-        <div class="clearfix"></div>
-      </div>
-    </div>
-    <ul class="filter-controls-actions">
-      <li>
-        <div class="dropdown">
-          <button class="btn btn-inverse dropdown-toggle" data-toggle="dropdown">
-            <input class="cr1" id="state-enabled" type="checkbox">
-            <label for="state-enabled" class="cr1"></label> <span class="caret"></span>
-          </button>
-          <ul class="dropdown-menu" role="menu">
-            <li><a>All</a></li>
-          </ul>
-        </div>
-      </li>
-      <li>
-        <div class="dropdown">
-          <button class="btn btn-inverse dropdown-toggle" data-toggle="dropdown">
-            Bulk Actions &nbsp; <span class="caret"></span>
-          </button>
-          <ul class="dropdown-menu" role="menu">
-            <li><a>Update notifications</a></li>
-          </ul>
-        </div>
-      </li>
-      <li>
-        <button class="btn btn-inverse" data-toggle="dropdown">
-          <i class="fa fa-fw fa-th-large"></i> New Dashboard from selected
-        </button>
-      </li>
-      <li>
-        <span class="filter-controls-actions-selected">2 selected, showing 6 of 6 total</span>
-      </li>
-    </ul>
-    <ul class="filter-list">
-      <li>
-        <ul class="filter-list-card">
-          <li class="filter-list-card-select">
-            <input class="cr1" id="alert1" type="checkbox">
-            <label for="alert1" class="cr1"></label>
-          </li>
-          <li>
-            <div class="filter-list-card-controls">
-              <div class="filter-list-card-links">
-                <span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Super Sekret</a></span>
-                <span class="filter-list-card-link">Panel: <a href="">Prod CPU Data Writes</a></span>
-              </div>
-              <div class="filter-list-card-config">
-                <a href="#"><i class="fa fa-cog"></i></a>
-              </div>
-              <div class="filter-list-card-expand" ng-click="alert1.expanded = !alert1.expanded">
-                <i class="fa fa-angle-right" ng-show="!alert1.expanded"></i>
-                <i class="fa fa-angle-down" ng-show="alert1.expanded"></i>
-              </div>
-            </div>
-            <span class="filter-list-card-title">Prod CPU Data Writes</span>
-            <span class="filter-list-card-status">
-              <span class="filter-list-card-state online">Online</span> for 19 hours
-            </span>
-          </li>
-        </ul>
-        <div class="filter-list-card-details" ng-show="alert1.expanded">
-          <h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
-          <div class="tight-form last">
-            <ul class="tight-form-list">
-              <li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
-              <li class="tight-form-item">apps</li>
-              <li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
-              <li class="tight-form-item">fakesite</li>
-              <li class="tight-form-item">counters</li>
-              <li class="tight-form-item">requests</li>
-              <li class="tight-form-item">count</li>
-              <li class="tight-form-item">scaleToSeconds(1)</li>
-              <li class="tight-form-item">aliasByNode(2)</li>
-            </ul>
-            <div class="clearfix"></div>
-          </div>
-        </div>
-      </li>
-      <li>
-        <ul class="filter-list-card">
-          <li class="filter-list-card-select">
-            <input class="cr1" id="alert2" type="checkbox" checked>
-            <label for="alert2" class="cr1"></label>
-          </li>
-          <li>
-            <div class="filter-list-card-controls">
-              <div class="filter-list-card-links">
-                <span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Insanely Super Duper Sekret</a></span>
-                <span class="filter-list-card-link">Panel: <a href="">client side full page load</a></span>
-              </div>
-              <div class="filter-list-card-config">
-                <a href="#"><i class="fa fa-cog"></i></a>
-              </div>
-              <div class="filter-list-card-expand" ng-click="alert2.expanded = !alert2.expanded">
-                <i class="fa fa-angle-right" ng-show="!alert2.expanded"></i>
-                <i class="fa fa-angle-down" ng-show="alert2.expanded"></i>
-              </div>
-            </div>
-            <span class="filter-list-card-title">Prod DB Reads</span>
-            <span class="filter-list-card-status">
-              <span class="filter-list-card-state warn">Warn</span> for 1 hour
-            </span>
-          </li>
-        </ul>
-        <div class="filter-list-card-details" ng-show="alert2.expanded">
-          <h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
-          <div class="tight-form last">
-            <ul class="tight-form-list">
-              <li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
-              <li class="tight-form-item">apps</li>
-              <li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
-              <li class="tight-form-item">fakesite</li>
-              <li class="tight-form-item">counters</li>
-              <li class="tight-form-item">requests</li>
-              <li class="tight-form-item">count</li>
-              <li class="tight-form-item">scaleToSeconds(1)</li>
-              <li class="tight-form-item">aliasByNode(2)</li>
-            </ul>
-            <div class="clearfix"></div>
-          </div>
-        </div>
-      </li>
-      <li>
-        <ul class="filter-list-card">
-          <li class="filter-list-card-select">
-            <input class="cr1" id="alert3" type="checkbox" checked>
-            <label for="alert3" class="cr1"></label>
-          </li>
-          <li>
-            <div class="filter-list-card-controls">
-              <div class="filter-list-card-links">
-                <span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Mildly Sekret</a></span>
-                <span class="filter-list-card-link">Panel: <a href="">Memory/CPU</a></span>
-              </div>
-              <div class="filter-list-card-config">
-                <a href="#"><i class="fa fa-cog"></i></a>
-              </div>
-              <div class="filter-list-card-expand" ng-click="alert3.expanded = !alert3.expanded">
-                <i class="fa fa-angle-right" ng-show="!alert3.expanded"></i>
-                <i class="fa fa-angle-down" ng-show="alert3.expanded"></i>
-              </div>
-            </div>
-            <span class="filter-list-card-title">Prod CPU Data Writes</span>
-            <span class="filter-list-card-status">
-              <span class="filter-list-card-state critical">Online</span> for 10 minutes
-            </span>
-          </li>
-        </ul>
-        <div class="filter-list-card-details" ng-show="alert3.expanded">
-          <h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
-          <div class="tight-form last">
-            <ul class="tight-form-list">
-              <li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
-              <li class="tight-form-item">apps</li>
-              <li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
-              <li class="tight-form-item">fakesite</li>
-              <li class="tight-form-item">counters</li>
-              <li class="tight-form-item">requests</li>
-              <li class="tight-form-item">count</li>
-              <li class="tight-form-item">scaleToSeconds(1)</li>
-              <li class="tight-form-item">aliasByNode(2)</li>
-            </ul>
-            <div class="clearfix"></div>
-          </div>
-        </div>
-      </li>
-      <li>
-        <ul class="filter-list-card">
-          <li class="filter-list-card-select">
-            <input class="cr1" id="alert4" type="checkbox">
-            <label for="alert4" class="cr1"></label>
-          </li>
-          <li>
-            <div class="filter-list-card-controls">
-              <div class="filter-list-card-links">
-                <span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Super Sekret</a></span>
-                <span class="filter-list-card-link">Panel: <a href="">Stacked lines</a></span>
-              </div>
-              <div class="filter-list-card-config">
-                <a href="#"><i class="fa fa-cog"></i></a>
-              </div>
-              <div class="filter-list-card-expand" ng-click="alert4.expanded = !alert4.expanded">
-                <i class="fa fa-angle-right" ng-show="!alert4.expanded"></i>
-                <i class="fa fa-angle-down" ng-show="alert4.expanded"></i>
-              </div>
-            </div>
-            <span class="filter-list-card-title">Critical Thing</span>
-            <span class="filter-list-card-status">
-              <span class="filter-list-card-state online">Online</span> for 5 weeks
-            </span>
-          </li>
-        </ul>
-        <div class="filter-list-card-details" ng-show="alert4.expanded">
-          <h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
-          <div class="tight-form last">
-            <ul class="tight-form-list">
-              <li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
-              <li class="tight-form-item">apps</li>
-              <li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
-              <li class="tight-form-item">fakesite</li>
-              <li class="tight-form-item">counters</li>
-              <li class="tight-form-item">requests</li>
-              <li class="tight-form-item">count</li>
-              <li class="tight-form-item">scaleToSeconds(1)</li>
-              <li class="tight-form-item">aliasByNode(2)</li>
-            </ul>
-            <div class="clearfix"></div>
-          </div>
-        </div>
-      </li>
-      <li>
-        <ul class="filter-list-card">
-          <li class="filter-list-card-select">
-            <input class="cr1" id="alert5" type="checkbox">
-            <label for="alert5" class="cr1"></label>
-          </li>
-          <li>
-            <div class="filter-list-card-controls">
-              <div class="filter-list-card-links">
-                <span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Public</a></span>
-                <span class="filter-list-card-link">Panel: <a href="">More Critical Thing</a></span>
-              </div>
-              <div class="filter-list-card-config">
-                <a href="#"><i class="fa fa-cog"></i></a>
-              </div>
-              <div class="filter-list-card-expand" ng-click="alert5.expanded = !alert5.expanded">
-                <i class="fa fa-angle-right" ng-show="!alert5.expanded"></i>
-                <i class="fa fa-angle-down" ng-show="alert5.expanded"></i>
-              </div>
-            </div>
-            <span class="filter-list-card-title">More Critical Thing</span>
-            <span class="filter-list-card-status">
-              <span class="filter-list-card-state online">Online</span> for 2 months
-            </span>
-          </li>
-        </ul>
-        <div class="filter-list-card-details" ng-show="alert5.expanded">
-          <h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
-          <div class="tight-form last">
-            <ul class="tight-form-list">
-              <li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
-              <li class="tight-form-item">apps</li>
-              <li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
-              <li class="tight-form-item">fakesite</li>
-              <li class="tight-form-item">counters</li>
-              <li class="tight-form-item">requests</li>
-              <li class="tight-form-item">count</li>
-              <li class="tight-form-item">scaleToSeconds(1)</li>
-              <li class="tight-form-item">aliasByNode(2)</li>
-            </ul>
-            <div class="clearfix"></div>
-          </div>
-        </div>
-      </li>
-    </ul>
-  </div>
-</div>

+ 1 - 1
public/app/features/dashboard/row/add_panel.html

@@ -5,7 +5,7 @@
 
 
   <div class="gf-form-inline dash-row-add-panel-form">
   <div class="gf-form-inline dash-row-add-panel-form">
     <div class="gf-form">
     <div class="gf-form">
-      <input type="text" class="gf-form-input max-width-14" ng-model='ctrl.panelSearch' give-focus='true' ng-keydown="ctrl.keyDown($event)" ng-change="ctrl.panelSearchChanged()" placeholder="panel search filter" ng-blur="ctrl.panelSearchBlur()"></input>
+      <input type="text" class="gf-form-input max-width-14" ng-model='ctrl.panelSearch' give-focus='true' ng-keydown="ctrl.keyDown($event)" ng-change="ctrl.panelSearchChanged()" placeholder="panel search filter"></input>
     </div>
     </div>
   </div>
   </div>
 
 

+ 0 - 6
public/app/features/dashboard/row/add_panel.ts

@@ -45,12 +45,6 @@ export class AddPanelCtrl {
     }
     }
   }
   }
 
 
-  panelSearchBlur() {
-    // this.$timeout(() => {
-    //   this.rowCtrl.dropView = 0;
-    // }, 400);
-  }
-
   moveSelection(direction) {
   moveSelection(direction) {
     var max = this.panelHits.length;
     var max = this.panelHits.length;
     var newIndex = this.activeIndex + direction;
     var newIndex = this.activeIndex + direction;

+ 2 - 3
public/app/features/dashboard/row/row_ctrl.ts

@@ -19,7 +19,6 @@ export class DashRowCtrl {
 
 
     if (this.row.isNew) {
     if (this.row.isNew) {
       this.dropView = 1;
       this.dropView = 1;
-      delete this.row.isNew;
     }
     }
   }
   }
 
 
@@ -35,8 +34,8 @@ export class DashRowCtrl {
           title: config.new_panel_title,
           title: config.new_panel_title,
           type: panelId,
           type: panelId,
           id: this.dashboard.getNextPanelId(),
           id: this.dashboard.getNextPanelId(),
+          isNew: true,
         },
         },
-        isNew: true,
       };
       };
     } else {
     } else {
       dragObject = this.dashboard.getPanelInfoById(panelId);
       dragObject = this.dashboard.getPanelInfoById(panelId);
@@ -65,7 +64,7 @@ export class DashRowCtrl {
       this.row.panels.push(dragObject.panel);
       this.row.panels.push(dragObject.panel);
 
 
       // if not new remove from source row
       // if not new remove from source row
-      if (!dragObject.isNew) {
+      if (!dragObject.panel.isNew) {
         dragObject.row.removePanel(dragObject.panel, false);
         dragObject.row.removePanel(dragObject.panel, false);
       }
       }
     }
     }

+ 4 - 0
public/app/features/dashboard/row/row_model.ts

@@ -32,7 +32,11 @@ export class DashboardRow {
   }
   }
 
 
   getSaveModel() {
   getSaveModel() {
+    this.model = {};
     assignModelProperties(this.model, this, this.defaults);
     assignModelProperties(this.model, this, this.defaults);
+
+    // remove properties that dont server persisted purpose
+    delete this.model.isNew;
     return this.model;
     return this.model;
   }
   }
 
 

+ 6 - 2
public/app/features/dashboard/specs/dashboard_srv_specs.ts

@@ -62,7 +62,9 @@ describe('dashboardSrv', function() {
 
 
     it('duplicate panel should try to add it to same row', function() {
     it('duplicate panel should try to add it to same row', function() {
       var panel = { span: 4, attr: '123', id: 10 };
       var panel = { span: 4, attr: '123', id: 10 };
-      dashboard.rows = [{ panels: [panel] }];
+
+      dashboard.addEmptyRow();
+      dashboard.rows[0].addPanel(panel);
       dashboard.duplicatePanel(panel, dashboard.rows[0]);
       dashboard.duplicatePanel(panel, dashboard.rows[0]);
 
 
       expect(dashboard.rows[0].panels[0].span).to.be(4);
       expect(dashboard.rows[0].panels[0].span).to.be(4);
@@ -73,7 +75,9 @@ describe('dashboardSrv', function() {
 
 
     it('duplicate panel should remove repeat data', function() {
     it('duplicate panel should remove repeat data', function() {
       var panel = { span: 4, attr: '123', id: 10, repeat: 'asd', scopedVars: { test: 'asd' }};
       var panel = { span: 4, attr: '123', id: 10, repeat: 'asd', scopedVars: { test: 'asd' }};
-      dashboard.rows = [{ panels: [panel] }];
+
+      dashboard.addEmptyRow();
+      dashboard.rows[0].addPanel(panel);
       dashboard.duplicatePanel(panel, dashboard.rows[0]);
       dashboard.duplicatePanel(panel, dashboard.rows[0]);
 
 
       expect(dashboard.rows[0].panels[1].repeat).to.be(undefined);
       expect(dashboard.rows[0].panels[1].repeat).to.be(undefined);

+ 1 - 3
public/app/features/dashboard/specs/dynamic_dashboard_srv_specs.ts

@@ -20,7 +20,6 @@ function dynamicDashScenario(desc, func)  {
 
 
       beforeEach(angularMocks.inject(function(dashboardSrv) {
       beforeEach(angularMocks.inject(function(dashboardSrv) {
         ctx.dashboardSrv = dashboardSrv;
         ctx.dashboardSrv = dashboardSrv;
-        ctx.variableSrv = {};
 
 
         var model = {
         var model = {
           rows: [],
           rows: [],
@@ -29,9 +28,8 @@ function dynamicDashScenario(desc, func)  {
 
 
         setupFunc(model);
         setupFunc(model);
         ctx.dash = ctx.dashboardSrv.create(model);
         ctx.dash = ctx.dashboardSrv.create(model);
-        ctx.variableSrv.variables = ctx.dash.templating.list;
         ctx.dynamicDashboardSrv = new DynamicDashboardSrv();
         ctx.dynamicDashboardSrv = new DynamicDashboardSrv();
-        ctx.dynamicDashboardSrv.init(ctx.dash, ctx.variableSrv);
+        ctx.dynamicDashboardSrv.init(ctx.dash);
         ctx.dynamicDashboardSrv.process();
         ctx.dynamicDashboardSrv.process();
         ctx.rows = ctx.dash.rows;
         ctx.rows = ctx.dash.rows;
       }));
       }));

+ 10 - 1
public/app/features/dashboard/specs/exporter_specs.ts

@@ -34,6 +34,14 @@ describe('given dashboard with repeated panels', function() {
       options: []
       options: []
     });
     });
 
 
+    dash.templating.list.push({
+      name: 'ds',
+      type: 'datasource',
+      query: 'testdb',
+      current: {value: 'prod', text: 'prod'},
+      options: []
+    });
+
     dash.annotations.list.push({
     dash.annotations.list.push({
       name: 'logs',
       name: 'logs',
       datasource: 'gfdb',
       datasource: 'gfdb',
@@ -49,6 +57,7 @@ describe('given dashboard with repeated panels', function() {
           datasource: '-- Mixed --',
           datasource: '-- Mixed --',
           targets: [{datasource: 'other'}],
           targets: [{datasource: 'other'}],
         },
         },
+        {id: 5, datasource: '$ds'},
       ]
       ]
     });
     });
 
 
@@ -87,7 +96,7 @@ describe('given dashboard with repeated panels', function() {
   });
   });
 
 
   it('exported dashboard should not contain repeated panels', function() {
   it('exported dashboard should not contain repeated panels', function() {
-    expect(exported.rows[0].panels.length).to.be(2);
+    expect(exported.rows[0].panels.length).to.be(3);
   });
   });
 
 
   it('exported dashboard should not contain repeated rows', function() {
   it('exported dashboard should not contain repeated rows', function() {

+ 9 - 0
public/app/features/panel/panel_ctrl.ts

@@ -54,6 +54,12 @@ export class PanelCtrl {
       this.events.emit('panel-teardown');
       this.events.emit('panel-teardown');
       this.events.removeAllListeners();
       this.events.removeAllListeners();
     });
     });
+
+    // we should do something interesting
+    // with newly added panels
+    if (this.panel.isNew) {
+      delete this.panel.isNew;
+    }
   }
   }
 
 
   init() {
   init() {
@@ -188,6 +194,9 @@ export class PanelCtrl {
 
 
   duplicate() {
   duplicate() {
     this.dashboard.duplicatePanel(this.panel, this.row);
     this.dashboard.duplicatePanel(this.panel, this.row);
+    this.$timeout(() => {
+      this.$scope.$root.$broadcast('render');
+    });
   }
   }
 
 
   updateColumnSpan(span) {
   updateColumnSpan(span) {

+ 8 - 2
public/app/features/panel/panel_directive.ts

@@ -68,8 +68,8 @@ module.directive('grafanaPanel', function($rootScope) {
 
 
       // the reason for handling these classes this way is for performance
       // the reason for handling these classes this way is for performance
       // limit the watchers on panels etc
       // limit the watchers on panels etc
-      var transparentLastState;
-      var lastHasAlertRule;
+      var transparentLastState = false;
+      var lastHasAlertRule = false;
       var lastAlertState;
       var lastAlertState;
       var hasAlertRule;
       var hasAlertRule;
       var lastHeight = 0;
       var lastHeight = 0;
@@ -91,6 +91,12 @@ module.directive('grafanaPanel', function($rootScope) {
         lastHeight = ctrl.containerHeight;
         lastHeight = ctrl.containerHeight;
       }
       }
 
 
+      // set initial transparency
+      if (ctrl.panel.transparent) {
+        transparentLastState = true;
+        panelContainer.addClass('panel-transparent', true);
+      }
+
       ctrl.events.on('render', () => {
       ctrl.events.on('render', () => {
         if (lastHeight !== ctrl.containerHeight) {
         if (lastHeight !== ctrl.containerHeight) {
           panelContainer.css({minHeight: ctrl.containerHeight});
           panelContainer.css({minHeight: ctrl.containerHeight});

+ 0 - 56
public/app/features/panel/partials/query_editor_row.html

@@ -57,59 +57,3 @@
 	</div>
 	</div>
 </div>
 </div>
 
 
-<div class="tight-form" ng-if="false">
-	<ul class="tight-form-list pull-right">
-		<li ng-show="ctrl.error" class="tight-form-item">
-			<a bs-tooltip="ctrl.error" style="color: rgb(229, 189, 28)" role="menuitem">
-				<i class="fa fa-warning"></i>
-			</a>
-		</li>
-		<li class="tight-form-item small" ng-show="ctrl.target.datasource">
-			<em>{{ctrl.target.datasource}}</em>
-		</li>
-		<li class="tight-form-item" ng-if="ctrl.toggleEditorMode">
-			<a class="pointer" tabindex="1" ng-click="ctrl.toggleEditorMode()">
-				<i class="fa fa-pencil"></i>
-			</a>
-		</li>
-		<li class="tight-form-item">
-			<div class="dropdown">
-				<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
-					<i class="fa fa-bars"></i>
-				</a>
-				<ul class="dropdown-menu pull-right" role="menu">
-					<li role="menuitem">
-						<a tabindex="1" ng-click="ctrl.duplicateQuery()">Duplicate</a>
-					</li>
-					<li role="menuitem">
-						<a tabindex="1" ng-click="ctrl.moveQuery(-1)">Move up</a>
-					</li>
-					<li role="menuitem">
-						<a tabindex="1" ng-click="ctrl.moveQuery(1)">Move down</a>
-					</li>
-				</ul>
-			</div>
-		</li>
-		<li class="tight-form-item last">
-			<a class="pointer" tabindex="1" ng-click="ctrl.removeQuery(target)">
-				<i class="fa fa-trash"></i>
-			</a>
-		</li>
-	</ul>
-
-	<ul class="tight-form-list">
-		<li class="tight-form-item" style="min-width: 15px; text-align: center">
-			{{ctrl.target.refId}}
-		</li>
-		<li>
-			<a class="tight-form-item" ng-click="ctrl.toggleHideQuery()" role="menuitem">
-				<i class="fa fa-eye"></i>
-			</a>
-		</li>
-	</ul>
-
-	<ul class="tight-form-list" ng-transclude>
-	</ul>
-
-	<div class="clearfix"></div>
-</div>

+ 1 - 1
public/app/features/templating/adhoc_variable.ts

@@ -26,7 +26,7 @@ export class AdhocVariable implements Variable {
     return Promise.resolve();
     return Promise.resolve();
   }
   }
 
 
-  getModel() {
+  getSaveModel() {
     assignModelProperties(this.model, this, this.defaults);
     assignModelProperties(this.model, this, this.defaults);
     return this.model;
     return this.model;
   }
   }

+ 1 - 1
public/app/features/templating/constant_variable.ts

@@ -24,7 +24,7 @@ export class ConstantVariable implements Variable {
     assignModelProperties(this, model, this.defaults);
     assignModelProperties(this, model, this.defaults);
   }
   }
 
 
-  getModel() {
+  getSaveModel() {
     assignModelProperties(this.model, this, this.defaults);
     assignModelProperties(this.model, this, this.defaults);
     return this.model;
     return this.model;
   }
   }

+ 1 - 1
public/app/features/templating/custom_variable.ts

@@ -34,7 +34,7 @@ export class CustomVariable implements Variable {
     return this.variableSrv.setOptionAsCurrent(this, option);
     return this.variableSrv.setOptionAsCurrent(this, option);
   }
   }
 
 
-  getModel() {
+  getSaveModel() {
     assignModelProperties(this.model, this, this.defaults);
     assignModelProperties(this.model, this, this.defaults);
     return this.model;
     return this.model;
   }
   }

+ 4 - 1
public/app/features/templating/datasource_variable.ts

@@ -30,8 +30,11 @@ export class DatasourceVariable implements Variable {
     this.refresh = 1;
     this.refresh = 1;
   }
   }
 
 
-  getModel() {
+  getSaveModel() {
     assignModelProperties(this.model, this, this.defaults);
     assignModelProperties(this.model, this, this.defaults);
+
+    // dont persist options
+    this.model.options = [];
     return this.model;
     return this.model;
   }
   }
 
 

+ 1 - 1
public/app/features/templating/interval_variable.ts

@@ -34,7 +34,7 @@ export class IntervalVariable implements Variable {
     this.refresh = 2;
     this.refresh = 2;
   }
   }
 
 
-  getModel() {
+  getSaveModel() {
     assignModelProperties(this.model, this, this.defaults);
     assignModelProperties(this.model, this, this.defaults);
     return this.model;
     return this.model;
   }
   }

+ 1 - 1
public/app/features/templating/partials/editor.html

@@ -136,7 +136,7 @@
 			<div ng-if="current.type === 'custom'" class="gf-form-group">
 			<div ng-if="current.type === 'custom'" class="gf-form-group">
         <h5 class="section-heading">Custom Options</h5>
         <h5 class="section-heading">Custom Options</h5>
 				<div class="gf-form">
 				<div class="gf-form">
-					<span class="gf-form-label width-13">Values separated by comma</span>
+					<span class="gf-form-label width-14">Values separated by comma</span>
 					<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue" required></input>
 					<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue" required></input>
 				</div>
 				</div>
 			</div>
 			</div>

+ 7 - 1
public/app/features/templating/query_variable.ts

@@ -47,9 +47,15 @@ export class QueryVariable implements Variable {
     assignModelProperties(this, model, this.defaults);
     assignModelProperties(this, model, this.defaults);
   }
   }
 
 
-  getModel() {
+  getSaveModel() {
     // copy back model properties to model
     // copy back model properties to model
     assignModelProperties(this.model, this, this.defaults);
     assignModelProperties(this.model, this, this.defaults);
+
+    // remove options
+    if (this.refresh !== 0) {
+      this.model.options = [];
+    }
+
     return this.model;
     return this.model;
   }
   }
 
 

+ 9 - 2
public/app/features/templating/specs/query_variable_specs.ts

@@ -25,7 +25,7 @@ describe('QueryVariable', function() {
       variable.regex = 'asd';
       variable.regex = 'asd';
       variable.sort = 50;
       variable.sort = 50;
 
 
-      var model = variable.getModel();
+      var model = variable.getSaveModel();
       expect(model.options.length).to.be(1);
       expect(model.options.length).to.be(1);
       expect(model.options[0].text).to.be('test');
       expect(model.options[0].text).to.be('test');
       expect(model.datasource).to.be('google');
       expect(model.datasource).to.be('google');
@@ -33,7 +33,14 @@ describe('QueryVariable', function() {
       expect(model.sort).to.be(50);
       expect(model.sort).to.be(50);
     });
     });
 
 
-  });
+    it('if refresh != 0 then remove options in presisted mode', () => {
+      var variable = new QueryVariable({}, null, null, null, null);
+      variable.options = [{text: 'test'}];
+      variable.refresh = 1;
 
 
+      var model = variable.getSaveModel();
+      expect(model.options.length).to.be(0);
+    });
+  });
 });
 });
 
 

+ 1 - 1
public/app/features/templating/variable.ts

@@ -10,7 +10,7 @@ export interface Variable {
   dependsOn(variable);
   dependsOn(variable);
   setValueFromUrl(urlValue);
   setValueFromUrl(urlValue);
   getValueForUrl();
   getValueForUrl();
-  getModel();
+  getSaveModel();
 }
 }
 
 
 export var variableTypes = {};
 export var variableTypes = {};

+ 1 - 10
public/app/features/templating/variable_srv.ts

@@ -20,12 +20,9 @@ export class VariableSrv {
     this.dashboard = dashboard;
     this.dashboard = dashboard;
 
 
     // create working class models representing variables
     // create working class models representing variables
-    this.variables = dashboard.templating.list.map(this.createVariableFromModel.bind(this));
+    this.variables = dashboard.templating.list = dashboard.templating.list.map(this.createVariableFromModel.bind(this));
     this.templateSrv.init(this.variables);
     this.templateSrv.init(this.variables);
 
 
-    // register event to sync back to persisted model
-    this.dashboard.events.on('prepare-save-model', this.syncToDashboardModel.bind(this));
-
     // init variables
     // init variables
     for (let variable of this.variables) {
     for (let variable of this.variables) {
       variable.initLock = this.$q.defer();
       variable.initLock = this.$q.defer();
@@ -99,12 +96,6 @@ export class VariableSrv {
     return variable;
     return variable;
   }
   }
 
 
-  syncToDashboardModel() {
-    this.dashboard.templating.list = this.variables.map(variable => {
-      return variable.getModel();
-    });
-  }
-
   updateOptions(variable) {
   updateOptions(variable) {
     return variable.updateOptions();
     return variable.updateOptions();
   }
   }

+ 0 - 3
public/app/partials/bootstrap/tab.html

@@ -1,3 +0,0 @@
-<li ng-class="{active: active, disabled: disabled}">
-  <a href ng-click="select()" tab-heading-transclude>{{heading}}</a>
-</li>

+ 0 - 11
public/app/partials/bootstrap/tabset.html

@@ -1,11 +0,0 @@
-<div>
-	<ul class="nav nav-tabs" ng-class="{'nav-stacked': vertical, 'nav-justified': justified}" ng-transclude>
-	</ul>
-  <div class="tab-content">
-    <div class="tab-pane"
-         ng-repeat="tab in tabs"
-         ng-class="{active: tab.active}"
-         tab-content-transclude="tab">
-    </div>
-  </div>
-</div>

+ 8 - 7
public/app/partials/error.html

@@ -1,11 +1,12 @@
+<navbar title="404" icon="fa fa-fw fa-question" title-url="/">
+</navbar>
 
 
-<div class="row-fluid" style="margin-top: 100px;">
-	<div class="span2"></div>
+<div class="page-container">
 
 
-	<div class="grafana-info-box span8 text-center">
-			<h3>Page not found (404)</h3>
-		</div>
-
-	<div class="span2"></div>
+  <div class="page-header">
+    <h1>
+			Page not found (404)
+		</h1>
+	</div>
 
 
 </div>
 </div>

+ 234 - 51
public/app/plugins/app/testdata/dashboards/graph_last_1h.json

@@ -1,5 +1,5 @@
 {
 {
-  "revision": 5,
+  "revision": 6,
   "title": "TestData - Graph Panel Last 1h",
   "title": "TestData - Graph Panel Last 1h",
   "tags": [
   "tags": [
     "grafana-test"
     "grafana-test"
@@ -7,8 +7,48 @@
   "style": "dark",
   "style": "dark",
   "timezone": "browser",
   "timezone": "browser",
   "editable": true,
   "editable": true,
-  "hideControls": false,
   "sharedCrosshair": false,
   "sharedCrosshair": false,
+  "hideControls": false,
+  "time": {
+    "from": "2016-11-16T16:59:38.294Z",
+    "to": "2016-11-16T17:09:01.532Z"
+  },
+  "timepicker": {
+    "refresh_intervals": [
+      "5s",
+      "10s",
+      "30s",
+      "1m",
+      "5m",
+      "15m",
+      "30m",
+      "1h",
+      "2h",
+      "1d"
+    ],
+    "time_options": [
+      "5m",
+      "15m",
+      "1h",
+      "6h",
+      "12h",
+      "24h",
+      "2d",
+      "7d",
+      "30d"
+    ]
+  },
+  "templating": {
+    "list": []
+  },
+  "annotations": {
+    "list": []
+  },
+  "refresh": false,
+  "schemaVersion": 13,
+  "version": 4,
+  "links": [],
+  "gnetId": null,
   "rows": [
   "rows": [
     {
     {
       "collapse": false,
       "collapse": false,
@@ -238,7 +278,13 @@
           ]
           ]
         }
         }
       ],
       ],
-      "title": "New row"
+      "title": "New row",
+      "showTitle": false,
+      "titleSize": "h6",
+      "isNew": false,
+      "repeat": null,
+      "repeatRowId": null,
+      "repeatIteration": null
     },
     },
     {
     {
       "collapse": false,
       "collapse": false,
@@ -332,7 +378,13 @@
           "type": "text"
           "type": "text"
         }
         }
       ],
       ],
-      "title": "New row"
+      "title": "New row",
+      "showTitle": false,
+      "titleSize": "h6",
+      "isNew": false,
+      "repeat": null,
+      "repeatRowId": null,
+      "repeatIteration": null
     },
     },
     {
     {
       "collapse": false,
       "collapse": false,
@@ -371,7 +423,7 @@
               "yaxis": 2
               "yaxis": 2
             }
             }
           ],
           ],
-          "span": 7.99561403508772,
+          "span": 8,
           "stack": false,
           "stack": false,
           "steppedLine": false,
           "steppedLine": false,
           "targets": [
           "targets": [
@@ -432,12 +484,18 @@
           "isNew": true,
           "isNew": true,
           "links": [],
           "links": [],
           "mode": "markdown",
           "mode": "markdown",
-          "span": 4.00438596491228,
+          "span": 4,
           "title": "",
           "title": "",
           "type": "text"
           "type": "text"
         }
         }
       ],
       ],
-      "title": "New row"
+      "title": "New row",
+      "showTitle": false,
+      "titleSize": "h6",
+      "isNew": false,
+      "repeat": null,
+      "repeatRowId": null,
+      "repeatIteration": null
     },
     },
     {
     {
       "collapse": false,
       "collapse": false,
@@ -545,7 +603,7 @@
           "points": false,
           "points": false,
           "renderer": "flot",
           "renderer": "flot",
           "seriesOverrides": [],
           "seriesOverrides": [],
-          "span": 3,
+          "span": 4,
           "stack": false,
           "stack": false,
           "steppedLine": false,
           "steppedLine": false,
           "targets": [
           "targets": [
@@ -592,6 +650,31 @@
             }
             }
           ]
           ]
         },
         },
+        {
+          "content": "Should be a long line connecting the null region in the `connected`  mode, and in zero it should just be a line with zero value at the null points. ",
+          "editable": true,
+          "error": false,
+          "id": 13,
+          "isNew": true,
+          "links": [],
+          "mode": "markdown",
+          "span": 4,
+          "title": "",
+          "type": "text"
+        }
+      ],
+      "title": "New row",
+      "showTitle": false,
+      "titleSize": "h6",
+      "isNew": false,
+      "repeat": null,
+      "repeatRowId": null,
+      "repeatIteration": null
+    },
+    {
+      "isNew": false,
+      "title": "Dashboard Row",
+      "panels": [
         {
         {
           "aliasColors": {},
           "aliasColors": {},
           "bars": false,
           "bars": false,
@@ -624,7 +707,7 @@
               "zindex": -3
               "zindex": -3
             }
             }
           ],
           ],
-          "span": 5,
+          "span": 8,
           "stack": true,
           "stack": true,
           "steppedLine": false,
           "steppedLine": false,
           "targets": [
           "targets": [
@@ -687,49 +770,149 @@
               "show": true
               "show": true
             }
             }
           ]
           ]
+        },
+        {
+          "content": "Stacking values on top of nulls, should treat the null values as zero. ",
+          "editable": true,
+          "error": false,
+          "id": 14,
+          "isNew": true,
+          "links": [],
+          "mode": "markdown",
+          "span": 4,
+          "title": "",
+          "type": "text"
         }
         }
       ],
       ],
-      "title": "New row"
+      "showTitle": false,
+      "titleSize": "h6",
+      "height": 250,
+      "repeat": null,
+      "repeatRowId": null,
+      "repeatIteration": null,
+      "collapse": false
+    },
+    {
+      "isNew": false,
+      "title": "Dashboard Row",
+      "panels": [
+        {
+          "aliasColors": {},
+          "bars": false,
+          "datasource": "Grafana TestData",
+          "editable": true,
+          "error": false,
+          "fill": 1,
+          "id": 12,
+          "isNew": true,
+          "legend": {
+            "avg": false,
+            "current": false,
+            "max": false,
+            "min": false,
+            "show": true,
+            "total": false,
+            "values": false
+          },
+          "lines": true,
+          "linewidth": 2,
+          "links": [],
+          "nullPointMode": "null",
+          "percentage": false,
+          "pointradius": 5,
+          "points": false,
+          "renderer": "flot",
+          "seriesOverrides": [
+            {
+              "alias": "B-series",
+              "zindex": -3
+            }
+          ],
+          "span": 8,
+          "stack": true,
+          "steppedLine": false,
+          "targets": [
+            {
+              "hide": false,
+              "refId": "B",
+              "scenarioId": "csv_metric_values",
+              "stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
+              "target": "",
+              "alias": ""
+            },
+            {
+              "alias": "",
+              "hide": false,
+              "refId": "A",
+              "scenarioId": "csv_metric_values",
+              "stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
+              "target": ""
+            },
+            {
+              "alias": "",
+              "hide": false,
+              "refId": "C",
+              "scenarioId": "csv_metric_values",
+              "stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
+              "target": ""
+            }
+          ],
+          "thresholds": [],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "Stacking all series null segment",
+          "tooltip": {
+            "msResolution": false,
+            "shared": true,
+            "sort": 0,
+            "value_type": "cumulative"
+          },
+          "type": "graph",
+          "xaxis": {
+            "mode": "time",
+            "name": null,
+            "show": true,
+            "values": []
+          },
+          "yaxes": [
+            {
+              "format": "short",
+              "label": null,
+              "logBase": 1,
+              "max": null,
+              "min": null,
+              "show": true
+            },
+            {
+              "format": "short",
+              "label": null,
+              "logBase": 1,
+              "max": null,
+              "min": null,
+              "show": true
+            }
+          ]
+        },
+        {
+          "content": "Stacking when all values are null should leave a gap in the graph",
+          "editable": true,
+          "error": false,
+          "id": 15,
+          "isNew": true,
+          "links": [],
+          "mode": "markdown",
+          "span": 4,
+          "title": "",
+          "type": "text"
+        }
+      ],
+      "showTitle": false,
+      "titleSize": "h6",
+      "height": 250,
+      "repeat": null,
+      "repeatRowId": null,
+      "repeatIteration": null,
+      "collapse": false
     }
     }
-  ],
-  "time": {
-    "from": "now-1h",
-    "to": "now"
-  },
-  "timepicker": {
-    "refresh_intervals": [
-      "5s",
-      "10s",
-      "30s",
-      "1m",
-      "5m",
-      "15m",
-      "30m",
-      "1h",
-      "2h",
-      "1d"
-    ],
-    "time_options": [
-      "5m",
-      "15m",
-      "1h",
-      "6h",
-      "12h",
-      "24h",
-      "2d",
-      "7d",
-      "30d"
-    ]
-  },
-  "templating": {
-    "list": []
-  },
-  "annotations": {
-    "list": []
-  },
-  "refresh": false,
-  "schemaVersion": 13,
-  "version": 13,
-  "links": [],
-  "gnetId": null
+  ]
 }
 }

+ 1 - 1
public/app/plugins/app/testdata/plugin.json

@@ -9,7 +9,7 @@
       "name": "Grafana Project",
       "name": "Grafana Project",
       "url": "http://grafana.org"
       "url": "http://grafana.org"
     },
     },
-    "version": "1.0.14",
+    "version": "1.0.15",
     "updated": "2016-09-26"
     "updated": "2016-09-26"
   },
   },
 
 

+ 15 - 6
public/app/plugins/datasource/cloudwatch/datasource.js

@@ -37,7 +37,8 @@ function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) {
         query.dimensions = self.convertDimensionFormat(target.dimensions, options.scopedVars);
         query.dimensions = self.convertDimensionFormat(target.dimensions, options.scopedVars);
         query.statistics = target.statistics;
         query.statistics = target.statistics;
 
 
-        var period = this._getPeriod(target, query, options, start, end);
+        var now = Math.round(Date.now() / 1000);
+        var period = this._getPeriod(target, query, options, start, end, now);
         target.period = period;
         target.period = period;
         query.period = period;
         query.period = period;
 
 
@@ -67,22 +68,30 @@ function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) {
       });
       });
     };
     };
 
 
-    this._getPeriod = function(target, query, options, start, end) {
+    this._getPeriod = function(target, query, options, start, end, now) {
       var period;
       var period;
       var range = end - start;
       var range = end - start;
 
 
-      if (!target.period) {
+      var daySec = 60 * 60 * 24;
+      var periodUnit = 60;
+      if (now - start > (daySec * 15)) { // until 63 days ago
+        periodUnit = period = 60 * 5;
+      } else if (now - start > (daySec * 63)) { // until 455 days ago
+        periodUnit = period = 60 * 60;
+      } else if (now - start > (daySec * 455)) { // over 455 days, should return error, but try to long period
+        periodUnit = period = 60 * 60;
+      } else if (!target.period) {
         period = (query.namespace === 'AWS/EC2') ? 300 : 60;
         period = (query.namespace === 'AWS/EC2') ? 300 : 60;
       } else if (/^\d+$/.test(target.period)) {
       } else if (/^\d+$/.test(target.period)) {
         period = parseInt(target.period, 10);
         period = parseInt(target.period, 10);
       } else {
       } else {
         period = kbn.interval_to_seconds(templateSrv.replace(target.period, options.scopedVars));
         period = kbn.interval_to_seconds(templateSrv.replace(target.period, options.scopedVars));
       }
       }
-      if (query.period < 60) {
+      if (period < 60) {
         period = 60;
         period = 60;
       }
       }
-      if (range / query.period >= 1440) {
-        period = Math.ceil(range / 1440 / 60) * 60;
+      if (range / period >= 1440) {
+        period = Math.ceil(range / 1440 / periodUnit) * periodUnit;
       }
       }
 
 
       return period;
       return period;

+ 1 - 0
public/app/plugins/panel/alertlist/editor.html

@@ -11,6 +11,7 @@
       <span class="gf-form-label width-8">Max items</span>
       <span class="gf-form-label width-8">Max items</span>
       <input type="text" class="gf-form-input max-width-15" ng-model="ctrl.panel.limit" ng-change="ctrl.onRender()" />
       <input type="text" class="gf-form-input max-width-15" ng-model="ctrl.panel.limit" ng-change="ctrl.onRender()" />
     </div>
     </div>
+    <gf-form-switch class="gf-form" label="Alerts from this dashboard" label-class="width-18" checked="ctrl.panel.onlyAlertsOnDashboard" on-change="ctrl.updateStateFilter()"></gf-form-switch>
   </div>
   </div>
   <div class="section gf-form-group">
   <div class="section gf-form-group">
     <h5 class="section-heading">State filter</h5>
     <h5 class="section-heading">State filter</h5>

+ 11 - 2
public/app/plugins/panel/alertlist/module.ts

@@ -25,7 +25,8 @@ class AlertListPanel extends PanelCtrl {
   panelDefaults = {
   panelDefaults = {
     show: 'current',
     show: 'current',
     limit: 10,
     limit: 10,
-    stateFilter: []
+    stateFilter: [],
+    onlyAlertsOnDashboard: false
   };
   };
 
 
 
 
@@ -71,9 +72,13 @@ class AlertListPanel extends PanelCtrl {
     var params: any = {
     var params: any = {
       limit: this.panel.limit,
       limit: this.panel.limit,
       type: 'alert',
       type: 'alert',
-      newState: this.panel.stateFilter
+      newState: this.panel.stateFilter,
     };
     };
 
 
+    if (this.panel.onlyAlertsOnDashboard) {
+      params.dashboardId = this.dashboard.id;
+    }
+
     params.from = dateMath.parse(this.dashboard.time.from).unix() * 1000;
     params.from = dateMath.parse(this.dashboard.time.from).unix() * 1000;
     params.to = dateMath.parse(this.dashboard.time.to).unix() * 1000;
     params.to = dateMath.parse(this.dashboard.time.to).unix() * 1000;
 
 
@@ -93,6 +98,10 @@ class AlertListPanel extends PanelCtrl {
       state: this.panel.stateFilter
       state: this.panel.stateFilter
     };
     };
 
 
+    if (this.panel.onlyAlertsOnDashboard) {
+      params.dashboardId = this.dashboard.id;
+    }
+
     this.backendSrv.get(`/api/alerts`, params)
     this.backendSrv.get(`/api/alerts`, params)
       .then(res => {
       .then(res => {
         this.currentAlerts = _.map(res, al => {
         this.currentAlerts = _.map(res, al => {

+ 1 - 1
public/app/plugins/panel/graph/graph_tooltip.js

@@ -195,7 +195,7 @@ function ($) {
           }
           }
 
 
           var highlightClass = '';
           var highlightClass = '';
-          if (item && i === item.seriesIndex) {
+          if (item && hoverInfo.index === item.seriesIndex) {
             highlightClass = 'graph-tooltip-list-item--highlight';
             highlightClass = 'graph-tooltip-list-item--highlight';
           }
           }
 
 

+ 3 - 6
public/app/plugins/panel/singlestat/module.ts

@@ -208,11 +208,8 @@ class SingleStatCtrl extends MetricsPanelCtrl {
       }
       }
 
 
       // Add $__name variable for using in prefix or postfix
       // Add $__name variable for using in prefix or postfix
-      data.scopedVars = {
-        __name: {
-          value: this.series[0].label
-        }
-      };
+      data.scopedVars = _.extend({}, this.panel.scopedVars);
+      data.scopedVars["__name"] = {value: this.series[0].label};
     }
     }
 
 
     // check value to text mappings if its enabled
     // check value to text mappings if its enabled
@@ -526,7 +523,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
       elem.toggleClass('pointer', panel.links.length > 0);
       elem.toggleClass('pointer', panel.links.length > 0);
 
 
       if (panel.links.length > 0) {
       if (panel.links.length > 0) {
-        linkInfo = linkSrv.getPanelLinkAnchorInfo(panel.links[0], panel.scopedVars);
+        linkInfo = linkSrv.getPanelLinkAnchorInfo(panel.links[0], data.scopedVars);
       } else {
       } else {
         linkInfo = null;
         linkInfo = null;
       }
       }

+ 0 - 2
public/sass/_grafana.scss

@@ -51,11 +51,9 @@
 @import "components/tagsinput";
 @import "components/tagsinput";
 @import "components/tables_lists";
 @import "components/tables_lists";
 @import "components/search";
 @import "components/search";
-@import "components/tightform";
 @import "components/gf-form";
 @import "components/gf-form";
 @import "components/sidemenu";
 @import "components/sidemenu";
 @import "components/navbar";
 @import "components/navbar";
-@import "components/gfbox";
 @import "components/timepicker";
 @import "components/timepicker";
 @import "components/filter-controls";
 @import "components/filter-controls";
 @import "components/filter-list";
 @import "components/filter-list";

+ 0 - 69
public/sass/components/_gfbox.scss

@@ -1,69 +0,0 @@
-.gf-box {
-  margin: 10px 5px;
-  background-color: $page-bg;
-  position: relative;
-  border: 1px solid $tight-form-func-bg;
-}
-
-.gf-box-no-margin {
-  margin: 0;
-}
-
-.gf-box-header-close-btn {
-  float: right;
-  padding: 0;
-  margin: 0;
-  background-color: transparent;
-  border: none;
-  padding: 8px;
-  i {
-    font-size: 120%;
-  }
-  color: $text-color;
-  &:hover {
-    color: $white;
-  }
-}
-
-.gf-box-header-save-btn {
-  padding: 7px 0;
-  float: right;
-  color: $gray-2;
-  font-style: italic;
-}
-
-.gf-box-body {
-  padding: 20px;
-  min-height: 150px;
-}
-
-.gf-box-footer {
-  overflow: hidden;
-}
-
-.gf-box-header {
-  border-bottom: 1px solid $tight-form-func-bg;
-  overflow: hidden;
-  background-color: $tight-form-bg;
-  .tabs {
-    float: left;
-  }
-  .nav {
-    margin: 0;
-  }
-}
-
-.gf-box-title {
-  padding-right: 20px;
-  padding-left: 10px;
-  float: left;
-  color: $link-color;
-  font-size: 18px;
-  font-weight: normal;
-  line-height: 38px;
-  margin: 0;
-  .fa {
-    padding: 0 8px 0 5px;
-    color: $text-color;
-  }
-}

+ 1 - 1
public/sass/components/_navs.scss

@@ -87,7 +87,7 @@
 }
 }
 
 
 // temp hack
 // temp hack
-.modal-body, .gf-box {
+.modal-body {
   .nav-tabs {
   .nav-tabs {
     border-bottom: none;
     border-bottom: none;
   }
   }

+ 79 - 0
public/sass/components/_query_editor.scss

@@ -67,3 +67,82 @@
   }
   }
 }
 }
 
 
+.grafana-metric-options {
+  margin-top: 25px;
+}
+
+.tight-form-func {
+  background: $tight-form-func-bg;
+
+  &.show-function-controls {
+    padding-top: 5px;
+    min-width: 100px;
+    text-align: center;
+  }
+}
+
+input[type="text"].tight-form-func-param {
+  background: transparent;
+  border: none;
+  margin: 0;
+  padding: 0;
+}
+
+.tight-form-func-controls {
+  display: none;
+  text-align: center;
+
+  .fa-arrow-left {
+    float: left;
+    position: relative;
+    top: 2px;
+  }
+  .fa-arrow-right {
+    float: right;
+    position: relative;
+    top: 2px;
+  }
+  .fa-remove {
+    margin-left: 10px;
+  }
+}
+
+.grafana-metric-options {
+  margin-top: 25px;
+}
+
+.tight-form-func {
+  background: $tight-form-func-bg;
+
+  &.show-function-controls {
+    padding-top: 5px;
+    min-width: 100px;
+    text-align: center;
+  }
+}
+
+input[type="text"].tight-form-func-param {
+  background: transparent;
+  border: none;
+  margin: 0;
+  padding: 0;
+}
+
+.tight-form-func-controls {
+  display: none;
+  text-align: center;
+
+  .fa-arrow-left {
+    float: left;
+    position: relative;
+    top: 2px;
+  }
+  .fa-arrow-right {
+    float: right;
+    position: relative;
+    top: 2px;
+  }
+  .fa-remove {
+    margin-left: 10px;
+  }
+}

+ 1 - 2
public/sass/components/_row.scss

@@ -74,12 +74,11 @@
 .add-panel-panels-scroll {
 .add-panel-panels-scroll {
   width: 100%;
   width: 100%;
   overflow: auto;
   overflow: auto;
+  -ms-overflow-style: none;
 
 
   &::-webkit-scrollbar {
   &::-webkit-scrollbar {
     display: none
     display: none
   }
   }
-
-  -ms-overflow-style: none;
 }
 }
 
 
 .add-panel-panels {
 .add-panel-panels {

+ 2 - 2
public/sass/components/_shortcuts.scss

@@ -6,6 +6,8 @@
 }
 }
 
 
 .shortcut-table {
 .shortcut-table {
+  margin-bottom: $spacer;
+
   .shortcut-table-category-header {
   .shortcut-table-category-header {
     font-weight: normal;
     font-weight: normal;
     font-size: $font-size-h6;
     font-size: $font-size-h6;
@@ -26,8 +28,6 @@
     text-align: right;
     text-align: right;
     color: $text-color;
     color: $text-color;
   }
   }
-
-  margin-bottom: $spacer;
 }
 }
 
 
 .shortcut-table-key {
 .shortcut-table-key {

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

@@ -7,11 +7,12 @@
 }
 }
 
 
 .annotation-segment {
 .annotation-segment {
+  padding: 8px 7px;
+
   label.cr1 {
   label.cr1 {
     margin-left: 5px;
     margin-left: 5px;
     margin-top: 3px;
     margin-top: 3px;
   }
   }
-  padding: 8px 7px;
 }
 }
 
 
 .submenu-item {
 .submenu-item {
@@ -31,14 +32,14 @@
 
 
 .variable-value-link {
 .variable-value-link {
   padding-right: 10px;
   padding-right: 10px;
-  .label-tag {
-    margin: 0 5px;
-  }
-
   padding: 8px 7px;
   padding: 8px 7px;
   box-sizing: content-box;
   box-sizing: content-box;
   display: inline-block;
   display: inline-block;
   color: $text-color;
   color: $text-color;
+
+  .label-tag {
+    margin: 0 5px;
+  }
 }
 }
 
 
 .variable-link-wrapper  {
 .variable-link-wrapper  {

+ 1 - 1
public/sass/components/_tabbed_view.scss

@@ -38,10 +38,10 @@
   background-color: transparent;
   background-color: transparent;
   border: none;
   border: none;
   padding: ($tabs-padding-top + $tabs-top-margin) $spacer $tabs-padding-bottom;
   padding: ($tabs-padding-top + $tabs-top-margin) $spacer $tabs-padding-bottom;
+  color: $text-color;
   i {
   i {
     font-size: 120%;
     font-size: 120%;
   }
   }
-  color: $text-color;
   &:hover {
   &:hover {
     color: $white;
     color: $white;
   }
   }

+ 0 - 235
public/sass/components/_tightform.scss

@@ -1,235 +0,0 @@
-.tight-form {
-  border-top: 1px solid $tight-form-border;
-  border-left: 1px solid $tight-form-border;
-  border-right: 1px solid $tight-form-border;
-  background: $tight-form-bg;
-
-  &.last {
-    border-bottom: 1px solid $tight-form-border;
-  }
-
-  &.borderless {
-    background: transparent;
-    border: none;
-  }
-
-  .checkbox-label {
-    display: inline;
-    padding-right: 4px;
-    margin-bottom: 0;
-    cursor: pointer;
-  }
-}
-
-.tight-form-container-no-item-borders {
-  border: 1px solid $tight-form-border;
-  border-bottom: none;
-
-  .tight-form, .tight-form-item, [type="text"].tight-form-input, [type="text"].tight-form-clear-input  {
-    border: none;
-  }
-}
-
-.spaced-form {
-  .tight-form {
-    margin: 7px 0;
-  }
-}
-
-.borderless {
-  .tight-form-item,
-  .tight-form-input {
-    border: none;
-  }
-}
-
-.tight-form-container {
-  border-bottom: 1px solid $tight-form-border;
-}
-
-.tight-form-btn {
-  padding: 7px 12px;
-}
-
-.tight-form-list {
-  list-style: none;
-  margin: 0;
-  >li {
-    float: left;
-  }
-}
-
-.tight-form-flex-wrapper {
-  display: flex;
-  flex-direction: row;
-  float: none !important;
-}
-
-.grafana-metric-options {
-  margin-top: 25px;
-}
-
-.tight-form-item {
-  padding: 8px 7px;
-  box-sizing: content-box;
-  display: inline-block;
-  font-weight: normal;
-  border-right: 1px solid $tight-form-border;
-  display: inline-block;
-  color: $text-color;
-
-  .has-open-function & {
-    padding-top: 25px;
-  }
-
-  .tight-form-disabled & {
-    color: $link-color-disabled;
-    a {
-      color: $link-color-disabled;
-    }
-  }
-
-  &:hover, &:focus {
-    text-decoration: none;
-  }
-
-  &a:hover {
-    background: $tight-form-func-bg;
-  }
-
-  &.last {
-    border-right: none;
-  }
-}
-
-
-.tight-form-item-icon {
-  i {
-    width: 15px;
-    text-align: center;
-    display: inline-block;
-  }
-}
-
-.tight-form-func {
-  background: $tight-form-func-bg;
-
-  &.show-function-controls {
-    padding-top: 5px;
-    min-width: 100px;
-    text-align: center;
-  }
-}
-
-input[type="text"].tight-form-func-param {
-  background: transparent;
-  border: none;
-  margin: 0;
-  padding: 0;
-}
-
-input[type="text"].tight-form-clear-input {
-  padding: 8px 7px;
-  border: none;
-  margin: 0px;
-  background: transparent;
-  border-radius: 0;
-  border-right: 1px solid $tight-form-border;
-}
-
-[type="text"],
-[type="email"],
-[type="number"],
-[type="password"] {
-  &.tight-form-input {
-    background-color: $input-bg;
-    border: none;
-    border-right: 1px solid $tight-form-border;
-    margin: 0px;
-    border-radius: 0;
-    padding: 8px 6px;
-    height: 100%;
-    box-sizing: border-box;
-    &.last {
-      border-right: none;
-    }
-  }
-}
-
-input[type="checkbox"].tight-form-checkbox {
-  margin: 0;
-}
-
-.tight-form-textarea {
-  height: 200px;
-  margin: 0;
-  box-sizing: border-box;
-}
-
-select.tight-form-input {
-  border: none;
-  border-right: 1px solid $tight-form-border;
-  background-color: $input-bg;
-  margin: 0px;
-  border-radius: 0;
-  height: 36px;
-  padding: 9px 3px;
-  &.last {
-    border-right: none;
-  }
-}
-
-.tight-form-func-controls {
-  display: none;
-  text-align: center;
-
-  .fa-arrow-left {
-    float: left;
-    position: relative;
-    top: 2px;
-  }
-  .fa-arrow-right {
-    float: right;
-    position: relative;
-    top: 2px;
-  }
-  .fa-remove {
-    margin-left: 10px;
-  }
-}
-
-.tight-form-radio {
-  input[type="radio"] {
-    margin: 0;
-  }
-  label {
-    display: inline;
-  }
-}
-
-.tight-form-section {
-  margin-bottom: 20px;
-  margin-right: 40px;
-  vertical-align: top;
-  display: inline-block;
-  .tight-form {
-    margin-left: 20px;
-  }
-}
-
-.tight-form-align {
-  padding-left: 66px;
-}
-
-.tight-form-item-large { width: 115px; }
-.tight-form-item-xlarge { width: 150px; }
-.tight-form-item-xxlarge { width: 200px; }
-
-.tight-form-input.tight-form-item-xxlarge {
-  width: 215px;
-}
-
-.tight-form-inner-box {
-  margin: 20px 0 20px 148px;
-  display: inline-block;
-}

+ 4 - 2
public/sass/components/_timepicker.scss

@@ -65,15 +65,17 @@
 }
 }
 
 
 .gf-timepicker-component {
 .gf-timepicker-component {
-  margin-bottom: 10px;
+  padding: $spacer/2 0 $spacer 0;
+
   td {
   td {
     padding: 1px;
     padding: 1px;
   }
   }
   button.btn-sm {
   button.btn-sm {
     @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl);
     @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl);
+    font-size: $font-size-sm;
     background-image: none;
     background-image: none;
     border: none;
     border: none;
-    padding: 6px 10px;
+    padding: 5px 11px;
     color: $text-color;
     color: $text-color;
     &.active span {
     &.active span {
       color: $blue;
       color: $blue;

+ 0 - 6
public/sass/layout/_page.scss

@@ -62,12 +62,6 @@
 .admin-page {
 .admin-page {
   max-width: 800px;
   max-width: 800px;
   margin-left: 10px;
   margin-left: 10px;
-  .gf-box {
-    margin-top: 0;
-  }
-  .gf-box-body {
-    min-height: 0;
-  }
   h2 {
   h2 {
     margin-left: 15px;
     margin-left: 15px;
     margin-bottom: 0px;
     margin-bottom: 0px;

+ 0 - 1
public/sass/pages/_alerting.scss

@@ -61,7 +61,6 @@
   }
   }
 
 
   &--ok {
   &--ok {
-    box-shadow: 0 0 5px rgba(0,200,0,10.8);
     .panel-alert-icon:before {
     .panel-alert-icon:before {
       color: $online;
       color: $online;
       content: "\e611";
       content: "\e611";

+ 6 - 0
public/sass/pages/_dashboard.scss

@@ -172,6 +172,12 @@ div.flot-text {
   }
   }
 }
 }
 
 
+.panel-in-fullscreen {
+  .panel-drop-zone {
+    display: none !important;
+  }
+}
+
 .panel-time-info {
 .panel-time-info {
   font-weight: bold;
   font-weight: bold;
   float: right;
   float: right;

+ 2 - 320
public/vendor/angular-ui/ui-bootstrap-tpls.js

@@ -5,8 +5,8 @@
  * Version: 0.13.4 - 2015-09-03
  * Version: 0.13.4 - 2015-09-03
  * License: MIT
  * License: MIT
  */
  */
-angular.module("ui.bootstrap", ["ui.bootstrap.tpls","ui.bootstrap.position","ui.bootstrap.dateparser","ui.bootstrap.datepicker","ui.bootstrap.tabs"]);
-angular.module("ui.bootstrap.tpls", ["template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html","template/tabs/tab.html","template/tabs/tabset.html"]);
+angular.module("ui.bootstrap", ["ui.bootstrap.tpls","ui.bootstrap.position","ui.bootstrap.dateparser","ui.bootstrap.datepicker"]);
+angular.module("ui.bootstrap.tpls", ["template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html"]);
 angular.module('ui.bootstrap.position', [])
 angular.module('ui.bootstrap.position', [])
 
 
 /**
 /**
@@ -1180,302 +1180,6 @@ function($compile, $parse, $document, $rootScope, $position, dateFilter, datePar
 });
 });
 
 
 
 
-/**
- * @ngdoc overview
- * @name ui.bootstrap.tabs
- *
- * @description
- * AngularJS version of the tabs directive.
- */
-
-angular.module('ui.bootstrap.tabs', [])
-
-.controller('TabsetController', ['$scope', function TabsetCtrl($scope) {
-  var ctrl = this,
-      tabs = ctrl.tabs = $scope.tabs = [];
-
-  ctrl.select = function(selectedTab) {
-    angular.forEach(tabs, function(tab) {
-      if (tab.active && tab !== selectedTab) {
-        tab.active = false;
-        tab.onDeselect();
-        selectedTab.selectCalled = false;
-      }
-    });
-    selectedTab.active = true;
-    // only call select if it has not already been called
-    if (!selectedTab.selectCalled) {
-      selectedTab.onSelect();
-      selectedTab.selectCalled = true;
-    }
-  };
-
-  ctrl.addTab = function addTab(tab) {
-    tabs.push(tab);
-    // we can't run the select function on the first tab
-    // since that would select it twice
-    if (tabs.length === 1 && tab.active !== false) {
-      tab.active = true;
-    } else if (tab.active) {
-      ctrl.select(tab);
-    } else {
-      tab.active = false;
-    }
-  };
-
-  ctrl.removeTab = function removeTab(tab) {
-    var index = tabs.indexOf(tab);
-    //Select a new tab if the tab to be removed is selected and not destroyed
-    if (tab.active && tabs.length > 1 && !destroyed) {
-      //If this is the last tab, select the previous tab. else, the next tab.
-      var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1;
-      ctrl.select(tabs[newActiveIndex]);
-    }
-    tabs.splice(index, 1);
-  };
-
-  var destroyed;
-  $scope.$on('$destroy', function() {
-    destroyed = true;
-  });
-}])
-
-/**
- * @ngdoc directive
- * @name ui.bootstrap.tabs.directive:tabset
- * @restrict EA
- *
- * @description
- * Tabset is the outer container for the tabs directive
- *
- * @param {boolean=} vertical Whether or not to use vertical styling for the tabs.
- * @param {boolean=} justified Whether or not to use justified styling for the tabs.
- *
- * @example
-<example module="ui.bootstrap">
-  <file name="index.html">
-    <tabset>
-      <tab heading="Tab 1"><b>First</b> Content!</tab>
-      <tab heading="Tab 2"><i>Second</i> Content!</tab>
-    </tabset>
-    <hr />
-    <tabset vertical="true">
-      <tab heading="Vertical Tab 1"><b>First</b> Vertical Content!</tab>
-      <tab heading="Vertical Tab 2"><i>Second</i> Vertical Content!</tab>
-    </tabset>
-    <tabset justified="true">
-      <tab heading="Justified Tab 1"><b>First</b> Justified Content!</tab>
-      <tab heading="Justified Tab 2"><i>Second</i> Justified Content!</tab>
-    </tabset>
-  </file>
-</example>
- */
-.directive('tabset', function() {
-  return {
-    restrict: 'EA',
-    transclude: true,
-    replace: true,
-    scope: {
-      type: '@'
-    },
-    controller: 'TabsetController',
-    templateUrl: 'template/tabs/tabset.html',
-    link: function(scope, element, attrs) {
-      scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false;
-      scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false;
-    }
-  };
-})
-
-/**
- * @ngdoc directive
- * @name ui.bootstrap.tabs.directive:tab
- * @restrict EA
- *
- * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}.
- * @param {string=} select An expression to evaluate when the tab is selected.
- * @param {boolean=} active A binding, telling whether or not this tab is selected.
- * @param {boolean=} disabled A binding, telling whether or not this tab is disabled.
- *
- * @description
- * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}.
- *
- * @example
-<example module="ui.bootstrap">
-  <file name="index.html">
-    <div ng-controller="TabsDemoCtrl">
-      <button class="btn btn-small" ng-click="items[0].active = true">
-        Select item 1, using active binding
-      </button>
-      <button class="btn btn-small" ng-click="items[1].disabled = !items[1].disabled">
-        Enable/disable item 2, using disabled binding
-      </button>
-      <br />
-      <tabset>
-        <tab heading="Tab 1">First Tab</tab>
-        <tab select="alertMe()">
-          <tab-heading><i class="icon-bell"></i> Alert me!</tab-heading>
-          Second Tab, with alert callback and html heading!
-        </tab>
-        <tab ng-repeat="item in items"
-          heading="{{item.title}}"
-          disabled="item.disabled"
-          active="item.active">
-          {{item.content}}
-        </tab>
-      </tabset>
-    </div>
-  </file>
-  <file name="script.js">
-    function TabsDemoCtrl($scope) {
-      $scope.items = [
-        { title:"Dynamic Title 1", content:"Dynamic Item 0" },
-        { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true }
-      ];
-
-      $scope.alertMe = function() {
-        setTimeout(function() {
-          alert("You've selected the alert tab!");
-        });
-      };
-    };
-  </file>
-</example>
- */
-
-/**
- * @ngdoc directive
- * @name ui.bootstrap.tabs.directive:tabHeading
- * @restrict EA
- *
- * @description
- * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element.
- *
- * @example
-<example module="ui.bootstrap">
-  <file name="index.html">
-    <tabset>
-      <tab>
-        <tab-heading><b>HTML</b> in my titles?!</tab-heading>
-        And some content, too!
-      </tab>
-      <tab>
-        <tab-heading><i class="icon-heart"></i> Icon heading?!?</tab-heading>
-        That's right.
-      </tab>
-    </tabset>
-  </file>
-</example>
- */
-.directive('tab', ['$parse', '$log', function($parse, $log) {
-  return {
-    require: '^tabset',
-    restrict: 'EA',
-    replace: true,
-    templateUrl: 'template/tabs/tab.html',
-    transclude: true,
-    scope: {
-      active: '=?',
-      heading: '@',
-      onSelect: '&select', //This callback is called in contentHeadingTransclude
-                          //once it inserts the tab's content into the dom
-      onDeselect: '&deselect'
-    },
-    controller: function() {
-      //Empty controller so other directives can require being 'under' a tab
-    },
-    link: function(scope, elm, attrs, tabsetCtrl, transclude) {
-      scope.$watch('active', function(active) {
-        if (active) {
-          tabsetCtrl.select(scope);
-        }
-      });
-
-      scope.disabled = false;
-      if (attrs.disable) {
-        scope.$parent.$watch($parse(attrs.disable), function(value) {
-          scope.disabled = !! value;
-        });
-      }
-
-      // Deprecation support of "disabled" parameter
-      // fix(tab): IE9 disabled attr renders grey text on enabled tab #2677
-      // This code is duplicated from the lines above to make it easy to remove once
-      // the feature has been completely deprecated
-      if (attrs.disabled) {
-        $log.warn('Use of "disabled" attribute has been deprecated, please use "disable"');
-        scope.$parent.$watch($parse(attrs.disabled), function(value) {
-          scope.disabled = !! value;
-        });
-      }
-
-      scope.select = function() {
-        if (!scope.disabled) {
-          scope.active = true;
-        }
-      };
-
-      tabsetCtrl.addTab(scope);
-      scope.$on('$destroy', function() {
-        tabsetCtrl.removeTab(scope);
-      });
-
-      //We need to transclude later, once the content container is ready.
-      //when this link happens, we're inside a tab heading.
-      scope.$transcludeFn = transclude;
-    }
-  };
-}])
-
-.directive('tabHeadingTransclude', function() {
-  return {
-    restrict: 'A',
-    require: '^tab',
-    link: function(scope, elm, attrs, tabCtrl) {
-      scope.$watch('headingElement', function updateHeadingElement(heading) {
-        if (heading) {
-          elm.html('');
-          elm.append(heading);
-        }
-      });
-    }
-  };
-})
-
-.directive('tabContentTransclude', function() {
-  return {
-    restrict: 'A',
-    require: '^tabset',
-    link: function(scope, elm, attrs) {
-      var tab = scope.$eval(attrs.tabContentTransclude);
-
-      //Now our tab is ready to be transcluded: both the tab heading area
-      //and the tab content area are loaded.  Transclude 'em both.
-      tab.$transcludeFn(tab.$parent, function(contents) {
-        angular.forEach(contents, function(node) {
-          if (isTabHeading(node)) {
-            //Let tabHeadingTransclude know.
-            tab.headingElement = node;
-          } else {
-            elm.append(node);
-          }
-        });
-      });
-    }
-  };
-
-  function isTabHeading(node) {
-    return node.tagName && (
-      node.hasAttribute('tab-heading') ||
-      node.hasAttribute('data-tab-heading') ||
-      node.hasAttribute('x-tab-heading') ||
-      node.tagName.toLowerCase() === 'tab-heading' ||
-      node.tagName.toLowerCase() === 'data-tab-heading' ||
-      node.tagName.toLowerCase() === 'x-tab-heading'
-    );
-  }
-});
-
 angular.module("template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) {
 angular.module("template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) {
   $templateCache.put("template/datepicker/datepicker.html",
   $templateCache.put("template/datepicker/datepicker.html",
     "<div ng-switch=\"datepickerMode\" role=\"application\" ng-keydown=\"keydown($event)\">\n" +
     "<div ng-switch=\"datepickerMode\" role=\"application\" ng-keydown=\"keydown($event)\">\n" +
@@ -1568,25 +1272,3 @@ angular.module("template/datepicker/year.html", []).run(["$templateCache", funct
     "");
     "");
 }]);
 }]);
 
 
-angular.module("template/tabs/tab.html", []).run(["$templateCache", function($templateCache) {
-  $templateCache.put("template/tabs/tab.html",
-    "<li ng-class=\"{active: active, disabled: disabled}\">\n" +
-    "  <a href ng-click=\"select()\" tab-heading-transclude>{{heading}}</a>\n" +
-    "</li>\n" +
-    "");
-}]);
-
-angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) {
-  $templateCache.put("template/tabs/tabset.html",
-    "<div>\n" +
-    "  <ul class=\"nav nav-{{type || 'tabs'}} nav-tabs-alt\" ng-class=\"{'nav-stacked': vertical, 'nav-justified': justified}\" ng-transclude></ul>\n" +
-    "  <div class=\"tab-content\">\n" +
-    "    <div class=\"tab-pane\" \n" +
-    "         ng-repeat=\"tab in tabs\" \n" +
-    "         ng-class=\"{active: tab.active}\"\n" +
-    "         tab-content-transclude=\"tab\">\n" +
-    "    </div>\n" +
-    "  </div>\n" +
-    "</div>\n" +
-    "");
-}]);

+ 16 - 12
public/views/500.html

@@ -5,28 +5,32 @@
     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
     <meta name="viewport" content="width=device-width">
     <meta name="viewport" content="width=device-width">
 
 
-    <title>Grafana</title>
+    <title>Grafana - Error</title>
+
+		<link href='[[.AppSubUrl]]/public/css/fonts.min.css' rel='stylesheet' type='text/css'>
+
+		<link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.dark.min.css">
 
 
-    <link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.dark.min.css" title="Dark">
     <link rel="icon" type="image/png" href="[[.AppSubUrl]]/public/img/fav32.png">
     <link rel="icon" type="image/png" href="[[.AppSubUrl]]/public/img/fav32.png">
 
 
+		<base href="[[.AppSubUrl]]/" />
+
   </head>
   </head>
 
 
 	<body>
 	<body>
 
 
-		<div class="gf-box" style="margin: 200px auto 0 auto; width: 500px;">
-			<div class="gf-box-header">
-				<span class="gf-box-title">
+		<div class="page-container">
+			<div class="page-header">
+				<h1>
 					Server side error :(
 					Server side error :(
-				</span>
+				</h1>
 			</div>
 			</div>
 
 
-			<div class="gf-box-body">
-				<h4>[[.Title]]</h4>
-				[[.ErrorMsg]]
-			</div>
-		</div>
+			<h4>[[.Title]]</h4>
+
+			<pre>[[.ErrorMsg]]</pre>
 
 
-  </body>
+		</div>
+	</body>
 
 
 </html>
 </html>