diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dd0adba4c5..b30865b3e60 100644 --- a/CHANGELOG.md +++ b/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**: 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) +* **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) diff --git a/conf/defaults.ini b/conf/defaults.ini index b25f3ab6f28..7ead103d027 100644 --- a/conf/defaults.ini +++ b/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 api_url = https://www.googleapis.com/oauth2/v1/userinfo allowed_domains = -hosted_domain = +hosted_domain = #################################### Grafana.net Auth #################### [auth.grafananet] @@ -390,21 +390,6 @@ global_api_key = -1 global_session = -1 #################################### Alerting ############################ -# docs about alerting can be found in /docs/sources/alerting/ -# __.-/| -# \`o_O' -# =( )= +----------------------------+ -# U| | Alerting is still in alpha | -# /\ /\ / | +----------------------------+ -# ) /^\) ^\/ _)\ | -# ) /^\/ _) \ | -# ) _ / / _) \___|_ -# /\ )/\/ || | )_)\___,|)) -# < > |(,,) )__) | -# || / \)___)\ -# | \____( )___) )____ -# \______(_______;;;)__;;;) - [alerting] # Makes it possible to turn off alert rule execution. execute_alerts = true diff --git a/conf/sample.ini b/conf/sample.ini index e1ed408210a..e8c9c990a63 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -339,21 +339,6 @@ ;path = /var/lib/grafana/dashboards #################################### Alerting ###################################### -# docs about alerting can be found in /docs/sources/alerting/ -# __.-/| -# \`o_O' -# =( )= +----------------------------+ -# U| | Alerting is still in alpha | -# /\ /\ / | +----------------------------+ -# ) /^\) ^\/ _)\ | -# ) /^\/ _) \ | -# ) _ / / _) \___|_ -# /\ )/\/ || | )_)\___,|)) -# < > |(,,) )__) | -# || / \)___)\ -# | \____( )___) )____ -# \______(_______;;;)__;;;) - [alerting] # Makes it possible to turn off alert rule execution. ;execute_alerts = true diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index ce9dcdf1b93..dfe84f142ec 100644 --- a/docs/sources/alerting/notifications.md +++ b/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 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. diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md index ecab88f48d2..af3cfecfdb8 100644 --- a/docs/sources/alerting/rules.md +++ b/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 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 -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 of another alert in your conditions, and `Time Of Day`. diff --git a/docs/sources/datasources/influxdb.md b/docs/sources/datasources/influxdb.md index a5eab80af06..01b68d5b7dc 100644 --- a/docs/sources/datasources/influxdb.md +++ b/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. -![](img/docs/influxdb/templating_simple_ex1.png) +![](/img/docs/influxdb/templating_simple_ex1.png) ## Annotations Annotations allows you to overlay rich event information on top of graphs. diff --git a/docs/sources/datasources/plugin_api.md b/docs/sources/datasources/plugin_api.md index cdcaca29460..2e2121ed21a 100644 --- a/docs/sources/datasources/plugin_api.md +++ b/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 ``. 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. diff --git a/docs/sources/http_api/org.md b/docs/sources/http_api/org.md index adb5d5cd31e..36188075124 100644 --- a/docs/sources/http_api/org.md +++ b/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 `PUT /api/org` diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 2fc727d659f..f3b40b206bf 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -413,7 +413,7 @@ Set to `true` to enable LDAP integration (default: `false`) ### config_file 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.
diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index cf33e69aa12..ca71a2355f9 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -141,6 +141,18 @@ those options. - [OpenTSDB]({{< relref "datasources/opentsdb.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 diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index ab9f4b7c80b..6d55f03bea3 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -119,7 +119,8 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response { res := backendCmd.Result dtoRes := &dtos.AlertTestResult{ - Firing: res.Firing, + Firing: res.Firing, + ConditionEvals: res.ConditionEvals, } if res.Error != nil { diff --git a/pkg/api/api.go b/pkg/api/api.go index 6ea5bcd7c95..a1668590ad7 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -310,4 +310,5 @@ func Register(r *macaron.Macaron) { InitAppPluginRoutes(r) + r.NotFound(NotFoundHandler) } diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index bf4d7f4353e..c9ba30e2407 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -35,11 +35,12 @@ type AlertTestCommand 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 { diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index b174f88d649..2e58d9155fc 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -96,7 +96,7 @@ func OAuthLogin(ctx *middleware.Context) { } sslcli := &http.Client{Transport: tr} - oauthCtx = context.TODO() + oauthCtx = context.Background() oauthCtx = context.WithValue(oauthCtx, oauth2.HTTPClient, sslcli) } @@ -106,6 +106,8 @@ func OAuthLogin(ctx *middleware.Context) { ctx.Handle(500, "login.OAuthLogin(NewTransportWithCode)", err) 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") diff --git a/pkg/metrics/publish.go b/pkg/metrics/publish.go index 4255481b8d1..70db3fc0f86 100644 --- a/pkg/metrics/publish.go +++ b/pkg/metrics/publish.go @@ -101,6 +101,7 @@ func sendUsageStats() { metrics["stats.plugins.apps.count"] = len(plugins.Apps) metrics["stats.plugins.panels.count"] = len(plugins.Panels) metrics["stats.plugins.datasources.count"] = len(plugins.DataSources) + metrics["stats.alerts.count"] = statsQuery.Result.AlertCount dsStats := m.GetDataSourceStatsQuery{} if err := bus.Dispatch(&dsStats); err != nil { diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index cb3f4480821..a546d7e76fc 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -187,6 +187,7 @@ func (ctx *Context) Handle(status int, title string, err error) { } ctx.Data["Title"] = title + ctx.Data["AppSubUrl"] = setting.AppSubUrl ctx.HTML(status, strconv.Itoa(status)) } diff --git a/pkg/middleware/recovery.go b/pkg/middleware/recovery.go index 8843f2e55d3..b63bc623549 100644 --- a/pkg/middleware/recovery.go +++ b/pkg/middleware/recovery.go @@ -19,53 +19,14 @@ import ( "bytes" "fmt" "io/ioutil" - "net/http" "runtime" "gopkg.in/macaron.v1" - "github.com/go-macaron/inject" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/setting" ) -const ( - panicHtml = ` -PANIC: %s - - - -

PANIC

-
%s
-
%s
- -` -) - var ( dunno = []byte("???") centerDot = []byte("ยท") @@ -151,21 +112,34 @@ func Recovery() macaron.Handler { 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 + + if theErr, ok := err.(error); ok { + c.Data["Title"] = theErr.Error() + } - // 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)) + c.Data["ErrorMsg"] = string(stack) } - res.WriteHeader(http.StatusInternalServerError) - if nil != body { - res.Write(body) - } + 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) + // } } }() diff --git a/pkg/models/stats.go b/pkg/models/stats.go index 067dec763e5..09c251b6cd7 100644 --- a/pkg/models/stats.go +++ b/pkg/models/stats.go @@ -5,6 +5,7 @@ type SystemStats struct { UserCount int64 OrgCount int64 PlaylistCount int64 + AlertCount int64 } type DataSourceStats struct { @@ -29,6 +30,7 @@ type AdminStats struct { DataSourceCount int `json:"data_source_count"` PlaylistCount int `json:"playlist_count"` StarredDbCount int `json:"starred_db_count"` + AlertCount int `json:"alert_count"` } type GetAdminStatsQuery struct { diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index b73db9d590e..e58dbf1d583 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -23,6 +23,7 @@ type QueryCondition struct { Query AlertQuery Reducer QueryReducer Evaluator AlertEvaluator + Operator string HandleRequest tsdb.HandleRequestFunc } @@ -72,6 +73,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio return &alerting.ConditionResult{ Firing: evalMatchCount > 0, NoDataFound: emptySerieCount == len(seriesList), + Operator: c.Operator, EvalMatches: matches, }, nil } @@ -168,8 +170,12 @@ func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, erro if err != nil { return nil, err } - condition.Evaluator = evaluator + + operatorJson := model.Get("operator") + operator := operatorJson.Get("type").MustString("and") + condition.Operator = operator + return &condition, nil } diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 711416970c5..2b252da8c4b 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -17,7 +17,7 @@ type EvalContext struct { EvalMatches []*EvalMatch Logs []*ResultLogEntry Error error - Description string + ConditionEvals string StartTime time.Time EndTime time.Time Rule *Rule diff --git a/pkg/services/alerting/eval_handler.go b/pkg/services/alerting/eval_handler.go index 538c639abb8..075d0833fdf 100644 --- a/pkg/services/alerting/eval_handler.go +++ b/pkg/services/alerting/eval_handler.go @@ -1,6 +1,8 @@ package alerting import ( + "strconv" + "strings" "time" "github.com/grafana/grafana/pkg/log" @@ -21,7 +23,10 @@ func NewEvalHandler() *DefaultEvalHandler { func (e *DefaultEvalHandler) Eval(context *EvalContext) { 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) if err != nil { context.Error = err @@ -32,15 +37,23 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) { 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.ConditionEvals = conditionEvals + " = " + strconv.FormatBool(firing) context.Firing = firing context.EndTime = time.Now() elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond diff --git a/pkg/services/alerting/eval_handler_test.go b/pkg/services/alerting/eval_handler_test.go index 4c2ec24b506..84905870f52 100644 --- a/pkg/services/alerting/eval_handler_test.go +++ b/pkg/services/alerting/eval_handler_test.go @@ -8,12 +8,13 @@ import ( ) type conditionStub struct { - firing bool - matches []*EvalMatch + firing bool + operator string + matches []*EvalMatch } 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) { @@ -29,18 +30,102 @@ func TestAlertingExecutor(t *testing.T) { handler.Eval(context) So(context.Firing, ShouldEqual, true) + So(context.ConditionEvals, ShouldEqual, "true = true") }) Convey("Show return false with not passing asdf", func() { context := NewEvalContext(context.TODO(), &Rule{ 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) 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") }) }) } diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index cc2561473e3..566fbdb2898 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -24,6 +24,7 @@ type Notifier interface { type ConditionResult struct { Firing bool NoDataFound bool + Operator string EvalMatches []*EvalMatch } diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 809640ed4a7..bdf53798e34 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -26,11 +26,32 @@ type Rule struct { } type ValidationError struct { - Reason string + Reason string + Err error + Alertid int64 + DashboardId int64 + PanelId int64 } 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 ( @@ -83,7 +104,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { for _, v := range ruleDef.Settings.Get("notifications").MustArray() { jsonModel := simplejson.NewFromAny(v) 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 { model.Notifications = append(model.Notifications, id) } @@ -93,10 +114,10 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { conditionModel := simplejson.NewFromAny(condition) conditionType := conditionModel.Get("type").MustString() 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 { 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 { model.Conditions = append(model.Conditions, queryCondition) } diff --git a/pkg/services/alerting/scheduler.go b/pkg/services/alerting/scheduler.go index b6ef1a63ff8..151f802ec15 100644 --- a/pkg/services/alerting/scheduler.go +++ b/pkg/services/alerting/scheduler.go @@ -39,6 +39,9 @@ func (s *SchedulerImpl) Update(rules []*Rule) { offset := ((rule.Frequency * 1000) / int64(len(rules))) * int64(i) 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 } diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go index 7580996ad57..dd1a8111332 100644 --- a/pkg/services/sqlstore/stats.go +++ b/pkg/services/sqlstore/stats.go @@ -39,7 +39,11 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error { ( SELECT COUNT(*) FROM ` + dialect.Quote("playlist") + ` - ) AS playlist_count + ) AS playlist_count, + ( + SELECT COUNT(*) + FROM ` + dialect.Quote("alert") + ` + ) AS alert_count ` var stats m.SystemStats @@ -85,7 +89,11 @@ func GetAdminStats(query *m.GetAdminStatsQuery) error { ( SELECT COUNT(DISTINCT ` + dialect.Quote("dashboard_id") + ` ) 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 diff --git a/pkg/tsdb/influxdb/influxdb.go b/pkg/tsdb/influxdb/influxdb.go index a64a6af2ee9..5043a2deffc 100644 --- a/pkg/tsdb/influxdb/influxdb.go +++ b/pkg/tsdb/influxdb/influxdb.go @@ -18,7 +18,6 @@ import ( type InfluxDBExecutor struct { *tsdb.DataSourceInfo QueryParser *InfluxdbQueryParser - QueryBuilder *QueryBuilder ResponseParser *ResponseParser } @@ -26,7 +25,6 @@ func NewInfluxDBExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor { return &InfluxDBExecutor{ DataSourceInfo: dsInfo, QueryParser: &InfluxdbQueryParser{}, - QueryBuilder: &QueryBuilder{}, ResponseParser: &ResponseParser{}, } } @@ -51,7 +49,7 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, return result.WithError(err) } - rawQuery, err := e.QueryBuilder.Build(query, context) + rawQuery, err := query.Build(context) if err != nil { return result.WithError(err) } @@ -84,6 +82,10 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, return result.WithError(err) } + if response.Err != nil { + return result.WithError(response.Err) + } + result.QueryResults = make(map[string]*tsdb.QueryResult) result.QueryResults["A"] = e.ResponseParser.Parse(&response, query) diff --git a/pkg/tsdb/influxdb/model_parser.go b/pkg/tsdb/influxdb/model_parser.go index 2db7a78ed75..410bf8f6e82 100644 --- a/pkg/tsdb/influxdb/model_parser.go +++ b/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) { policy := model.Get("policy").MustString("default") rawQuery := model.Get("query").MustString("") + useRawQuery := model.Get("rawQuery").MustBool(false) alias := model.Get("alias").MustString("") measurement := model.Get("measurement").MustString("") @@ -54,6 +55,7 @@ func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSo RawQuery: rawQuery, Interval: interval, Alias: alias, + UseRawQuery: useRawQuery, }, nil } diff --git a/pkg/tsdb/influxdb/models.go b/pkg/tsdb/influxdb/models.go index 0dcecd20773..44e05608290 100644 --- a/pkg/tsdb/influxdb/models.go +++ b/pkg/tsdb/influxdb/models.go @@ -8,6 +8,7 @@ type Query struct { GroupBy []*QueryPart Selects []*Select RawQuery string + UseRawQuery bool Alias string Interval string diff --git a/pkg/tsdb/influxdb/query_builder.go b/pkg/tsdb/influxdb/query.go similarity index 62% rename from pkg/tsdb/influxdb/query_builder.go rename to pkg/tsdb/influxdb/query.go index b783ff5c603..094f98cbe53 100644 --- a/pkg/tsdb/influxdb/query_builder.go +++ b/pkg/tsdb/influxdb/query.go @@ -2,7 +2,6 @@ package influxdb import ( "fmt" - "strconv" "strings" "regexp" @@ -11,31 +10,30 @@ import ( ) 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 = 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) 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 } -func (qb *QueryBuilder) renderTags(query *Query) []string { +func (query *Query) renderTags() []string { var res []string for i, tag := range query.Tags { str := "" @@ -59,13 +57,12 @@ func (qb *QueryBuilder) renderTags(query *Query) []string { } textValue := "" - numericValue, err := strconv.ParseFloat(tag.Value, 64) // quote value unless regex or number if tag.Operator == "=~" || tag.Operator == "!~" { textValue = tag.Value - } else if err == nil { - textValue = fmt.Sprintf("%v", numericValue) + } else if tag.Operator == "<" || tag.Operator == ">" { + textValue = tag.Value } else { textValue = fmt.Sprintf("'%s'", tag.Value) } @@ -76,7 +73,7 @@ func (qb *QueryBuilder) renderTags(query *Query) []string { 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 to := "" @@ -87,7 +84,7 @@ func (qb *QueryBuilder) renderTimeFilter(query *Query, queryContext *tsdb.QueryC 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 " var selectors []string @@ -103,19 +100,26 @@ func (qb *QueryBuilder) renderSelectors(query *Query, queryContext *tsdb.QueryCo return res + strings.Join(selectors, ", ") } -func (qb *QueryBuilder) renderMeasurement(query *Query) string { +func (query *Query) renderMeasurement() string { policy := "" if query.Policy == "" || query.Policy == "default" { policy = "" } else { 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 " - conditions := qb.renderTags(query) + conditions := query.renderTags() res += strings.Join(conditions, " ") if len(conditions) > 0 { res += " AND " @@ -124,7 +128,7 @@ func (qb *QueryBuilder) renderWhereClause(query *Query) string { return res } -func (qb *QueryBuilder) renderGroupBy(query *Query, queryContext *tsdb.QueryContext) string { +func (query *Query) renderGroupBy(queryContext *tsdb.QueryContext) string { groupBy := "" for i, group := range query.GroupBy { if i == 0 { diff --git a/pkg/tsdb/influxdb/query_builder_test.go b/pkg/tsdb/influxdb/query_test.go similarity index 66% rename from pkg/tsdb/influxdb/query_builder_test.go rename to pkg/tsdb/influxdb/query_test.go index 408db18e549..b6af67e5e42 100644 --- a/pkg/tsdb/influxdb/query_builder_test.go +++ b/pkg/tsdb/influxdb/query_test.go @@ -12,7 +12,6 @@ import ( func TestInfluxdbQueryBuilder(t *testing.T) { Convey("Influxdb query builder", t, func() { - builder := QueryBuilder{} qp1, _ := NewQueryPart("field", []string{"value"}) qp2, _ := NewQueryPart("mean", []string{}) @@ -37,7 +36,7 @@ func TestInfluxdbQueryBuilder(t *testing.T) { Interval: "10s", } - rawQuery, err := builder.Build(query, queryContext) + rawQuery, err := query.Build(queryContext) So(err, ShouldBeNil) 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", } - rawQuery, err := builder.Build(query, queryContext) + rawQuery, err := query.Build(queryContext) 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)`) }) Convey("can render time range", func() { query := Query{} - builder := &QueryBuilder{} Convey("render from: 2h to now-1h", func() { query := Query{} 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() { 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}, Interval: "10s", RawQuery: "Raw query", + UseRawQuery: true, } - rawQuery, err := builder.Build(query, queryContext) + rawQuery, err := query.Build(queryContext) So(err, ShouldBeNil) So(rawQuery, ShouldEqual, `Raw query`) }) @@ -89,37 +88,55 @@ func TestInfluxdbQueryBuilder(t *testing.T) { Convey("can render normal tags without operator", func() { 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() { 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() { 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() { 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() { 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/`) }) }) } diff --git a/public/app/app.ts b/public/app/app.ts index c004bac4177..22431a5110c 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -40,7 +40,6 @@ export class GrafanaApp { init() { var app = angular.module('grafana', []); - app.constant('grafanaVersion', "@grafanaVersion@"); moment.locale(config.bootData.user.locale); diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index ed3425f670f..83b55ac476f 100644 --- a/public/app/core/components/grafana_app.ts +++ b/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.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', () => { lastActivity = 0; diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 567df736a4e..4aa2e7eb64a 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -6,11 +6,10 @@ import "./directives/dash_class"; import "./directives/confirm_click"; import "./directives/dash_edit_link"; import "./directives/dropdown_typeahead"; -import "./directives/grafana_version_check"; import "./directives/metric_segment"; import "./directives/misc"; import "./directives/ng_model_on_blur"; -import "./directives/password_strenght"; +import "./directives/password_strength"; import "./directives/spectrum_picker"; import "./directives/tags"; import "./directives/value_select_dropdown"; diff --git a/public/app/core/directives/grafana_version_check.js b/public/app/core/directives/grafana_version_check.js deleted file mode 100644 index bee437b8183..00000000000 --- a/public/app/core/directives/grafana_version_check.js +++ /dev/null @@ -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(' ' + - ' ' + - 'New version available: ' + response.data.version + - ''); - } - }); - } - }; - }); -}); diff --git a/public/app/core/directives/ng_model_on_blur.js b/public/app/core/directives/ng_model_on_blur.js index 6f4a55b53f0..09c87f5fabe 100644 --- a/public/app/core/directives/ng_model_on_blur.js +++ b/public/app/core/directives/ng_model_on_blur.js @@ -47,7 +47,7 @@ function (coreModule, kbn, rangeUtil) { if (ctrl.$isEmpty(modelValue)) { return true; } - if (viewValue.indexOf('$') === 0) { + if (viewValue.indexOf('$') === 0 || viewValue.indexOf('+$') === 0) { return true; // allow template variable } var info = rangeUtil.describeTextRange(viewValue); diff --git a/public/app/core/directives/password_strenght.js b/public/app/core/directives/password_strength.js similarity index 100% rename from public/app/core/directives/password_strenght.js rename to public/app/core/directives/password_strength.js diff --git a/public/app/core/utils/kbn.js b/public/app/core/utils/kbn.js index a807a249235..78489dcae58 100644 --- a/public/app/core/utils/kbn.js +++ b/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.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.Mbits = kbn.formatBuilders.decimalSIPrefix('bits', 2); + kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bps', 2); 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 kbn.valueFormats.ops = kbn.formatBuilders.simpleCountUnit('ops'); diff --git a/public/app/features/admin/partials/stats.html b/public/app/features/admin/partials/stats.html index bcc246b9e82..92e32f4f947 100644 --- a/public/app/features/admin/partials/stats.html +++ b/public/app/features/admin/partials/stats.html @@ -46,6 +46,10 @@ Total starred dashboards {{ctrl.stats.starred_db_count}} + + Total alerts + {{ctrl.stats.alert_count}} + diff --git a/public/app/features/alerting/alert_def.ts b/public/app/features/alerting/alert_def.ts index 8ee1981f3d8..52089560296 100644 --- a/public/app/features/alerting/alert_def.ts +++ b/public/app/features/alerting/alert_def.ts @@ -28,6 +28,11 @@ var evalFunctions = [ {text: 'HAS NO VALUE' , value: 'no_value'} ]; +var evalOperators = [ + {text: 'OR', value: 'or'}, + {text: 'AND', value: 'and'}, +]; + var reducerTypes = [ {text: 'avg()', value: 'avg'}, {text: 'min()', value: 'min'}, @@ -116,6 +121,7 @@ export default { getStateDisplayModel: getStateDisplayModel, conditionTypes: conditionTypes, evalFunctions: evalFunctions, + evalOperators: evalOperators, noDataModes: noDataModes, executionErrorModes: executionErrorModes, reducerTypes: reducerTypes, diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index c882a6ac3bf..6757cfc0d91 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -18,6 +18,7 @@ export class AlertTabCtrl { alert: any; conditionModels: any; evalFunctions: any; + evalOperators: any; noDataModes: any; executionErrorModes: any; addNotificationSegment; @@ -41,6 +42,7 @@ export class AlertTabCtrl { this.$scope.ctrl = this; this.subTabIndex = 0; this.evalFunctions = alertDef.evalFunctions; + this.evalOperators = alertDef.evalOperators; this.conditionTypes = alertDef.conditionTypes; this.noDataModes = alertDef.noDataModes; this.executionErrorModes = alertDef.executionErrorModes; @@ -194,6 +196,7 @@ export class AlertTabCtrl { query: {params: ['A', '5m', 'now']}, reducer: {type: 'avg', params: []}, evaluator: {type: 'gt', params: [null]}, + operator: {type: 'and'}, }; } @@ -250,6 +253,7 @@ export class AlertTabCtrl { cm.queryPart = new QueryPart(source.query, alertDef.alertQueryDef); cm.reducerPart = alertDef.createReducerPart(source.reducer); cm.evaluator = source.evaluator; + cm.operator = source.operator; return cm; } diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index 5b85ec105ae..f3fcf31fad6 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -38,23 +38,23 @@
Conditions
- AND + WHEN
- + OF
- +
- + - +
-
- - - - - - -
-
diff --git a/public/app/features/templating/adhoc_variable.ts b/public/app/features/templating/adhoc_variable.ts index 5ebffb0b0e6..b3c9980c7c7 100644 --- a/public/app/features/templating/adhoc_variable.ts +++ b/public/app/features/templating/adhoc_variable.ts @@ -26,7 +26,7 @@ export class AdhocVariable implements Variable { return Promise.resolve(); } - getModel() { + getSaveModel() { assignModelProperties(this.model, this, this.defaults); return this.model; } diff --git a/public/app/features/templating/constant_variable.ts b/public/app/features/templating/constant_variable.ts index 59659459f85..1dcc473740f 100644 --- a/public/app/features/templating/constant_variable.ts +++ b/public/app/features/templating/constant_variable.ts @@ -24,7 +24,7 @@ export class ConstantVariable implements Variable { assignModelProperties(this, model, this.defaults); } - getModel() { + getSaveModel() { assignModelProperties(this.model, this, this.defaults); return this.model; } diff --git a/public/app/features/templating/custom_variable.ts b/public/app/features/templating/custom_variable.ts index 90ce08cf9e4..4fadcd26742 100644 --- a/public/app/features/templating/custom_variable.ts +++ b/public/app/features/templating/custom_variable.ts @@ -34,7 +34,7 @@ export class CustomVariable implements Variable { return this.variableSrv.setOptionAsCurrent(this, option); } - getModel() { + getSaveModel() { assignModelProperties(this.model, this, this.defaults); return this.model; } diff --git a/public/app/features/templating/datasource_variable.ts b/public/app/features/templating/datasource_variable.ts index d43c0dd486d..bfd4d965029 100644 --- a/public/app/features/templating/datasource_variable.ts +++ b/public/app/features/templating/datasource_variable.ts @@ -30,8 +30,11 @@ export class DatasourceVariable implements Variable { this.refresh = 1; } - getModel() { + getSaveModel() { assignModelProperties(this.model, this, this.defaults); + + // dont persist options + this.model.options = []; return this.model; } diff --git a/public/app/features/templating/interval_variable.ts b/public/app/features/templating/interval_variable.ts index a1cfbf324c0..ab1b0e59442 100644 --- a/public/app/features/templating/interval_variable.ts +++ b/public/app/features/templating/interval_variable.ts @@ -34,7 +34,7 @@ export class IntervalVariable implements Variable { this.refresh = 2; } - getModel() { + getSaveModel() { assignModelProperties(this.model, this, this.defaults); return this.model; } diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index e485072eed0..8006e356d85 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -136,7 +136,7 @@
Custom Options
- Values separated by comma + Values separated by comma
diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts index c67a4d09209..e083aa2aab5 100644 --- a/public/app/features/templating/query_variable.ts +++ b/public/app/features/templating/query_variable.ts @@ -47,9 +47,15 @@ export class QueryVariable implements Variable { assignModelProperties(this, model, this.defaults); } - getModel() { + getSaveModel() { // copy back model properties to model assignModelProperties(this.model, this, this.defaults); + + // remove options + if (this.refresh !== 0) { + this.model.options = []; + } + return this.model; } diff --git a/public/app/features/templating/specs/query_variable_specs.ts b/public/app/features/templating/specs/query_variable_specs.ts index 8a2aef65be2..591362e0d84 100644 --- a/public/app/features/templating/specs/query_variable_specs.ts +++ b/public/app/features/templating/specs/query_variable_specs.ts @@ -25,7 +25,7 @@ describe('QueryVariable', function() { variable.regex = 'asd'; variable.sort = 50; - var model = variable.getModel(); + var model = variable.getSaveModel(); expect(model.options.length).to.be(1); expect(model.options[0].text).to.be('test'); expect(model.datasource).to.be('google'); @@ -33,7 +33,14 @@ describe('QueryVariable', function() { 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); + }); + }); }); diff --git a/public/app/features/templating/variable.ts b/public/app/features/templating/variable.ts index 0cd0cb9f847..381f1ea7a3c 100644 --- a/public/app/features/templating/variable.ts +++ b/public/app/features/templating/variable.ts @@ -10,7 +10,7 @@ export interface Variable { dependsOn(variable); setValueFromUrl(urlValue); getValueForUrl(); - getModel(); + getSaveModel(); } export var variableTypes = {}; diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index bb6f4f7cde3..ac2948dbd4b 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -20,12 +20,9 @@ export class VariableSrv { this.dashboard = dashboard; // 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); - // register event to sync back to persisted model - this.dashboard.events.on('prepare-save-model', this.syncToDashboardModel.bind(this)); - // init variables for (let variable of this.variables) { variable.initLock = this.$q.defer(); @@ -99,12 +96,6 @@ export class VariableSrv { return variable; } - syncToDashboardModel() { - this.dashboard.templating.list = this.variables.map(variable => { - return variable.getModel(); - }); - } - updateOptions(variable) { return variable.updateOptions(); } diff --git a/public/app/partials/bootstrap/tab.html b/public/app/partials/bootstrap/tab.html deleted file mode 100644 index d76dd67caf2..00000000000 --- a/public/app/partials/bootstrap/tab.html +++ /dev/null @@ -1,3 +0,0 @@ -
  • - {{heading}} -
  • diff --git a/public/app/partials/bootstrap/tabset.html b/public/app/partials/bootstrap/tabset.html deleted file mode 100644 index a811f83fbb5..00000000000 --- a/public/app/partials/bootstrap/tabset.html +++ /dev/null @@ -1,11 +0,0 @@ -
    - -
    -
    -
    -
    -
    diff --git a/public/app/partials/error.html b/public/app/partials/error.html index d38f71ac5d8..0401d6d98db 100644 --- a/public/app/partials/error.html +++ b/public/app/partials/error.html @@ -1,11 +1,12 @@ + + -
    -
    +
    -
    -

    Page not found (404)

    -
    - -
    +
    diff --git a/public/app/plugins/app/testdata/dashboards/graph_last_1h.json b/public/app/plugins/app/testdata/dashboards/graph_last_1h.json index c64ab84f338..c314feb56ad 100644 --- a/public/app/plugins/app/testdata/dashboards/graph_last_1h.json +++ b/public/app/plugins/app/testdata/dashboards/graph_last_1h.json @@ -1,5 +1,5 @@ { - "revision": 5, + "revision": 6, "title": "TestData - Graph Panel Last 1h", "tags": [ "grafana-test" @@ -7,8 +7,48 @@ "style": "dark", "timezone": "browser", "editable": true, - "hideControls": 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": [ { "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, @@ -332,7 +378,13 @@ "type": "text" } ], - "title": "New row" + "title": "New row", + "showTitle": false, + "titleSize": "h6", + "isNew": false, + "repeat": null, + "repeatRowId": null, + "repeatIteration": null }, { "collapse": false, @@ -371,7 +423,7 @@ "yaxis": 2 } ], - "span": 7.99561403508772, + "span": 8, "stack": false, "steppedLine": false, "targets": [ @@ -432,12 +484,18 @@ "isNew": true, "links": [], "mode": "markdown", - "span": 4.00438596491228, + "span": 4, "title": "", "type": "text" } ], - "title": "New row" + "title": "New row", + "showTitle": false, + "titleSize": "h6", + "isNew": false, + "repeat": null, + "repeatRowId": null, + "repeatIteration": null }, { "collapse": false, @@ -545,7 +603,7 @@ "points": false, "renderer": "flot", "seriesOverrides": [], - "span": 3, + "span": 4, "stack": false, "steppedLine": false, "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": {}, "bars": false, @@ -624,7 +707,7 @@ "zindex": -3 } ], - "span": 5, + "span": 8, "stack": true, "steppedLine": false, "targets": [ @@ -687,49 +770,149 @@ "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 + ] } diff --git a/public/app/plugins/app/testdata/plugin.json b/public/app/plugins/app/testdata/plugin.json index 63f88df8140..f8723f95a5f 100644 --- a/public/app/plugins/app/testdata/plugin.json +++ b/public/app/plugins/app/testdata/plugin.json @@ -9,7 +9,7 @@ "name": "Grafana Project", "url": "http://grafana.org" }, - "version": "1.0.14", + "version": "1.0.15", "updated": "2016-09-26" }, diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js index fde9e8f09ea..07648779648 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.js +++ b/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.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; 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 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; } else if (/^\d+$/.test(target.period)) { period = parseInt(target.period, 10); } else { period = kbn.interval_to_seconds(templateSrv.replace(target.period, options.scopedVars)); } - if (query.period < 60) { + if (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; diff --git a/public/app/plugins/panel/alertlist/editor.html b/public/app/plugins/panel/alertlist/editor.html index b2038df34b8..344af7408fc 100644 --- a/public/app/plugins/panel/alertlist/editor.html +++ b/public/app/plugins/panel/alertlist/editor.html @@ -11,6 +11,7 @@ Max items
    +
    State filter
    diff --git a/public/app/plugins/panel/alertlist/module.ts b/public/app/plugins/panel/alertlist/module.ts index 543c5f04833..d03fad49073 100644 --- a/public/app/plugins/panel/alertlist/module.ts +++ b/public/app/plugins/panel/alertlist/module.ts @@ -25,7 +25,8 @@ class AlertListPanel extends PanelCtrl { panelDefaults = { show: 'current', limit: 10, - stateFilter: [] + stateFilter: [], + onlyAlertsOnDashboard: false }; @@ -71,9 +72,13 @@ class AlertListPanel extends PanelCtrl { var params: any = { limit: this.panel.limit, 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.to = dateMath.parse(this.dashboard.time.to).unix() * 1000; @@ -93,6 +98,10 @@ class AlertListPanel extends PanelCtrl { state: this.panel.stateFilter }; + if (this.panel.onlyAlertsOnDashboard) { + params.dashboardId = this.dashboard.id; + } + this.backendSrv.get(`/api/alerts`, params) .then(res => { this.currentAlerts = _.map(res, al => { diff --git a/public/app/plugins/panel/graph/graph_tooltip.js b/public/app/plugins/panel/graph/graph_tooltip.js index 3aec815428f..40ed293ff4e 100644 --- a/public/app/plugins/panel/graph/graph_tooltip.js +++ b/public/app/plugins/panel/graph/graph_tooltip.js @@ -195,7 +195,7 @@ function ($) { } var highlightClass = ''; - if (item && i === item.seriesIndex) { + if (item && hoverInfo.index === item.seriesIndex) { highlightClass = 'graph-tooltip-list-item--highlight'; } diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 8247b2cd690..3af89abf1d7 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -208,11 +208,8 @@ class SingleStatCtrl extends MetricsPanelCtrl { } // 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 @@ -526,7 +523,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { elem.toggleClass('pointer', 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 { linkInfo = null; } diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index b26e8da68c1..25db63858e8 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -51,11 +51,9 @@ @import "components/tagsinput"; @import "components/tables_lists"; @import "components/search"; -@import "components/tightform"; @import "components/gf-form"; @import "components/sidemenu"; @import "components/navbar"; -@import "components/gfbox"; @import "components/timepicker"; @import "components/filter-controls"; @import "components/filter-list"; diff --git a/public/sass/components/_gfbox.scss b/public/sass/components/_gfbox.scss deleted file mode 100644 index b4e31a2397d..00000000000 --- a/public/sass/components/_gfbox.scss +++ /dev/null @@ -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; - } -} diff --git a/public/sass/components/_navs.scss b/public/sass/components/_navs.scss index 4bb02d7b514..abbb4ba5042 100644 --- a/public/sass/components/_navs.scss +++ b/public/sass/components/_navs.scss @@ -87,7 +87,7 @@ } // temp hack -.modal-body, .gf-box { +.modal-body { .nav-tabs { border-bottom: none; } diff --git a/public/sass/components/_query_editor.scss b/public/sass/components/_query_editor.scss index 4c807ed0e6b..f2d29572e37 100644 --- a/public/sass/components/_query_editor.scss +++ b/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; + } +} diff --git a/public/sass/components/_row.scss b/public/sass/components/_row.scss index 25d6b9eb622..840db839607 100644 --- a/public/sass/components/_row.scss +++ b/public/sass/components/_row.scss @@ -74,12 +74,11 @@ .add-panel-panels-scroll { width: 100%; overflow: auto; + -ms-overflow-style: none; &::-webkit-scrollbar { display: none } - - -ms-overflow-style: none; } .add-panel-panels { diff --git a/public/sass/components/_shortcuts.scss b/public/sass/components/_shortcuts.scss index 4e2d56503d9..1dedb062183 100644 --- a/public/sass/components/_shortcuts.scss +++ b/public/sass/components/_shortcuts.scss @@ -6,6 +6,8 @@ } .shortcut-table { + margin-bottom: $spacer; + .shortcut-table-category-header { font-weight: normal; font-size: $font-size-h6; @@ -26,8 +28,6 @@ text-align: right; color: $text-color; } - - margin-bottom: $spacer; } .shortcut-table-key { diff --git a/public/sass/components/_submenu.scss b/public/sass/components/_submenu.scss index 14f0658f7cb..93376e0d106 100644 --- a/public/sass/components/_submenu.scss +++ b/public/sass/components/_submenu.scss @@ -7,11 +7,12 @@ } .annotation-segment { + padding: 8px 7px; + label.cr1 { margin-left: 5px; margin-top: 3px; } - padding: 8px 7px; } .submenu-item { @@ -31,14 +32,14 @@ .variable-value-link { padding-right: 10px; - .label-tag { - margin: 0 5px; - } - padding: 8px 7px; box-sizing: content-box; display: inline-block; color: $text-color; + + .label-tag { + margin: 0 5px; + } } .variable-link-wrapper { diff --git a/public/sass/components/_tabbed_view.scss b/public/sass/components/_tabbed_view.scss index f1b59fa2363..e72252330a1 100644 --- a/public/sass/components/_tabbed_view.scss +++ b/public/sass/components/_tabbed_view.scss @@ -38,10 +38,10 @@ background-color: transparent; border: none; padding: ($tabs-padding-top + $tabs-top-margin) $spacer $tabs-padding-bottom; + color: $text-color; i { font-size: 120%; } - color: $text-color; &:hover { color: $white; } diff --git a/public/sass/components/_tightform.scss b/public/sass/components/_tightform.scss deleted file mode 100644 index 7c43854b06c..00000000000 --- a/public/sass/components/_tightform.scss +++ /dev/null @@ -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; -} diff --git a/public/sass/components/_timepicker.scss b/public/sass/components/_timepicker.scss index 1ecc2c7a166..a5f311cdb46 100644 --- a/public/sass/components/_timepicker.scss +++ b/public/sass/components/_timepicker.scss @@ -65,15 +65,17 @@ } .gf-timepicker-component { - margin-bottom: 10px; + padding: $spacer/2 0 $spacer 0; + td { padding: 1px; } button.btn-sm { @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl); + font-size: $font-size-sm; background-image: none; border: none; - padding: 6px 10px; + padding: 5px 11px; color: $text-color; &.active span { color: $blue; diff --git a/public/sass/layout/_page.scss b/public/sass/layout/_page.scss index 6203f4be083..5702a1aa260 100644 --- a/public/sass/layout/_page.scss +++ b/public/sass/layout/_page.scss @@ -62,12 +62,6 @@ .admin-page { max-width: 800px; margin-left: 10px; - .gf-box { - margin-top: 0; - } - .gf-box-body { - min-height: 0; - } h2 { margin-left: 15px; margin-bottom: 0px; diff --git a/public/sass/pages/_alerting.scss b/public/sass/pages/_alerting.scss index b739485f170..70124f0cd85 100644 --- a/public/sass/pages/_alerting.scss +++ b/public/sass/pages/_alerting.scss @@ -61,7 +61,6 @@ } &--ok { - box-shadow: 0 0 5px rgba(0,200,0,10.8); .panel-alert-icon:before { color: $online; content: "\e611"; diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 213220674cd..73b2772abc8 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -172,6 +172,12 @@ div.flot-text { } } +.panel-in-fullscreen { + .panel-drop-zone { + display: none !important; + } +} + .panel-time-info { font-weight: bold; float: right; diff --git a/public/vendor/angular-ui/ui-bootstrap-tpls.js b/public/vendor/angular-ui/ui-bootstrap-tpls.js index 2e7fd621809..87120b66ce1 100644 --- a/public/vendor/angular-ui/ui-bootstrap-tpls.js +++ b/public/vendor/angular-ui/ui-bootstrap-tpls.js @@ -5,8 +5,8 @@ * Version: 0.13.4 - 2015-09-03 * 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', []) /** @@ -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 - - - - First Content! - Second Content! - -
    - - First Vertical Content! - Second Vertical Content! - - - First Justified Content! - Second Justified Content! - -
    -
    - */ -.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 - - -
    - - -
    - - First Tab - - Alert me! - Second Tab, with alert callback and html heading! - - - {{item.content}} - - -
    -
    - - 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!"); - }); - }; - }; - -
    - */ - -/** - * @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 - - - - - HTML in my titles?! - And some content, too! - - - Icon heading?!? - That's right. - - - - - */ -.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) { $templateCache.put("template/datepicker/datepicker.html", "
    \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", - "
  • \n" + - " {{heading}}\n" + - "
  • \n" + - ""); -}]); - -angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tabs/tabset.html", - "
    \n" + - "
      \n" + - "
      \n" + - "
      \n" + - "
      \n" + - "
      \n" + - "
      \n" + - ""); -}]); diff --git a/public/views/500.html b/public/views/500.html index 9565304e7ff..cb2e99595f1 100644 --- a/public/views/500.html +++ b/public/views/500.html @@ -5,28 +5,32 @@ - Grafana + Grafana - Error + + + + - + + -
      -
      - +
      + -
      -

      [[.Title]]

      - [[.ErrorMsg]] -
      +

      [[.Title]]

      + +
      [[.ErrorMsg]]
      +
      - - +