diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d293d04100..1242a0e1e06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ * **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) * **Cloudwatch**: Fixed cloudwatch datasource requesting to many datapoints, [#6544](https://github.com/grafana/grafana/issues/6544) +* **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/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/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 `<metric-query-editor-graphite />`. 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/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 ed73f2dc76d..4fa28f799b0 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -307,4 +307,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/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 = `<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 ( 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/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/tsdb/influxdb/query.go b/pkg/tsdb/influxdb/query.go index e95b54b0a18..094f98cbe53 100644 --- a/pkg/tsdb/influxdb/query.go +++ b/pkg/tsdb/influxdb/query.go @@ -2,7 +2,6 @@ package influxdb import ( "fmt" - "strconv" "strings" "regexp" @@ -58,13 +57,12 @@ func (query *Query) renderTags() []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) } diff --git a/pkg/tsdb/influxdb/query_test.go b/pkg/tsdb/influxdb/query_test.go index ee045444c9b..b6af67e5e42 100644 --- a/pkg/tsdb/influxdb/query_test.go +++ b/pkg/tsdb/influxdb/query_test.go @@ -106,13 +106,19 @@ func TestInfluxdbQueryBuilder(t *testing.T) { Convey("can render number tags", func() { query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001", Key: "key"}}} - So(strings.Join(query.renderTags(), ""), 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(query.renderTags(), ""), 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() { 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 8e9f64fd17f..4aa2e7eb64a 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -6,7 +6,6 @@ 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"; 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('<i class="icon-info-sign"></i> ' + - '<a href="http://grafana.org/download" target="_blank"> ' + - 'New version available: ' + response.data.version + - '</a>'); - } - }); - } - }; - }); -}); 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/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/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 @@ <h5 class="section-heading">Conditions</h5> <div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels"> <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> </div> <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> <span class="gf-form-label query-keyword">OF</span> </div> <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> </div> <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> - <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> - <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 class="gf-form"> <label class="gf-form-label"> diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index cf72bb87f61..c07f84e820c 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -52,7 +52,7 @@ export class DashboardCtrl { .catch($scope.onInitFailed.bind(this, 'Templating init failed', false)) // continue .finally(function() { - dynamicDashboardSrv.init(dashboard, variableSrv); + dynamicDashboardSrv.init(dashboard); dynamicDashboardSrv.process(); unsavedChangesSrv.init(dashboard, $scope); diff --git a/public/app/features/dashboard/dynamic_dashboard_srv.ts b/public/app/features/dashboard/dynamic_dashboard_srv.ts index 9f8fe7f7ab2..629098e290b 100644 --- a/public/app/features/dashboard/dynamic_dashboard_srv.ts +++ b/public/app/features/dashboard/dynamic_dashboard_srv.ts @@ -12,12 +12,12 @@ export class DynamicDashboardSrv { dashboard: any; variables: any; - init(dashboard, variableSrv) { + init(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) { return; } @@ -31,6 +31,8 @@ export class DynamicDashboardSrv { // cleanup scopedVars for (i = 0; i < this.dashboard.rows.length; i++) { row = this.dashboard.rows[i]; + delete row.scopedVars; + for (j = 0; j < row.panels.length; j++) { delete row.panels[j].scopedVars; } @@ -64,6 +66,8 @@ export class DynamicDashboardSrv { j = j - 1; } } + + row.panelSpanChanged(); } } diff --git a/public/app/features/dashboard/export/export_modal.ts b/public/app/features/dashboard/export/export_modal.ts index 57af9d9caf8..c334818b3e7 100644 --- a/public/app/features/dashboard/export/export_modal.ts +++ b/public/app/features/dashboard/export/export_modal.ts @@ -17,9 +17,7 @@ export class DashExportCtrl { constructor(private backendSrv, dashboardSrv, datasourceSrv, $scope) { 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(() => { this.dash = dash; }); diff --git a/public/app/features/dashboard/export/exporter.ts b/public/app/features/dashboard/export/exporter.ts index 9f9e326e848..5efcd498f67 100644 --- a/public/app/features/dashboard/export/exporter.ts +++ b/public/app/features/dashboard/export/exporter.ts @@ -11,19 +11,40 @@ export class DashboardExporter { constructor(private datasourceSrv) { } - makeExportable(dash) { + makeExportable(dashboard) { 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}); - dash.id = null; + var saveModel = dashboard.getSaveModelClone(); + saveModel.id = null; + + // undo repeat cleanup + dynSrv.process(); var inputs = []; var requires = {}; var datasources = {}; var promises = []; + var variableLookup: any = {}; + + for (let variable of saveModel.templating.list) { + variableLookup[variable.name] = variable; + } 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 => { if (ds.meta.builtIn) { return; @@ -50,7 +71,7 @@ export class DashboardExporter { }; // check up panel data sources - for (let row of dash.rows) { + for (let row of saveModel.rows) { for (let panel of row.panels) { if (panel.datasource !== undefined) { templateizeDatasourceUsage(panel); @@ -77,7 +98,7 @@ export class DashboardExporter { } // templatize template vars - for (let variable of dash.templating.list) { + for (let variable of saveModel.templating.list) { if (variable.type === 'query') { templateizeDatasourceUsage(variable); variable.options = []; @@ -87,7 +108,7 @@ export class DashboardExporter { } // templatize annotations vars - for (let annotationDef of dash.annotations.list) { + for (let annotationDef of saveModel.annotations.list) { templateizeDatasourceUsage(annotationDef); } @@ -105,7 +126,7 @@ export class DashboardExporter { }); // templatize constants - for (let variable of dash.templating.list) { + for (let variable of saveModel.templating.list) { if (variable.type === 'constant') { var refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase(); inputs.push({ @@ -133,7 +154,7 @@ export class DashboardExporter { newObj["__inputs"] = inputs; newObj["__requires"] = requires; - _.defaults(newObj, dash); + _.defaults(newObj, saveModel); return newObj; }).catch(err => { diff --git a/public/app/features/dashboard/model.ts b/public/app/features/dashboard/model.ts index ad28ce4643c..f0a32fd58fc 100644 --- a/public/app/features/dashboard/model.ts +++ b/public/app/features/dashboard/model.ts @@ -98,12 +98,14 @@ export class DashboardModel { var events = this.events; var meta = this.meta; var rows = this.rows; + var variables = this.templating.list; + delete this.events; delete this.meta; // 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); @@ -111,6 +113,8 @@ export class DashboardModel { this.events = events; this.meta = meta; this.rows = rows; + this.templating.list = variables; + return copy; } @@ -233,7 +237,6 @@ export class DashboardModel { } duplicatePanel(panel, row) { - var rowIndex = _.indexOf(this.rows, row); var newPanel = angular.copy(panel); newPanel.id = this.getNextPanelId(); @@ -241,9 +244,9 @@ export class DashboardModel { delete newPanel.repeatIteration; delete newPanel.repeatPanelId; delete newPanel.scopedVars; + delete newPanel.alert; - var currentRow = this.rows[rowIndex]; - currentRow.panels.push(newPanel); + row.addPanel(newPanel); return newPanel; } diff --git a/public/app/features/dashboard/partials/globalAlerts.html b/public/app/features/dashboard/partials/globalAlerts.html deleted file mode 100644 index 2c065c714fb..00000000000 --- a/public/app/features/dashboard/partials/globalAlerts.html +++ /dev/null @@ -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 <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> diff --git a/public/app/features/dashboard/row/add_panel.html b/public/app/features/dashboard/row/add_panel.html index 7f79e697484..5a4ecc118cd 100644 --- a/public/app/features/dashboard/row/add_panel.html +++ b/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"> - <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> diff --git a/public/app/features/dashboard/row/add_panel.ts b/public/app/features/dashboard/row/add_panel.ts index 3e2dc9e31d8..771f5e00b64 100644 --- a/public/app/features/dashboard/row/add_panel.ts +++ b/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) { var max = this.panelHits.length; var newIndex = this.activeIndex + direction; diff --git a/public/app/features/dashboard/row/row_ctrl.ts b/public/app/features/dashboard/row/row_ctrl.ts index f82f91e04af..e28f576c8ba 100644 --- a/public/app/features/dashboard/row/row_ctrl.ts +++ b/public/app/features/dashboard/row/row_ctrl.ts @@ -19,7 +19,6 @@ export class DashRowCtrl { if (this.row.isNew) { this.dropView = 1; - delete this.row.isNew; } } @@ -35,8 +34,8 @@ export class DashRowCtrl { title: config.new_panel_title, type: panelId, id: this.dashboard.getNextPanelId(), + isNew: true, }, - isNew: true, }; } else { dragObject = this.dashboard.getPanelInfoById(panelId); @@ -65,7 +64,7 @@ export class DashRowCtrl { this.row.panels.push(dragObject.panel); // if not new remove from source row - if (!dragObject.isNew) { + if (!dragObject.panel.isNew) { dragObject.row.removePanel(dragObject.panel, false); } } diff --git a/public/app/features/dashboard/row/row_model.ts b/public/app/features/dashboard/row/row_model.ts index 0729d43bf0a..d99e75b3621 100644 --- a/public/app/features/dashboard/row/row_model.ts +++ b/public/app/features/dashboard/row/row_model.ts @@ -33,7 +33,11 @@ export class DashboardRow { } getSaveModel() { + this.model = {}; assignModelProperties(this.model, this, this.defaults); + + // remove properties that dont server persisted purpose + delete this.model.isNew; return this.model; } diff --git a/public/app/features/dashboard/specs/dashboard_srv_specs.ts b/public/app/features/dashboard/specs/dashboard_srv_specs.ts index 520216f18ec..6646851e597 100644 --- a/public/app/features/dashboard/specs/dashboard_srv_specs.ts +++ b/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() { 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]); 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() { 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]); expect(dashboard.rows[0].panels[1].repeat).to.be(undefined); diff --git a/public/app/features/dashboard/specs/dynamic_dashboard_srv_specs.ts b/public/app/features/dashboard/specs/dynamic_dashboard_srv_specs.ts index 95de65b2459..93feddc0654 100644 --- a/public/app/features/dashboard/specs/dynamic_dashboard_srv_specs.ts +++ b/public/app/features/dashboard/specs/dynamic_dashboard_srv_specs.ts @@ -20,7 +20,6 @@ function dynamicDashScenario(desc, func) { beforeEach(angularMocks.inject(function(dashboardSrv) { ctx.dashboardSrv = dashboardSrv; - ctx.variableSrv = {}; var model = { rows: [], @@ -29,9 +28,8 @@ function dynamicDashScenario(desc, func) { setupFunc(model); ctx.dash = ctx.dashboardSrv.create(model); - ctx.variableSrv.variables = ctx.dash.templating.list; ctx.dynamicDashboardSrv = new DynamicDashboardSrv(); - ctx.dynamicDashboardSrv.init(ctx.dash, ctx.variableSrv); + ctx.dynamicDashboardSrv.init(ctx.dash); ctx.dynamicDashboardSrv.process(); ctx.rows = ctx.dash.rows; })); diff --git a/public/app/features/dashboard/specs/exporter_specs.ts b/public/app/features/dashboard/specs/exporter_specs.ts index fd3973206d1..0aaadae2b63 100644 --- a/public/app/features/dashboard/specs/exporter_specs.ts +++ b/public/app/features/dashboard/specs/exporter_specs.ts @@ -34,6 +34,14 @@ describe('given dashboard with repeated panels', function() { options: [] }); + dash.templating.list.push({ + name: 'ds', + type: 'datasource', + query: 'testdb', + current: {value: 'prod', text: 'prod'}, + options: [] + }); + dash.annotations.list.push({ name: 'logs', datasource: 'gfdb', @@ -49,6 +57,7 @@ describe('given dashboard with repeated panels', function() { datasource: '-- Mixed --', 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() { - 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() { diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 40b7365da23..3bd6cef569d 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -54,6 +54,12 @@ export class PanelCtrl { this.events.emit('panel-teardown'); this.events.removeAllListeners(); }); + + // we should do something interesting + // with newly added panels + if (this.panel.isNew) { + delete this.panel.isNew; + } } init() { @@ -188,6 +194,9 @@ export class PanelCtrl { duplicate() { this.dashboard.duplicatePanel(this.panel, this.row); + this.$timeout(() => { + this.$scope.$root.$broadcast('render'); + }); } updateColumnSpan(span) { diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index 3af80d25483..28ef0d2bd63 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/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 // limit the watchers on panels etc - var transparentLastState; - var lastHasAlertRule; + var transparentLastState = false; + var lastHasAlertRule = false; var lastAlertState; var hasAlertRule; var lastHeight = 0; @@ -91,6 +91,12 @@ module.directive('grafanaPanel', function($rootScope) { lastHeight = ctrl.containerHeight; } + // set initial transparency + if (ctrl.panel.transparent) { + transparentLastState = true; + panelContainer.addClass('panel-transparent', true); + } + ctrl.events.on('render', () => { if (lastHeight !== ctrl.containerHeight) { panelContainer.css({minHeight: ctrl.containerHeight}); diff --git a/public/app/features/panel/partials/query_editor_row.html b/public/app/features/panel/partials/query_editor_row.html index e8dbe1434e7..55933bbbae8 100644 --- a/public/app/features/panel/partials/query_editor_row.html +++ b/public/app/features/panel/partials/query_editor_row.html @@ -57,59 +57,3 @@ </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> 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 @@ <div ng-if="current.type === 'custom'" class="gf-form-group"> <h5 class="section-heading">Custom Options</h5> <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> </div> </div> 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 @@ -<li ng-class="{active: active, disabled: disabled}"> - <a href ng-click="select()" tab-heading-transclude>{{heading}}</a> -</li> 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 @@ -<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> 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 @@ +<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> 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 105b08c2b17..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,11 +68,19 @@ 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); @@ -82,7 +91,7 @@ function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) { period = 60; } if (range / period >= 1440) { - period = Math.ceil(range / 1440 / 60) * 60; + period = Math.ceil(range / 1440 / periodUnit) * periodUnit; } return period; diff --git a/public/app/plugins/panel/graph/graph_tooltip.js b/public/app/plugins/panel/graph/graph_tooltip.js index 3aec815428f..e3c5a1edf85 100644 --- a/public/app/plugins/panel/graph/graph_tooltip.js +++ b/public/app/plugins/panel/graph/graph_tooltip.js @@ -22,7 +22,7 @@ function ($) { var len = series.datapoints.points.length; for (var j = initial; j < len; j += ps) { // Special case of a non stepped line, highlight the very last point just before a null point - if ((series.datapoints.points[initial] != null && series.datapoints.points[j] == null && ! series.lines.steps) + if ((!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null) //normal case || series.datapoints.points[j] > posX) { return Math.max(j - ps, 0)/ps; @@ -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 dfb8506d295..53a96cf9291 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -50,11 +50,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 -<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) { $templateCache.put("template/datepicker/datepicker.html", "<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" + - ""); -}]); diff --git a/public/vendor/flot/jquery.flot.stack.js b/public/vendor/flot/jquery.flot.stack.js index 5367ff30603..267d4e51642 100644 --- a/public/vendor/flot/jquery.flot.stack.js +++ b/public/vendor/flot/jquery.flot.stack.js @@ -72,52 +72,49 @@ charts or filled areas). horizontal = s.bars.horizontal, withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), withsteps = withlines && s.lines.steps, - fromgap = true, keyOffset = horizontal ? 1 : 0, accumulateOffset = horizontal ? 0 : 1, i = 0, j = 0, l, m; while (true) { - // browse all points from the current series and from the previous series if (i >= points.length && j >= otherpoints.length) break; - // newpoints will replace current series with - // as many points as different timestamps we have in the 2 (current & previous) series l = newpoints.length; - px = points[i + keyOffset]; - py = points[i + accumulateOffset]; - qx = otherpoints[j + keyOffset]; - qy = otherpoints[j + accumulateOffset]; - bottom = 0; - if (i < points.length && px == null) { - // let's ignore null points from current series, nothing to do with them - i += ps; - } - else if (j < otherpoints.length && qx == null) { - // let's ignore null points from previous series, nothing to do with them - j += otherps; - } - else if (i >= points.length) { - // no more points in the current series, simply take the remaining points - // from the previous series so that next series will correctly stack - for (m = 0; m < ps; ++m) - newpoints.push(otherpoints[j + m]); - bottom = qy; - j += otherps; - } - else if (j >= otherpoints.length) { - // no more points in the previous series, of course let's take - // the remaining points from the current series + if (i < points.length && points[i] == null) { + // copy gaps for (m = 0; m < ps; ++m) newpoints.push(points[i + m]); i += ps; } + else if (i >= points.length) { + // take the remaining points from the previous series + for (m = 0; m < ps; ++m) + newpoints.push(otherpoints[j + m]); + if (withbottom) + newpoints[l + 2] = otherpoints[j + accumulateOffset]; + j += otherps; + } + else if (j >= otherpoints.length) { + // take the remaining points from the current series + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + i += ps; + } + else if (j < otherpoints.length && otherpoints[j] == null) { + // ignore point + j += otherps; + } else { - // next available points from current and previous series have the same timestamp + // cases where we actually got two points + px = points[i + keyOffset]; + py = points[i + accumulateOffset]; + qx = otherpoints[j + keyOffset]; + qy = otherpoints[j + accumulateOffset]; + bottom = 0; + if (px == qx) { - // so take the point from the current series and skip the previous' one for (m = 0; m < ps; ++m) newpoints.push(points[i + m]); @@ -127,23 +124,27 @@ charts or filled areas). i += ps; j += otherps; } - // next available point with the smallest timestamp is from the previous series else if (px > qx) { - // so take the point from the previous series so that next series will correctly stack - for (m = 0; m < ps; ++m) - newpoints.push(otherpoints[j + m]); - - // we might be able to interpolate - if (i > 0 && points[i - ps] != null) - newpoints[l + accumulateOffset] += py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); - - bottom = qy; + // take the point from the previous series so that next series will correctly stack + if (i == 0) { + for (m = 0; m < ps; ++m) + newpoints.push(otherpoints[j + m]); + bottom = qy; + } + // we got past point below, might need to + // insert interpolated extra point + if (i > 0 && points[i - ps] != null) { + intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); + newpoints.push(qx); + newpoints.push(intery + qy); + for (m = 2; m < ps; ++m) + newpoints.push(points[i + m]); + bottom = qy; + } j += otherps; } - // (px < qx) next available point with the smallest timestamp is from the current series - else { - // so of course let's take the point from the current series + else { // px < qx for (m = 0; m < ps; ++m) newpoints.push(points[i + m]); @@ -156,10 +157,22 @@ charts or filled areas). i += ps; } - } - if (l != newpoints.length && withbottom) - newpoints[l + 2] = bottom; + fromgap = false; + + if (l != newpoints.length && withbottom) + newpoints[l + 2] = bottom; + } + + // maintain the line steps invariant + if (withsteps && l != newpoints.length && l > 0 + && newpoints[l] != null + && newpoints[l] != newpoints[l - ps] + && newpoints[l + 1] != newpoints[l - ps + 1]) { + for (m = 0; m < ps; ++m) + newpoints[l + ps + m] = newpoints[l + m]; + newpoints[l + 1] = newpoints[l - ps + 1]; + } } datapoints.points = newpoints; 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 @@ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <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"> + <base href="[[.AppSubUrl]]/" /> + </head> <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 :( - </span> + </h1> </div> - <div class="gf-box-body"> - <h4>[[.Title]]</h4> - [[.ErrorMsg]] - </div> + <h4>[[.Title]]</h4> + + <pre>[[.ErrorMsg]]</pre> + </div> - - </body> + </body> </html>