Commit 8d808126 by Torkel Ödegaard

Merge branch 'issue-6566' of https://github.com/benrubson/grafana into benrubson-issue-6566

parents 05c5a09f e71114f4
...@@ -5,6 +5,13 @@ ...@@ -5,6 +5,13 @@
* **Graph Panel**: Bar width if bars was only used in series override, [#6528](https://github.com/grafana/grafana/issues/6528) * **Graph Panel**: Bar width if bars was only used in series override, [#6528](https://github.com/grafana/grafana/issues/6528)
* **UI/Browser**: Fixed issue with page/view header gradient border not showing in Safari, [#6530](https://github.com/grafana/grafana/issues/6530) * **UI/Browser**: Fixed issue with page/view header gradient border not showing in Safari, [#6530](https://github.com/grafana/grafana/issues/6530)
* **Cloudwatch**: Fixed cloudwatch datasource requesting to many datapoints, [#6544](https://github.com/grafana/grafana/issues/6544) * **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) # 4.0-beta1 (2016-11-09)
......
...@@ -98,6 +98,6 @@ Amazon S3 for this and Webdav. So to set that up you need to configure the ...@@ -98,6 +98,6 @@ Amazon S3 for this and Webdav. So to set that up you need to configure the
[external image uploader](/installation/configuration/#external-image-storage) in your grafana-server ini [external image uploader](/installation/configuration/#external-image-storage) in your grafana-server ini
config file. config file.
This is not an optional requirement, you can get slack and email notifications without setting this up. This is an optional requirement, you can get slack and email notifications without setting this up.
...@@ -55,7 +55,10 @@ Currently the only condition type that exists is a `Query` condition that allows ...@@ -55,7 +55,10 @@ Currently the only condition type that exists is a `Query` condition that allows
specify a query letter, time range and an aggregation function. The letter refers to specify a query letter, time range and an aggregation function. The letter refers to
a query you already have added in the **Metrics** tab. The result from the query and the aggregation function is a query you already have added in the **Metrics** tab. The result from the query and the aggregation function is
a single value that is then used in the threshold check. The query used in an alert rule cannot a single value that is then used in the threshold check. The query used in an alert rule cannot
contain any template variables. Currently we only support `AND` operator between conditions. contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially.
For example, we have 3 conditions in the following order:
`condition:A(evaluates to: TRUE) OR condition:B(evaluates to: FALSE) AND condition:C(evaluates to: TRUE)`
so the result will be calculated as ((TRUE OR FALSE) AND TRUE) = TRUE.
We plan to add other condition types in the future, like `Other Alert`, where you can include the state We plan to add other condition types in the future, like `Other Alert`, where you can include the state
of another alert in your conditions, and `Time Of Day`. of another alert in your conditions, and `Time Of Day`.
......
...@@ -30,11 +30,5 @@ Even though the data source type name is with lowercase `g`, the directive uses ...@@ -30,11 +30,5 @@ Even though the data source type name is with lowercase `g`, the directive uses
that is how angular directives needs to be named in order to match an element with name `<metric-query-editor-graphite />`. that is how angular directives needs to be named in order to match an element with name `<metric-query-editor-graphite />`.
You also specify the query controller here instead of in the query.editor.html partial like before. You also specify the query controller here instead of in the query.editor.html partial like before.
### query.editor.html
This partial needs to be updated, remove the `np-repeat` this is done in the outer partial now,m the query.editor.html
should only render a single query. Take a look at the Graphite or InfluxDB partials for `query.editor.html` for reference.
You should also add a `tight-form-item` with `{{target.refId}}`, all queries needs to be assigned a letter (`refId`).
These query reference letters are going to be utilized in a later feature.
...@@ -85,6 +85,34 @@ page_keywords: grafana, admin, http, api, documentation, orgs, organisation ...@@ -85,6 +85,34 @@ page_keywords: grafana, admin, http, api, documentation, orgs, organisation
} }
} }
## Create Organisation
`POST /api/org`
**Example Request**:
POST /api/org HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"name":"New Org."
}
**Example Response**:
HTTP/1.1 200
Content-Type: application/json
{
"orgId":"1",
"message":"Organization created"
}
## Update current Organisation ## Update current Organisation
`PUT /api/org` `PUT /api/org`
......
...@@ -141,6 +141,18 @@ those options. ...@@ -141,6 +141,18 @@ those options.
- [OpenTSDB]({{< relref "datasources/opentsdb.md" >}}) - [OpenTSDB]({{< relref "datasources/opentsdb.md" >}})
- [Prometheus]({{< relref "datasources/prometheus.md" >}}) - [Prometheus]({{< relref "datasources/prometheus.md" >}})
### Server side image rendering
Server side image (png) rendering is a feature that is optional but very useful when sharing visualizations,
for example in alert notifications.
If the image is missing text make sure you have font packages installed.
```
yum install fontconfig
yum install freetype*
yum install urw-fonts
```
## Installing from binary tar file ## Installing from binary tar file
......
...@@ -119,7 +119,8 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response { ...@@ -119,7 +119,8 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response {
res := backendCmd.Result res := backendCmd.Result
dtoRes := &dtos.AlertTestResult{ dtoRes := &dtos.AlertTestResult{
Firing: res.Firing, Firing: res.Firing,
ConditionEvals: res.ConditionEvals,
} }
if res.Error != nil { if res.Error != nil {
......
...@@ -307,4 +307,5 @@ func Register(r *macaron.Macaron) { ...@@ -307,4 +307,5 @@ func Register(r *macaron.Macaron) {
InitAppPluginRoutes(r) InitAppPluginRoutes(r)
r.NotFound(NotFoundHandler)
} }
...@@ -35,11 +35,12 @@ type AlertTestCommand struct { ...@@ -35,11 +35,12 @@ type AlertTestCommand struct {
} }
type AlertTestResult struct { type AlertTestResult struct {
Firing bool `json:"firing"` Firing bool `json:"firing"`
TimeMs string `json:"timeMs"` ConditionEvals string `json:"conditionEvals"`
Error string `json:"error,omitempty"` TimeMs string `json:"timeMs"`
EvalMatches []*EvalMatch `json:"matches,omitempty"` Error string `json:"error,omitempty"`
Logs []*AlertTestResultLog `json:"logs,omitempty"` EvalMatches []*EvalMatch `json:"matches,omitempty"`
Logs []*AlertTestResultLog `json:"logs,omitempty"`
} }
type AlertTestResultLog struct { type AlertTestResultLog struct {
......
...@@ -96,7 +96,7 @@ func OAuthLogin(ctx *middleware.Context) { ...@@ -96,7 +96,7 @@ func OAuthLogin(ctx *middleware.Context) {
} }
sslcli := &http.Client{Transport: tr} sslcli := &http.Client{Transport: tr}
oauthCtx = context.TODO() oauthCtx = context.Background()
oauthCtx = context.WithValue(oauthCtx, oauth2.HTTPClient, sslcli) oauthCtx = context.WithValue(oauthCtx, oauth2.HTTPClient, sslcli)
} }
...@@ -106,6 +106,8 @@ func OAuthLogin(ctx *middleware.Context) { ...@@ -106,6 +106,8 @@ func OAuthLogin(ctx *middleware.Context) {
ctx.Handle(500, "login.OAuthLogin(NewTransportWithCode)", err) ctx.Handle(500, "login.OAuthLogin(NewTransportWithCode)", err)
return return
} }
// token.TokenType was defaulting to "bearer", which is out of spec, so we explicitly set to "Bearer"
token.TokenType = "Bearer"
ctx.Logger.Debug("OAuthLogin Got token") ctx.Logger.Debug("OAuthLogin Got token")
......
...@@ -187,6 +187,7 @@ func (ctx *Context) Handle(status int, title string, err error) { ...@@ -187,6 +187,7 @@ func (ctx *Context) Handle(status int, title string, err error) {
} }
ctx.Data["Title"] = title ctx.Data["Title"] = title
ctx.Data["AppSubUrl"] = setting.AppSubUrl
ctx.HTML(status, strconv.Itoa(status)) ctx.HTML(status, strconv.Itoa(status))
} }
......
...@@ -19,53 +19,14 @@ import ( ...@@ -19,53 +19,14 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http"
"runtime" "runtime"
"gopkg.in/macaron.v1" "gopkg.in/macaron.v1"
"github.com/go-macaron/inject"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
const (
panicHtml = `<html>
<head><title>PANIC: %s</title>
<meta charset="utf-8" />
<style type="text/css">
html, body {
font-family: "Roboto", sans-serif;
color: #333333;
background-color: #ea5343;
margin: 0px;
}
h1 {
color: #d04526;
background-color: #ffffff;
padding: 20px;
border-bottom: 1px dashed #2b3848;
}
pre {
margin: 20px;
padding: 20px;
border: 2px solid #2b3848;
background-color: #ffffff;
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
}
</style>
</head><body>
<h1>PANIC</h1>
<pre style="font-weight: bold;">%s</pre>
<pre>%s</pre>
</body>
</html>`
)
var ( var (
dunno = []byte("???") dunno = []byte("???")
centerDot = []byte("·") centerDot = []byte("·")
...@@ -151,21 +112,34 @@ func Recovery() macaron.Handler { ...@@ -151,21 +112,34 @@ func Recovery() macaron.Handler {
panicLogger.Error("Request error", "error", err, "stack", string(stack)) panicLogger.Error("Request error", "error", err, "stack", string(stack))
// Lookup the current responsewriter c.Data["Title"] = "Server Error"
val := c.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil))) c.Data["AppSubUrl"] = setting.AppSubUrl
res := val.Interface().(http.ResponseWriter)
// respond with panic message while in development mode if theErr, ok := err.(error); ok {
var body []byte c.Data["Title"] = theErr.Error()
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 setting.Env == setting.DEV {
if nil != body { c.Data["ErrorMsg"] = string(stack)
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)
// }
} }
}() }()
......
...@@ -23,6 +23,7 @@ type QueryCondition struct { ...@@ -23,6 +23,7 @@ type QueryCondition struct {
Query AlertQuery Query AlertQuery
Reducer QueryReducer Reducer QueryReducer
Evaluator AlertEvaluator Evaluator AlertEvaluator
Operator string
HandleRequest tsdb.HandleRequestFunc HandleRequest tsdb.HandleRequestFunc
} }
...@@ -72,6 +73,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio ...@@ -72,6 +73,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
return &alerting.ConditionResult{ return &alerting.ConditionResult{
Firing: evalMatchCount > 0, Firing: evalMatchCount > 0,
NoDataFound: emptySerieCount == len(seriesList), NoDataFound: emptySerieCount == len(seriesList),
Operator: c.Operator,
EvalMatches: matches, EvalMatches: matches,
}, nil }, nil
} }
...@@ -168,8 +170,12 @@ func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, erro ...@@ -168,8 +170,12 @@ func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
condition.Evaluator = evaluator condition.Evaluator = evaluator
operatorJson := model.Get("operator")
operator := operatorJson.Get("type").MustString("and")
condition.Operator = operator
return &condition, nil return &condition, nil
} }
......
...@@ -17,7 +17,7 @@ type EvalContext struct { ...@@ -17,7 +17,7 @@ type EvalContext struct {
EvalMatches []*EvalMatch EvalMatches []*EvalMatch
Logs []*ResultLogEntry Logs []*ResultLogEntry
Error error Error error
Description string ConditionEvals string
StartTime time.Time StartTime time.Time
EndTime time.Time EndTime time.Time
Rule *Rule Rule *Rule
......
package alerting package alerting
import ( import (
"strconv"
"strings"
"time" "time"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
...@@ -21,7 +23,10 @@ func NewEvalHandler() *DefaultEvalHandler { ...@@ -21,7 +23,10 @@ func NewEvalHandler() *DefaultEvalHandler {
func (e *DefaultEvalHandler) Eval(context *EvalContext) { func (e *DefaultEvalHandler) Eval(context *EvalContext) {
firing := true firing := true
for _, condition := range context.Rule.Conditions { conditionEvals := ""
for i := 0; i < len(context.Rule.Conditions); i++ {
condition := context.Rule.Conditions[i]
cr, err := condition.Eval(context) cr, err := condition.Eval(context)
if err != nil { if err != nil {
context.Error = err context.Error = err
...@@ -32,15 +37,23 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) { ...@@ -32,15 +37,23 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
break break
} }
// break if result has not triggered yet // calculating Firing based on operator
if cr.Firing == false { if cr.Operator == "or" {
firing = false firing = firing || cr.Firing
break } else {
firing = firing && cr.Firing
}
if i > 0 {
conditionEvals = "[" + conditionEvals + " " + strings.ToUpper(cr.Operator) + " " + strconv.FormatBool(cr.Firing) + "]"
} else {
conditionEvals = strconv.FormatBool(firing)
} }
context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...) context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...)
} }
context.ConditionEvals = conditionEvals + " = " + strconv.FormatBool(firing)
context.Firing = firing context.Firing = firing
context.EndTime = time.Now() context.EndTime = time.Now()
elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond
......
...@@ -8,12 +8,13 @@ import ( ...@@ -8,12 +8,13 @@ import (
) )
type conditionStub struct { type conditionStub struct {
firing bool firing bool
matches []*EvalMatch operator string
matches []*EvalMatch
} }
func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) { func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) {
return &ConditionResult{Firing: c.firing, EvalMatches: c.matches}, nil return &ConditionResult{Firing: c.firing, EvalMatches: c.matches, Operator: c.operator}, nil
} }
func TestAlertingExecutor(t *testing.T) { func TestAlertingExecutor(t *testing.T) {
...@@ -29,18 +30,102 @@ func TestAlertingExecutor(t *testing.T) { ...@@ -29,18 +30,102 @@ func TestAlertingExecutor(t *testing.T) {
handler.Eval(context) handler.Eval(context)
So(context.Firing, ShouldEqual, true) So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "true = true")
}) })
Convey("Show return false with not passing asdf", func() { Convey("Show return false with not passing asdf", func() {
context := NewEvalContext(context.TODO(), &Rule{ context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{ Conditions: []Condition{
&conditionStub{firing: true, matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}}, &conditionStub{firing: true, operator: "and", matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}},
&conditionStub{firing: false}, &conditionStub{firing: false, operator: "and"},
}, },
}) })
handler.Eval(context) handler.Eval(context)
So(context.Firing, ShouldEqual, false) So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[true AND false] = false")
})
Convey("Show return true if any of the condition is passing with OR operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "[true OR false] = true")
})
Convey("Show return false if any of the condition is failing with AND operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "and"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[true AND false] = false")
})
Convey("Show return true if one condition is failing with nested OR operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "[[true AND true] OR false] = true")
})
Convey("Show return false if one condition is passing with nested OR operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[[true AND false] OR false] = false")
})
Convey("Show return false if a condition is failing with nested AND operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "and"},
&conditionStub{firing: true, operator: "and"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[[true AND false] AND true] = false")
})
Convey("Show return true if a condition is passing with nested OR operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "or"},
&conditionStub{firing: true, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true")
}) })
}) })
} }
...@@ -24,6 +24,7 @@ type Notifier interface { ...@@ -24,6 +24,7 @@ type Notifier interface {
type ConditionResult struct { type ConditionResult struct {
Firing bool Firing bool
NoDataFound bool NoDataFound bool
Operator string
EvalMatches []*EvalMatch EvalMatches []*EvalMatch
} }
......
...@@ -26,11 +26,32 @@ type Rule struct { ...@@ -26,11 +26,32 @@ type Rule struct {
} }
type ValidationError struct { type ValidationError struct {
Reason string Reason string
Err error
Alertid int64
DashboardId int64
PanelId int64
} }
func (e ValidationError) Error() string { func (e ValidationError) Error() string {
return e.Reason extraInfo := ""
if e.Alertid != 0 {
extraInfo = fmt.Sprintf("%s AlertId: %v", extraInfo, e.Alertid)
}
if e.PanelId != 0 {
extraInfo = fmt.Sprintf("%s PanelId: %v ", extraInfo, e.PanelId)
}
if e.DashboardId != 0 {
extraInfo = fmt.Sprintf("%s DashboardId: %v", extraInfo, e.DashboardId)
}
if e.Err != nil {
return fmt.Sprintf("%s %s%s", e.Err.Error(), e.Reason, extraInfo)
}
return fmt.Sprintf("Failed to extract alert.Reason: %s %s", e.Reason, extraInfo)
} }
var ( var (
...@@ -83,7 +104,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { ...@@ -83,7 +104,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
for _, v := range ruleDef.Settings.Get("notifications").MustArray() { for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
jsonModel := simplejson.NewFromAny(v) jsonModel := simplejson.NewFromAny(v)
if id, err := jsonModel.Get("id").Int64(); err != nil { if id, err := jsonModel.Get("id").Int64(); err != nil {
return nil, ValidationError{Reason: "Invalid notification schema"} return nil, ValidationError{Reason: "Invalid notification schema", DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
} else { } else {
model.Notifications = append(model.Notifications, id) model.Notifications = append(model.Notifications, id)
} }
...@@ -93,10 +114,10 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { ...@@ -93,10 +114,10 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
conditionModel := simplejson.NewFromAny(condition) conditionModel := simplejson.NewFromAny(condition)
conditionType := conditionModel.Get("type").MustString() conditionType := conditionModel.Get("type").MustString()
if factory, exist := conditionFactories[conditionType]; !exist { if factory, exist := conditionFactories[conditionType]; !exist {
return nil, ValidationError{Reason: "Unknown alert condition: " + conditionType} return nil, ValidationError{Reason: "Unknown alert condition: " + conditionType, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
} else { } else {
if queryCondition, err := factory(conditionModel, index); err != nil { if queryCondition, err := factory(conditionModel, index); err != nil {
return nil, err return nil, ValidationError{Err: err, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
} else { } else {
model.Conditions = append(model.Conditions, queryCondition) model.Conditions = append(model.Conditions, queryCondition)
} }
......
...@@ -39,6 +39,9 @@ func (s *SchedulerImpl) Update(rules []*Rule) { ...@@ -39,6 +39,9 @@ func (s *SchedulerImpl) Update(rules []*Rule) {
offset := ((rule.Frequency * 1000) / int64(len(rules))) * int64(i) offset := ((rule.Frequency * 1000) / int64(len(rules))) * int64(i)
job.Offset = int64(math.Floor(float64(offset) / 1000)) job.Offset = int64(math.Floor(float64(offset) / 1000))
if job.Offset == 0 { //zero offset causes division with 0 panics.
job.Offset = 1
}
jobs[rule.Id] = job jobs[rule.Id] = job
} }
......
...@@ -2,7 +2,6 @@ package influxdb ...@@ -2,7 +2,6 @@ package influxdb
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
"regexp" "regexp"
...@@ -58,13 +57,12 @@ func (query *Query) renderTags() []string { ...@@ -58,13 +57,12 @@ func (query *Query) renderTags() []string {
} }
textValue := "" textValue := ""
numericValue, err := strconv.ParseFloat(tag.Value, 64)
// quote value unless regex or number // quote value unless regex or number
if tag.Operator == "=~" || tag.Operator == "!~" { if tag.Operator == "=~" || tag.Operator == "!~" {
textValue = tag.Value textValue = tag.Value
} else if err == nil { } else if tag.Operator == "<" || tag.Operator == ">" {
textValue = fmt.Sprintf("%v", numericValue) textValue = tag.Value
} else { } else {
textValue = fmt.Sprintf("'%s'", tag.Value) textValue = fmt.Sprintf("'%s'", tag.Value)
} }
......
...@@ -106,13 +106,19 @@ func TestInfluxdbQueryBuilder(t *testing.T) { ...@@ -106,13 +106,19 @@ func TestInfluxdbQueryBuilder(t *testing.T) {
Convey("can render number tags", func() { Convey("can render number tags", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001", Key: "key"}}} query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001", Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = 10001`) So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = '10001'`)
}) })
Convey("can render number tags with decimals", func() { Convey("can render numbers less then condition tags", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001.1", Key: "key"}}} 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() { Convey("can render string tags", func() {
......
...@@ -40,7 +40,6 @@ export class GrafanaApp { ...@@ -40,7 +40,6 @@ export class GrafanaApp {
init() { init() {
var app = angular.module('grafana', []); var app = angular.module('grafana', []);
app.constant('grafanaVersion', "@grafanaVersion@");
moment.locale(config.bootData.user.locale); moment.locale(config.bootData.user.locale);
......
...@@ -147,9 +147,14 @@ export function grafanaAppDirective(playlistSrv, contextSrv) { ...@@ -147,9 +147,14 @@ export function grafanaAppDirective(playlistSrv, contextSrv) {
} }
} }
// mouse and keyboard is user activity
body.mousemove(userActivityDetected); body.mousemove(userActivityDetected);
body.keydown(userActivityDetected); body.keydown(userActivityDetected);
setInterval(checkForInActiveUser, 1000); // treat tab change as activity
document.addEventListener('visibilitychange', userActivityDetected);
// check every 2 seconds
setInterval(checkForInActiveUser, 2000);
appEvents.on('toggle-view-mode', () => { appEvents.on('toggle-view-mode', () => {
lastActivity = 0; lastActivity = 0;
......
...@@ -6,7 +6,6 @@ import "./directives/dash_class"; ...@@ -6,7 +6,6 @@ import "./directives/dash_class";
import "./directives/confirm_click"; import "./directives/confirm_click";
import "./directives/dash_edit_link"; import "./directives/dash_edit_link";
import "./directives/dropdown_typeahead"; import "./directives/dropdown_typeahead";
import "./directives/grafana_version_check";
import "./directives/metric_segment"; import "./directives/metric_segment";
import "./directives/misc"; import "./directives/misc";
import "./directives/ng_model_on_blur"; import "./directives/ng_model_on_blur";
......
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>');
}
});
}
};
});
});
...@@ -47,7 +47,7 @@ function (coreModule, kbn, rangeUtil) { ...@@ -47,7 +47,7 @@ function (coreModule, kbn, rangeUtil) {
if (ctrl.$isEmpty(modelValue)) { if (ctrl.$isEmpty(modelValue)) {
return true; return true;
} }
if (viewValue.indexOf('$') === 0) { if (viewValue.indexOf('$') === 0 || viewValue.indexOf('+$') === 0) {
return true; // allow template variable return true; // allow template variable
} }
var info = rangeUtil.describeTextRange(viewValue); var info = rangeUtil.describeTextRange(viewValue);
......
...@@ -420,11 +420,11 @@ function($, _, moment) { ...@@ -420,11 +420,11 @@ function($, _, moment) {
kbn.valueFormats.bps = kbn.formatBuilders.decimalSIPrefix('bps'); kbn.valueFormats.bps = kbn.formatBuilders.decimalSIPrefix('bps');
kbn.valueFormats.Bps = kbn.formatBuilders.decimalSIPrefix('Bps'); kbn.valueFormats.Bps = kbn.formatBuilders.decimalSIPrefix('Bps');
kbn.valueFormats.KBs = kbn.formatBuilders.decimalSIPrefix('Bs', 1); kbn.valueFormats.KBs = kbn.formatBuilders.decimalSIPrefix('Bs', 1);
kbn.valueFormats.Kbits = kbn.formatBuilders.decimalSIPrefix('bits', 1); kbn.valueFormats.Kbits = kbn.formatBuilders.decimalSIPrefix('bps', 1);
kbn.valueFormats.MBs = kbn.formatBuilders.decimalSIPrefix('Bs', 2); kbn.valueFormats.MBs = kbn.formatBuilders.decimalSIPrefix('Bs', 2);
kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bits', 2); kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bps', 2);
kbn.valueFormats.GBs = kbn.formatBuilders.decimalSIPrefix('Bs', 3); kbn.valueFormats.GBs = kbn.formatBuilders.decimalSIPrefix('Bs', 3);
kbn.valueFormats.Gbits = kbn.formatBuilders.decimalSIPrefix('bits', 3); kbn.valueFormats.Gbits = kbn.formatBuilders.decimalSIPrefix('bps', 3);
// Throughput // Throughput
kbn.valueFormats.ops = kbn.formatBuilders.simpleCountUnit('ops'); kbn.valueFormats.ops = kbn.formatBuilders.simpleCountUnit('ops');
......
...@@ -28,6 +28,11 @@ var evalFunctions = [ ...@@ -28,6 +28,11 @@ var evalFunctions = [
{text: 'HAS NO VALUE' , value: 'no_value'} {text: 'HAS NO VALUE' , value: 'no_value'}
]; ];
var evalOperators = [
{text: 'OR', value: 'or'},
{text: 'AND', value: 'and'},
];
var reducerTypes = [ var reducerTypes = [
{text: 'avg()', value: 'avg'}, {text: 'avg()', value: 'avg'},
{text: 'min()', value: 'min'}, {text: 'min()', value: 'min'},
...@@ -116,6 +121,7 @@ export default { ...@@ -116,6 +121,7 @@ export default {
getStateDisplayModel: getStateDisplayModel, getStateDisplayModel: getStateDisplayModel,
conditionTypes: conditionTypes, conditionTypes: conditionTypes,
evalFunctions: evalFunctions, evalFunctions: evalFunctions,
evalOperators: evalOperators,
noDataModes: noDataModes, noDataModes: noDataModes,
executionErrorModes: executionErrorModes, executionErrorModes: executionErrorModes,
reducerTypes: reducerTypes, reducerTypes: reducerTypes,
......
...@@ -18,6 +18,7 @@ export class AlertTabCtrl { ...@@ -18,6 +18,7 @@ export class AlertTabCtrl {
alert: any; alert: any;
conditionModels: any; conditionModels: any;
evalFunctions: any; evalFunctions: any;
evalOperators: any;
noDataModes: any; noDataModes: any;
executionErrorModes: any; executionErrorModes: any;
addNotificationSegment; addNotificationSegment;
...@@ -41,6 +42,7 @@ export class AlertTabCtrl { ...@@ -41,6 +42,7 @@ export class AlertTabCtrl {
this.$scope.ctrl = this; this.$scope.ctrl = this;
this.subTabIndex = 0; this.subTabIndex = 0;
this.evalFunctions = alertDef.evalFunctions; this.evalFunctions = alertDef.evalFunctions;
this.evalOperators = alertDef.evalOperators;
this.conditionTypes = alertDef.conditionTypes; this.conditionTypes = alertDef.conditionTypes;
this.noDataModes = alertDef.noDataModes; this.noDataModes = alertDef.noDataModes;
this.executionErrorModes = alertDef.executionErrorModes; this.executionErrorModes = alertDef.executionErrorModes;
...@@ -194,6 +196,7 @@ export class AlertTabCtrl { ...@@ -194,6 +196,7 @@ export class AlertTabCtrl {
query: {params: ['A', '5m', 'now']}, query: {params: ['A', '5m', 'now']},
reducer: {type: 'avg', params: []}, reducer: {type: 'avg', params: []},
evaluator: {type: 'gt', params: [null]}, evaluator: {type: 'gt', params: [null]},
operator: {type: 'and'},
}; };
} }
...@@ -250,6 +253,7 @@ export class AlertTabCtrl { ...@@ -250,6 +253,7 @@ export class AlertTabCtrl {
cm.queryPart = new QueryPart(source.query, alertDef.alertQueryDef); cm.queryPart = new QueryPart(source.query, alertDef.alertQueryDef);
cm.reducerPart = alertDef.createReducerPart(source.reducer); cm.reducerPart = alertDef.createReducerPart(source.reducer);
cm.evaluator = source.evaluator; cm.evaluator = source.evaluator;
cm.operator = source.operator;
return cm; return cm;
} }
......
...@@ -38,23 +38,23 @@ ...@@ -38,23 +38,23 @@
<h5 class="section-heading">Conditions</h5> <h5 class="section-heading">Conditions</h5>
<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels"> <div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-label query-keyword width-5" ng-if="$index">AND</span> <metric-segment-model css-class="query-keyword width-5" ng-if="$index" property="conditionModel.operator.type" options="ctrl.evalOperators" custom="false"></metric-segment-model>
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span> <span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<query-part-editor class="gf-form-label query-part" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)"> <query-part-editor class="gf-form-label query-part width-5" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
</query-part-editor> </query-part-editor>
<span class="gf-form-label query-keyword">OF</span> <span class="gf-form-label query-keyword">OF</span>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)"> <query-part-editor class="gf-form-label query-part width-10" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
</query-part-editor> </query-part-editor>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model> <metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
<input class="gf-form-input max-width-7" type="number" step="any" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input> <input class="gf-form-input max-width-9" type="number" step="any" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input>
<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label> <label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
<input class="gf-form-input max-width-7" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input> <input class="gf-form-input max-width-9" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label"> <label class="gf-form-label">
......
...@@ -52,7 +52,7 @@ export class DashboardCtrl { ...@@ -52,7 +52,7 @@ export class DashboardCtrl {
.catch($scope.onInitFailed.bind(this, 'Templating init failed', false)) .catch($scope.onInitFailed.bind(this, 'Templating init failed', false))
// continue // continue
.finally(function() { .finally(function() {
dynamicDashboardSrv.init(dashboard, variableSrv); dynamicDashboardSrv.init(dashboard);
dynamicDashboardSrv.process(); dynamicDashboardSrv.process();
unsavedChangesSrv.init(dashboard, $scope); unsavedChangesSrv.init(dashboard, $scope);
......
...@@ -12,12 +12,12 @@ export class DynamicDashboardSrv { ...@@ -12,12 +12,12 @@ export class DynamicDashboardSrv {
dashboard: any; dashboard: any;
variables: any; variables: any;
init(dashboard, variableSrv) { init(dashboard) {
this.dashboard = dashboard; this.dashboard = dashboard;
this.variables = variableSrv.variables; this.variables = dashboard.templating.list;
} }
process(options) { process(options?) {
if (this.dashboard.snapshot || this.variables.length === 0) { if (this.dashboard.snapshot || this.variables.length === 0) {
return; return;
} }
...@@ -31,6 +31,8 @@ export class DynamicDashboardSrv { ...@@ -31,6 +31,8 @@ export class DynamicDashboardSrv {
// cleanup scopedVars // cleanup scopedVars
for (i = 0; i < this.dashboard.rows.length; i++) { for (i = 0; i < this.dashboard.rows.length; i++) {
row = this.dashboard.rows[i]; row = this.dashboard.rows[i];
delete row.scopedVars;
for (j = 0; j < row.panels.length; j++) { for (j = 0; j < row.panels.length; j++) {
delete row.panels[j].scopedVars; delete row.panels[j].scopedVars;
} }
...@@ -64,6 +66,8 @@ export class DynamicDashboardSrv { ...@@ -64,6 +66,8 @@ export class DynamicDashboardSrv {
j = j - 1; j = j - 1;
} }
} }
row.panelSpanChanged();
} }
} }
......
...@@ -17,9 +17,7 @@ export class DashExportCtrl { ...@@ -17,9 +17,7 @@ export class DashExportCtrl {
constructor(private backendSrv, dashboardSrv, datasourceSrv, $scope) { constructor(private backendSrv, dashboardSrv, datasourceSrv, $scope) {
this.exporter = new DashboardExporter(datasourceSrv); this.exporter = new DashboardExporter(datasourceSrv);
var current = dashboardSrv.getCurrent().getSaveModelClone(); this.exporter.makeExportable(dashboardSrv.getCurrent()).then(dash => {
this.exporter.makeExportable(current).then(dash => {
$scope.$apply(() => { $scope.$apply(() => {
this.dash = dash; this.dash = dash;
}); });
......
...@@ -11,19 +11,40 @@ export class DashboardExporter { ...@@ -11,19 +11,40 @@ export class DashboardExporter {
constructor(private datasourceSrv) { constructor(private datasourceSrv) {
} }
makeExportable(dash) { makeExportable(dashboard) {
var dynSrv = new DynamicDashboardSrv(); var dynSrv = new DynamicDashboardSrv();
dynSrv.init(dash, {variables: dash.templating.list});
// clean up repeated rows and panels,
// this is done on the live real dashboard instance, not on a clone
// so we need to undo this
// this is pretty hacky and needs to be changed
dynSrv.init(dashboard);
dynSrv.process({cleanUpOnly: true}); dynSrv.process({cleanUpOnly: true});
dash.id = null; var saveModel = dashboard.getSaveModelClone();
saveModel.id = null;
// undo repeat cleanup
dynSrv.process();
var inputs = []; var inputs = [];
var requires = {}; var requires = {};
var datasources = {}; var datasources = {};
var promises = []; var promises = [];
var variableLookup: any = {};
for (let variable of saveModel.templating.list) {
variableLookup[variable.name] = variable;
}
var templateizeDatasourceUsage = obj => { var templateizeDatasourceUsage = obj => {
// ignore data source properties that contain a variable
if (obj.datasource && obj.datasource.indexOf('$') === 0) {
if (variableLookup[obj.datasource.substring(1)]){
return;
}
}
promises.push(this.datasourceSrv.get(obj.datasource).then(ds => { promises.push(this.datasourceSrv.get(obj.datasource).then(ds => {
if (ds.meta.builtIn) { if (ds.meta.builtIn) {
return; return;
...@@ -50,7 +71,7 @@ export class DashboardExporter { ...@@ -50,7 +71,7 @@ export class DashboardExporter {
}; };
// check up panel data sources // check up panel data sources
for (let row of dash.rows) { for (let row of saveModel.rows) {
for (let panel of row.panels) { for (let panel of row.panels) {
if (panel.datasource !== undefined) { if (panel.datasource !== undefined) {
templateizeDatasourceUsage(panel); templateizeDatasourceUsage(panel);
...@@ -77,7 +98,7 @@ export class DashboardExporter { ...@@ -77,7 +98,7 @@ export class DashboardExporter {
} }
// templatize template vars // templatize template vars
for (let variable of dash.templating.list) { for (let variable of saveModel.templating.list) {
if (variable.type === 'query') { if (variable.type === 'query') {
templateizeDatasourceUsage(variable); templateizeDatasourceUsage(variable);
variable.options = []; variable.options = [];
...@@ -87,7 +108,7 @@ export class DashboardExporter { ...@@ -87,7 +108,7 @@ export class DashboardExporter {
} }
// templatize annotations vars // templatize annotations vars
for (let annotationDef of dash.annotations.list) { for (let annotationDef of saveModel.annotations.list) {
templateizeDatasourceUsage(annotationDef); templateizeDatasourceUsage(annotationDef);
} }
...@@ -105,7 +126,7 @@ export class DashboardExporter { ...@@ -105,7 +126,7 @@ export class DashboardExporter {
}); });
// templatize constants // templatize constants
for (let variable of dash.templating.list) { for (let variable of saveModel.templating.list) {
if (variable.type === 'constant') { if (variable.type === 'constant') {
var refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase(); var refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase();
inputs.push({ inputs.push({
...@@ -133,7 +154,7 @@ export class DashboardExporter { ...@@ -133,7 +154,7 @@ export class DashboardExporter {
newObj["__inputs"] = inputs; newObj["__inputs"] = inputs;
newObj["__requires"] = requires; newObj["__requires"] = requires;
_.defaults(newObj, dash); _.defaults(newObj, saveModel);
return newObj; return newObj;
}).catch(err => { }).catch(err => {
......
...@@ -98,12 +98,14 @@ export class DashboardModel { ...@@ -98,12 +98,14 @@ export class DashboardModel {
var events = this.events; var events = this.events;
var meta = this.meta; var meta = this.meta;
var rows = this.rows; var rows = this.rows;
var variables = this.templating.list;
delete this.events; delete this.events;
delete this.meta; delete this.meta;
// prepare save model // prepare save model
this.rows = _.map(this.rows, row => row.getSaveModel()); this.rows = _.map(rows, row => row.getSaveModel());
events.emit('prepare-save-model'); this.templating.list = _.map(variables, variable => variable.getSaveModel ? variable.getSaveModel() : variable);
var copy = $.extend(true, {}, this); var copy = $.extend(true, {}, this);
...@@ -111,6 +113,8 @@ export class DashboardModel { ...@@ -111,6 +113,8 @@ export class DashboardModel {
this.events = events; this.events = events;
this.meta = meta; this.meta = meta;
this.rows = rows; this.rows = rows;
this.templating.list = variables;
return copy; return copy;
} }
...@@ -233,7 +237,6 @@ export class DashboardModel { ...@@ -233,7 +237,6 @@ export class DashboardModel {
} }
duplicatePanel(panel, row) { duplicatePanel(panel, row) {
var rowIndex = _.indexOf(this.rows, row);
var newPanel = angular.copy(panel); var newPanel = angular.copy(panel);
newPanel.id = this.getNextPanelId(); newPanel.id = this.getNextPanelId();
...@@ -241,9 +244,9 @@ export class DashboardModel { ...@@ -241,9 +244,9 @@ export class DashboardModel {
delete newPanel.repeatIteration; delete newPanel.repeatIteration;
delete newPanel.repeatPanelId; delete newPanel.repeatPanelId;
delete newPanel.scopedVars; delete newPanel.scopedVars;
delete newPanel.alert;
var currentRow = this.rows[rowIndex]; row.addPanel(newPanel);
currentRow.panels.push(newPanel);
return newPanel; return newPanel;
} }
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
<div class="gf-form-inline dash-row-add-panel-form"> <div class="gf-form-inline dash-row-add-panel-form">
<div class="gf-form"> <div class="gf-form">
<input type="text" class="gf-form-input max-width-14" ng-model='ctrl.panelSearch' give-focus='true' ng-keydown="ctrl.keyDown($event)" ng-change="ctrl.panelSearchChanged()" placeholder="panel search filter" ng-blur="ctrl.panelSearchBlur()"></input> <input type="text" class="gf-form-input max-width-14" ng-model='ctrl.panelSearch' give-focus='true' ng-keydown="ctrl.keyDown($event)" ng-change="ctrl.panelSearchChanged()" placeholder="panel search filter"></input>
</div> </div>
</div> </div>
......
...@@ -45,12 +45,6 @@ export class AddPanelCtrl { ...@@ -45,12 +45,6 @@ export class AddPanelCtrl {
} }
} }
panelSearchBlur() {
// this.$timeout(() => {
// this.rowCtrl.dropView = 0;
// }, 400);
}
moveSelection(direction) { moveSelection(direction) {
var max = this.panelHits.length; var max = this.panelHits.length;
var newIndex = this.activeIndex + direction; var newIndex = this.activeIndex + direction;
......
...@@ -19,7 +19,6 @@ export class DashRowCtrl { ...@@ -19,7 +19,6 @@ export class DashRowCtrl {
if (this.row.isNew) { if (this.row.isNew) {
this.dropView = 1; this.dropView = 1;
delete this.row.isNew;
} }
} }
...@@ -35,8 +34,8 @@ export class DashRowCtrl { ...@@ -35,8 +34,8 @@ export class DashRowCtrl {
title: config.new_panel_title, title: config.new_panel_title,
type: panelId, type: panelId,
id: this.dashboard.getNextPanelId(), id: this.dashboard.getNextPanelId(),
isNew: true,
}, },
isNew: true,
}; };
} else { } else {
dragObject = this.dashboard.getPanelInfoById(panelId); dragObject = this.dashboard.getPanelInfoById(panelId);
...@@ -65,7 +64,7 @@ export class DashRowCtrl { ...@@ -65,7 +64,7 @@ export class DashRowCtrl {
this.row.panels.push(dragObject.panel); this.row.panels.push(dragObject.panel);
// if not new remove from source row // if not new remove from source row
if (!dragObject.isNew) { if (!dragObject.panel.isNew) {
dragObject.row.removePanel(dragObject.panel, false); dragObject.row.removePanel(dragObject.panel, false);
} }
} }
......
...@@ -33,7 +33,11 @@ export class DashboardRow { ...@@ -33,7 +33,11 @@ export class DashboardRow {
} }
getSaveModel() { getSaveModel() {
this.model = {};
assignModelProperties(this.model, this, this.defaults); assignModelProperties(this.model, this, this.defaults);
// remove properties that dont server persisted purpose
delete this.model.isNew;
return this.model; return this.model;
} }
......
...@@ -62,7 +62,9 @@ describe('dashboardSrv', function() { ...@@ -62,7 +62,9 @@ describe('dashboardSrv', function() {
it('duplicate panel should try to add it to same row', function() { it('duplicate panel should try to add it to same row', function() {
var panel = { span: 4, attr: '123', id: 10 }; var panel = { span: 4, attr: '123', id: 10 };
dashboard.rows = [{ panels: [panel] }];
dashboard.addEmptyRow();
dashboard.rows[0].addPanel(panel);
dashboard.duplicatePanel(panel, dashboard.rows[0]); dashboard.duplicatePanel(panel, dashboard.rows[0]);
expect(dashboard.rows[0].panels[0].span).to.be(4); expect(dashboard.rows[0].panels[0].span).to.be(4);
...@@ -73,7 +75,9 @@ describe('dashboardSrv', function() { ...@@ -73,7 +75,9 @@ describe('dashboardSrv', function() {
it('duplicate panel should remove repeat data', function() { it('duplicate panel should remove repeat data', function() {
var panel = { span: 4, attr: '123', id: 10, repeat: 'asd', scopedVars: { test: 'asd' }}; var panel = { span: 4, attr: '123', id: 10, repeat: 'asd', scopedVars: { test: 'asd' }};
dashboard.rows = [{ panels: [panel] }];
dashboard.addEmptyRow();
dashboard.rows[0].addPanel(panel);
dashboard.duplicatePanel(panel, dashboard.rows[0]); dashboard.duplicatePanel(panel, dashboard.rows[0]);
expect(dashboard.rows[0].panels[1].repeat).to.be(undefined); expect(dashboard.rows[0].panels[1].repeat).to.be(undefined);
......
...@@ -20,7 +20,6 @@ function dynamicDashScenario(desc, func) { ...@@ -20,7 +20,6 @@ function dynamicDashScenario(desc, func) {
beforeEach(angularMocks.inject(function(dashboardSrv) { beforeEach(angularMocks.inject(function(dashboardSrv) {
ctx.dashboardSrv = dashboardSrv; ctx.dashboardSrv = dashboardSrv;
ctx.variableSrv = {};
var model = { var model = {
rows: [], rows: [],
...@@ -29,9 +28,8 @@ function dynamicDashScenario(desc, func) { ...@@ -29,9 +28,8 @@ function dynamicDashScenario(desc, func) {
setupFunc(model); setupFunc(model);
ctx.dash = ctx.dashboardSrv.create(model); ctx.dash = ctx.dashboardSrv.create(model);
ctx.variableSrv.variables = ctx.dash.templating.list;
ctx.dynamicDashboardSrv = new DynamicDashboardSrv(); ctx.dynamicDashboardSrv = new DynamicDashboardSrv();
ctx.dynamicDashboardSrv.init(ctx.dash, ctx.variableSrv); ctx.dynamicDashboardSrv.init(ctx.dash);
ctx.dynamicDashboardSrv.process(); ctx.dynamicDashboardSrv.process();
ctx.rows = ctx.dash.rows; ctx.rows = ctx.dash.rows;
})); }));
......
...@@ -34,6 +34,14 @@ describe('given dashboard with repeated panels', function() { ...@@ -34,6 +34,14 @@ describe('given dashboard with repeated panels', function() {
options: [] options: []
}); });
dash.templating.list.push({
name: 'ds',
type: 'datasource',
query: 'testdb',
current: {value: 'prod', text: 'prod'},
options: []
});
dash.annotations.list.push({ dash.annotations.list.push({
name: 'logs', name: 'logs',
datasource: 'gfdb', datasource: 'gfdb',
...@@ -49,6 +57,7 @@ describe('given dashboard with repeated panels', function() { ...@@ -49,6 +57,7 @@ describe('given dashboard with repeated panels', function() {
datasource: '-- Mixed --', datasource: '-- Mixed --',
targets: [{datasource: 'other'}], targets: [{datasource: 'other'}],
}, },
{id: 5, datasource: '$ds'},
] ]
}); });
...@@ -87,7 +96,7 @@ describe('given dashboard with repeated panels', function() { ...@@ -87,7 +96,7 @@ describe('given dashboard with repeated panels', function() {
}); });
it('exported dashboard should not contain repeated panels', function() { it('exported dashboard should not contain repeated panels', function() {
expect(exported.rows[0].panels.length).to.be(2); expect(exported.rows[0].panels.length).to.be(3);
}); });
it('exported dashboard should not contain repeated rows', function() { it('exported dashboard should not contain repeated rows', function() {
......
...@@ -54,6 +54,12 @@ export class PanelCtrl { ...@@ -54,6 +54,12 @@ export class PanelCtrl {
this.events.emit('panel-teardown'); this.events.emit('panel-teardown');
this.events.removeAllListeners(); this.events.removeAllListeners();
}); });
// we should do something interesting
// with newly added panels
if (this.panel.isNew) {
delete this.panel.isNew;
}
} }
init() { init() {
...@@ -188,6 +194,9 @@ export class PanelCtrl { ...@@ -188,6 +194,9 @@ export class PanelCtrl {
duplicate() { duplicate() {
this.dashboard.duplicatePanel(this.panel, this.row); this.dashboard.duplicatePanel(this.panel, this.row);
this.$timeout(() => {
this.$scope.$root.$broadcast('render');
});
} }
updateColumnSpan(span) { updateColumnSpan(span) {
......
...@@ -68,8 +68,8 @@ module.directive('grafanaPanel', function($rootScope) { ...@@ -68,8 +68,8 @@ module.directive('grafanaPanel', function($rootScope) {
// the reason for handling these classes this way is for performance // the reason for handling these classes this way is for performance
// limit the watchers on panels etc // limit the watchers on panels etc
var transparentLastState; var transparentLastState = false;
var lastHasAlertRule; var lastHasAlertRule = false;
var lastAlertState; var lastAlertState;
var hasAlertRule; var hasAlertRule;
var lastHeight = 0; var lastHeight = 0;
...@@ -91,6 +91,12 @@ module.directive('grafanaPanel', function($rootScope) { ...@@ -91,6 +91,12 @@ module.directive('grafanaPanel', function($rootScope) {
lastHeight = ctrl.containerHeight; lastHeight = ctrl.containerHeight;
} }
// set initial transparency
if (ctrl.panel.transparent) {
transparentLastState = true;
panelContainer.addClass('panel-transparent', true);
}
ctrl.events.on('render', () => { ctrl.events.on('render', () => {
if (lastHeight !== ctrl.containerHeight) { if (lastHeight !== ctrl.containerHeight) {
panelContainer.css({minHeight: ctrl.containerHeight}); panelContainer.css({minHeight: ctrl.containerHeight});
......
...@@ -57,59 +57,3 @@ ...@@ -57,59 +57,3 @@
</div> </div>
</div> </div>
<div class="tight-form" ng-if="false">
<ul class="tight-form-list pull-right">
<li ng-show="ctrl.error" class="tight-form-item">
<a bs-tooltip="ctrl.error" style="color: rgb(229, 189, 28)" role="menuitem">
<i class="fa fa-warning"></i>
</a>
</li>
<li class="tight-form-item small" ng-show="ctrl.target.datasource">
<em>{{ctrl.target.datasource}}</em>
</li>
<li class="tight-form-item" ng-if="ctrl.toggleEditorMode">
<a class="pointer" tabindex="1" ng-click="ctrl.toggleEditorMode()">
<i class="fa fa-pencil"></i>
</a>
</li>
<li class="tight-form-item">
<div class="dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
<i class="fa fa-bars"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.duplicateQuery()">Duplicate</a>
</li>
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.moveQuery(-1)">Move up</a>
</li>
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.moveQuery(1)">Move down</a>
</li>
</ul>
</div>
</li>
<li class="tight-form-item last">
<a class="pointer" tabindex="1" ng-click="ctrl.removeQuery(target)">
<i class="fa fa-trash"></i>
</a>
</li>
</ul>
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">
{{ctrl.target.refId}}
</li>
<li>
<a class="tight-form-item" ng-click="ctrl.toggleHideQuery()" role="menuitem">
<i class="fa fa-eye"></i>
</a>
</li>
</ul>
<ul class="tight-form-list" ng-transclude>
</ul>
<div class="clearfix"></div>
</div>
...@@ -26,7 +26,7 @@ export class AdhocVariable implements Variable { ...@@ -26,7 +26,7 @@ export class AdhocVariable implements Variable {
return Promise.resolve(); return Promise.resolve();
} }
getModel() { getSaveModel() {
assignModelProperties(this.model, this, this.defaults); assignModelProperties(this.model, this, this.defaults);
return this.model; return this.model;
} }
......
...@@ -24,7 +24,7 @@ export class ConstantVariable implements Variable { ...@@ -24,7 +24,7 @@ export class ConstantVariable implements Variable {
assignModelProperties(this, model, this.defaults); assignModelProperties(this, model, this.defaults);
} }
getModel() { getSaveModel() {
assignModelProperties(this.model, this, this.defaults); assignModelProperties(this.model, this, this.defaults);
return this.model; return this.model;
} }
......
...@@ -34,7 +34,7 @@ export class CustomVariable implements Variable { ...@@ -34,7 +34,7 @@ export class CustomVariable implements Variable {
return this.variableSrv.setOptionAsCurrent(this, option); return this.variableSrv.setOptionAsCurrent(this, option);
} }
getModel() { getSaveModel() {
assignModelProperties(this.model, this, this.defaults); assignModelProperties(this.model, this, this.defaults);
return this.model; return this.model;
} }
......
...@@ -30,8 +30,11 @@ export class DatasourceVariable implements Variable { ...@@ -30,8 +30,11 @@ export class DatasourceVariable implements Variable {
this.refresh = 1; this.refresh = 1;
} }
getModel() { getSaveModel() {
assignModelProperties(this.model, this, this.defaults); assignModelProperties(this.model, this, this.defaults);
// dont persist options
this.model.options = [];
return this.model; return this.model;
} }
......
...@@ -34,7 +34,7 @@ export class IntervalVariable implements Variable { ...@@ -34,7 +34,7 @@ export class IntervalVariable implements Variable {
this.refresh = 2; this.refresh = 2;
} }
getModel() { getSaveModel() {
assignModelProperties(this.model, this, this.defaults); assignModelProperties(this.model, this, this.defaults);
return this.model; return this.model;
} }
......
...@@ -136,7 +136,7 @@ ...@@ -136,7 +136,7 @@
<div ng-if="current.type === 'custom'" class="gf-form-group"> <div ng-if="current.type === 'custom'" class="gf-form-group">
<h5 class="section-heading">Custom Options</h5> <h5 class="section-heading">Custom Options</h5>
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-label width-13">Values separated by comma</span> <span class="gf-form-label width-14">Values separated by comma</span>
<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue" required></input> <input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue" required></input>
</div> </div>
</div> </div>
......
...@@ -47,9 +47,15 @@ export class QueryVariable implements Variable { ...@@ -47,9 +47,15 @@ export class QueryVariable implements Variable {
assignModelProperties(this, model, this.defaults); assignModelProperties(this, model, this.defaults);
} }
getModel() { getSaveModel() {
// copy back model properties to model // copy back model properties to model
assignModelProperties(this.model, this, this.defaults); assignModelProperties(this.model, this, this.defaults);
// remove options
if (this.refresh !== 0) {
this.model.options = [];
}
return this.model; return this.model;
} }
......
...@@ -25,7 +25,7 @@ describe('QueryVariable', function() { ...@@ -25,7 +25,7 @@ describe('QueryVariable', function() {
variable.regex = 'asd'; variable.regex = 'asd';
variable.sort = 50; variable.sort = 50;
var model = variable.getModel(); var model = variable.getSaveModel();
expect(model.options.length).to.be(1); expect(model.options.length).to.be(1);
expect(model.options[0].text).to.be('test'); expect(model.options[0].text).to.be('test');
expect(model.datasource).to.be('google'); expect(model.datasource).to.be('google');
...@@ -33,7 +33,14 @@ describe('QueryVariable', function() { ...@@ -33,7 +33,14 @@ describe('QueryVariable', function() {
expect(model.sort).to.be(50); expect(model.sort).to.be(50);
}); });
}); it('if refresh != 0 then remove options in presisted mode', () => {
var variable = new QueryVariable({}, null, null, null, null);
variable.options = [{text: 'test'}];
variable.refresh = 1;
var model = variable.getSaveModel();
expect(model.options.length).to.be(0);
});
});
}); });
...@@ -10,7 +10,7 @@ export interface Variable { ...@@ -10,7 +10,7 @@ export interface Variable {
dependsOn(variable); dependsOn(variable);
setValueFromUrl(urlValue); setValueFromUrl(urlValue);
getValueForUrl(); getValueForUrl();
getModel(); getSaveModel();
} }
export var variableTypes = {}; export var variableTypes = {};
......
...@@ -20,12 +20,9 @@ export class VariableSrv { ...@@ -20,12 +20,9 @@ export class VariableSrv {
this.dashboard = dashboard; this.dashboard = dashboard;
// create working class models representing variables // create working class models representing variables
this.variables = dashboard.templating.list.map(this.createVariableFromModel.bind(this)); this.variables = dashboard.templating.list = dashboard.templating.list.map(this.createVariableFromModel.bind(this));
this.templateSrv.init(this.variables); this.templateSrv.init(this.variables);
// register event to sync back to persisted model
this.dashboard.events.on('prepare-save-model', this.syncToDashboardModel.bind(this));
// init variables // init variables
for (let variable of this.variables) { for (let variable of this.variables) {
variable.initLock = this.$q.defer(); variable.initLock = this.$q.defer();
...@@ -99,12 +96,6 @@ export class VariableSrv { ...@@ -99,12 +96,6 @@ export class VariableSrv {
return variable; return variable;
} }
syncToDashboardModel() {
this.dashboard.templating.list = this.variables.map(variable => {
return variable.getModel();
});
}
updateOptions(variable) { updateOptions(variable) {
return variable.updateOptions(); return variable.updateOptions();
} }
......
<li ng-class="{active: active, disabled: disabled}">
<a href ng-click="select()" tab-heading-transclude>{{heading}}</a>
</li>
<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>
<navbar title="404" icon="fa fa-fw fa-question" title-url="/">
</navbar>
<div class="row-fluid" style="margin-top: 100px;"> <div class="page-container">
<div class="span2"></div>
<div class="grafana-info-box span8 text-center"> <div class="page-header">
<h3>Page not found (404)</h3> <h1>
</div> Page not found (404)
</h1>
<div class="span2"></div> </div>
</div> </div>
{ {
"revision": 5, "revision": 6,
"title": "TestData - Graph Panel Last 1h", "title": "TestData - Graph Panel Last 1h",
"tags": [ "tags": [
"grafana-test" "grafana-test"
...@@ -7,8 +7,48 @@ ...@@ -7,8 +7,48 @@
"style": "dark", "style": "dark",
"timezone": "browser", "timezone": "browser",
"editable": true, "editable": true,
"hideControls": false,
"sharedCrosshair": false, "sharedCrosshair": false,
"hideControls": false,
"time": {
"from": "2016-11-16T16:59:38.294Z",
"to": "2016-11-16T17:09:01.532Z"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"templating": {
"list": []
},
"annotations": {
"list": []
},
"refresh": false,
"schemaVersion": 13,
"version": 4,
"links": [],
"gnetId": null,
"rows": [ "rows": [
{ {
"collapse": false, "collapse": false,
...@@ -238,7 +278,13 @@ ...@@ -238,7 +278,13 @@
] ]
} }
], ],
"title": "New row" "title": "New row",
"showTitle": false,
"titleSize": "h6",
"isNew": false,
"repeat": null,
"repeatRowId": null,
"repeatIteration": null
}, },
{ {
"collapse": false, "collapse": false,
...@@ -332,7 +378,13 @@ ...@@ -332,7 +378,13 @@
"type": "text" "type": "text"
} }
], ],
"title": "New row" "title": "New row",
"showTitle": false,
"titleSize": "h6",
"isNew": false,
"repeat": null,
"repeatRowId": null,
"repeatIteration": null
}, },
{ {
"collapse": false, "collapse": false,
...@@ -371,7 +423,7 @@ ...@@ -371,7 +423,7 @@
"yaxis": 2 "yaxis": 2
} }
], ],
"span": 7.99561403508772, "span": 8,
"stack": false, "stack": false,
"steppedLine": false, "steppedLine": false,
"targets": [ "targets": [
...@@ -432,12 +484,18 @@ ...@@ -432,12 +484,18 @@
"isNew": true, "isNew": true,
"links": [], "links": [],
"mode": "markdown", "mode": "markdown",
"span": 4.00438596491228, "span": 4,
"title": "", "title": "",
"type": "text" "type": "text"
} }
], ],
"title": "New row" "title": "New row",
"showTitle": false,
"titleSize": "h6",
"isNew": false,
"repeat": null,
"repeatRowId": null,
"repeatIteration": null
}, },
{ {
"collapse": false, "collapse": false,
...@@ -545,7 +603,7 @@ ...@@ -545,7 +603,7 @@
"points": false, "points": false,
"renderer": "flot", "renderer": "flot",
"seriesOverrides": [], "seriesOverrides": [],
"span": 3, "span": 4,
"stack": false, "stack": false,
"steppedLine": false, "steppedLine": false,
"targets": [ "targets": [
...@@ -593,6 +651,31 @@ ...@@ -593,6 +651,31 @@
] ]
}, },
{ {
"content": "Should be a long line connecting the null region in the `connected` mode, and in zero it should just be a line with zero value at the null points. ",
"editable": true,
"error": false,
"id": 13,
"isNew": true,
"links": [],
"mode": "markdown",
"span": 4,
"title": "",
"type": "text"
}
],
"title": "New row",
"showTitle": false,
"titleSize": "h6",
"isNew": false,
"repeat": null,
"repeatRowId": null,
"repeatIteration": null
},
{
"isNew": false,
"title": "Dashboard Row",
"panels": [
{
"aliasColors": {}, "aliasColors": {},
"bars": false, "bars": false,
"datasource": "Grafana TestData", "datasource": "Grafana TestData",
...@@ -624,7 +707,7 @@ ...@@ -624,7 +707,7 @@
"zindex": -3 "zindex": -3
} }
], ],
"span": 5, "span": 8,
"stack": true, "stack": true,
"steppedLine": false, "steppedLine": false,
"targets": [ "targets": [
...@@ -687,49 +770,149 @@ ...@@ -687,49 +770,149 @@
"show": true "show": true
} }
] ]
},
{
"content": "Stacking values on top of nulls, should treat the null values as zero. ",
"editable": true,
"error": false,
"id": 14,
"isNew": true,
"links": [],
"mode": "markdown",
"span": 4,
"title": "",
"type": "text"
} }
], ],
"title": "New row" "showTitle": false,
"titleSize": "h6",
"height": 250,
"repeat": null,
"repeatRowId": null,
"repeatIteration": null,
"collapse": false
},
{
"isNew": false,
"title": "Dashboard Row",
"panels": [
{
"aliasColors": {},
"bars": false,
"datasource": "Grafana TestData",
"editable": true,
"error": false,
"fill": 1,
"id": 12,
"isNew": true,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [
{
"alias": "B-series",
"zindex": -3
}
],
"span": 8,
"stack": true,
"steppedLine": false,
"targets": [
{
"hide": false,
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
"target": "",
"alias": ""
},
{
"alias": "",
"hide": false,
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
"target": ""
},
{
"alias": "",
"hide": false,
"refId": "C",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
"target": ""
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Stacking all series null segment",
"tooltip": {
"msResolution": false,
"shared": true,
"sort": 0,
"value_type": "cumulative"
},
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"content": "Stacking when all values are null should leave a gap in the graph",
"editable": true,
"error": false,
"id": 15,
"isNew": true,
"links": [],
"mode": "markdown",
"span": 4,
"title": "",
"type": "text"
}
],
"showTitle": false,
"titleSize": "h6",
"height": 250,
"repeat": null,
"repeatRowId": null,
"repeatIteration": null,
"collapse": false
} }
], ]
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"templating": {
"list": []
},
"annotations": {
"list": []
},
"refresh": false,
"schemaVersion": 13,
"version": 13,
"links": [],
"gnetId": null
} }
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
"name": "Grafana Project", "name": "Grafana Project",
"url": "http://grafana.org" "url": "http://grafana.org"
}, },
"version": "1.0.14", "version": "1.0.15",
"updated": "2016-09-26" "updated": "2016-09-26"
}, },
......
...@@ -37,7 +37,8 @@ function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) { ...@@ -37,7 +37,8 @@ function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) {
query.dimensions = self.convertDimensionFormat(target.dimensions, options.scopedVars); query.dimensions = self.convertDimensionFormat(target.dimensions, options.scopedVars);
query.statistics = target.statistics; query.statistics = target.statistics;
var period = this._getPeriod(target, query, options, start, end); var now = Math.round(Date.now() / 1000);
var period = this._getPeriod(target, query, options, start, end, now);
target.period = period; target.period = period;
query.period = period; query.period = period;
...@@ -67,11 +68,19 @@ function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) { ...@@ -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 period;
var range = end - start; var range = end - start;
if (!target.period) { var daySec = 60 * 60 * 24;
var periodUnit = 60;
if (now - start > (daySec * 15)) { // until 63 days ago
periodUnit = period = 60 * 5;
} else if (now - start > (daySec * 63)) { // until 455 days ago
periodUnit = period = 60 * 60;
} else if (now - start > (daySec * 455)) { // over 455 days, should return error, but try to long period
periodUnit = period = 60 * 60;
} else if (!target.period) {
period = (query.namespace === 'AWS/EC2') ? 300 : 60; period = (query.namespace === 'AWS/EC2') ? 300 : 60;
} else if (/^\d+$/.test(target.period)) { } else if (/^\d+$/.test(target.period)) {
period = parseInt(target.period, 10); period = parseInt(target.period, 10);
...@@ -82,7 +91,7 @@ function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) { ...@@ -82,7 +91,7 @@ function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) {
period = 60; period = 60;
} }
if (range / period >= 1440) { if (range / period >= 1440) {
period = Math.ceil(range / 1440 / 60) * 60; period = Math.ceil(range / 1440 / periodUnit) * periodUnit;
} }
return period; return period;
......
...@@ -22,7 +22,7 @@ function ($) { ...@@ -22,7 +22,7 @@ function ($) {
var len = series.datapoints.points.length; var len = series.datapoints.points.length;
for (var j = initial; j < len; j += ps) { 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 // 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 //normal case
|| series.datapoints.points[j] > posX) { || series.datapoints.points[j] > posX) {
return Math.max(j - ps, 0)/ps; return Math.max(j - ps, 0)/ps;
...@@ -195,7 +195,7 @@ function ($) { ...@@ -195,7 +195,7 @@ function ($) {
} }
var highlightClass = ''; var highlightClass = '';
if (item && i === item.seriesIndex) { if (item && hoverInfo.index === item.seriesIndex) {
highlightClass = 'graph-tooltip-list-item--highlight'; highlightClass = 'graph-tooltip-list-item--highlight';
} }
......
...@@ -208,11 +208,8 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -208,11 +208,8 @@ class SingleStatCtrl extends MetricsPanelCtrl {
} }
// Add $__name variable for using in prefix or postfix // Add $__name variable for using in prefix or postfix
data.scopedVars = { data.scopedVars = _.extend({}, this.panel.scopedVars);
__name: { data.scopedVars["__name"] = {value: this.series[0].label};
value: this.series[0].label
}
};
} }
// check value to text mappings if its enabled // check value to text mappings if its enabled
...@@ -526,7 +523,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -526,7 +523,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
elem.toggleClass('pointer', panel.links.length > 0); elem.toggleClass('pointer', panel.links.length > 0);
if (panel.links.length > 0) { if (panel.links.length > 0) {
linkInfo = linkSrv.getPanelLinkAnchorInfo(panel.links[0], panel.scopedVars); linkInfo = linkSrv.getPanelLinkAnchorInfo(panel.links[0], data.scopedVars);
} else { } else {
linkInfo = null; linkInfo = null;
} }
......
...@@ -50,11 +50,9 @@ ...@@ -50,11 +50,9 @@
@import "components/tagsinput"; @import "components/tagsinput";
@import "components/tables_lists"; @import "components/tables_lists";
@import "components/search"; @import "components/search";
@import "components/tightform";
@import "components/gf-form"; @import "components/gf-form";
@import "components/sidemenu"; @import "components/sidemenu";
@import "components/navbar"; @import "components/navbar";
@import "components/gfbox";
@import "components/timepicker"; @import "components/timepicker";
@import "components/filter-controls"; @import "components/filter-controls";
@import "components/filter-list"; @import "components/filter-list";
......
.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;
}
}
...@@ -87,7 +87,7 @@ ...@@ -87,7 +87,7 @@
} }
// temp hack // temp hack
.modal-body, .gf-box { .modal-body {
.nav-tabs { .nav-tabs {
border-bottom: none; border-bottom: none;
} }
......
...@@ -67,3 +67,82 @@ ...@@ -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;
}
}
...@@ -74,12 +74,11 @@ ...@@ -74,12 +74,11 @@
.add-panel-panels-scroll { .add-panel-panels-scroll {
width: 100%; width: 100%;
overflow: auto; overflow: auto;
-ms-overflow-style: none;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none display: none
} }
-ms-overflow-style: none;
} }
.add-panel-panels { .add-panel-panels {
......
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
} }
.shortcut-table { .shortcut-table {
margin-bottom: $spacer;
.shortcut-table-category-header { .shortcut-table-category-header {
font-weight: normal; font-weight: normal;
font-size: $font-size-h6; font-size: $font-size-h6;
...@@ -26,8 +28,6 @@ ...@@ -26,8 +28,6 @@
text-align: right; text-align: right;
color: $text-color; color: $text-color;
} }
margin-bottom: $spacer;
} }
.shortcut-table-key { .shortcut-table-key {
......
...@@ -7,11 +7,12 @@ ...@@ -7,11 +7,12 @@
} }
.annotation-segment { .annotation-segment {
padding: 8px 7px;
label.cr1 { label.cr1 {
margin-left: 5px; margin-left: 5px;
margin-top: 3px; margin-top: 3px;
} }
padding: 8px 7px;
} }
.submenu-item { .submenu-item {
...@@ -31,14 +32,14 @@ ...@@ -31,14 +32,14 @@
.variable-value-link { .variable-value-link {
padding-right: 10px; padding-right: 10px;
.label-tag {
margin: 0 5px;
}
padding: 8px 7px; padding: 8px 7px;
box-sizing: content-box; box-sizing: content-box;
display: inline-block; display: inline-block;
color: $text-color; color: $text-color;
.label-tag {
margin: 0 5px;
}
} }
.variable-link-wrapper { .variable-link-wrapper {
......
...@@ -38,10 +38,10 @@ ...@@ -38,10 +38,10 @@
background-color: transparent; background-color: transparent;
border: none; border: none;
padding: ($tabs-padding-top + $tabs-top-margin) $spacer $tabs-padding-bottom; padding: ($tabs-padding-top + $tabs-top-margin) $spacer $tabs-padding-bottom;
color: $text-color;
i { i {
font-size: 120%; font-size: 120%;
} }
color: $text-color;
&:hover { &:hover {
color: $white; color: $white;
} }
......
.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;
}
...@@ -65,15 +65,17 @@ ...@@ -65,15 +65,17 @@
} }
.gf-timepicker-component { .gf-timepicker-component {
margin-bottom: 10px; padding: $spacer/2 0 $spacer 0;
td { td {
padding: 1px; padding: 1px;
} }
button.btn-sm { button.btn-sm {
@include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl); @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl);
font-size: $font-size-sm;
background-image: none; background-image: none;
border: none; border: none;
padding: 6px 10px; padding: 5px 11px;
color: $text-color; color: $text-color;
&.active span { &.active span {
color: $blue; color: $blue;
......
...@@ -62,12 +62,6 @@ ...@@ -62,12 +62,6 @@
.admin-page { .admin-page {
max-width: 800px; max-width: 800px;
margin-left: 10px; margin-left: 10px;
.gf-box {
margin-top: 0;
}
.gf-box-body {
min-height: 0;
}
h2 { h2 {
margin-left: 15px; margin-left: 15px;
margin-bottom: 0px; margin-bottom: 0px;
......
...@@ -61,7 +61,6 @@ ...@@ -61,7 +61,6 @@
} }
&--ok { &--ok {
box-shadow: 0 0 5px rgba(0,200,0,10.8);
.panel-alert-icon:before { .panel-alert-icon:before {
color: $online; color: $online;
content: "\e611"; content: "\e611";
......
...@@ -172,6 +172,12 @@ div.flot-text { ...@@ -172,6 +172,12 @@ div.flot-text {
} }
} }
.panel-in-fullscreen {
.panel-drop-zone {
display: none !important;
}
}
.panel-time-info { .panel-time-info {
font-weight: bold; font-weight: bold;
float: right; float: right;
......
...@@ -72,52 +72,49 @@ charts or filled areas). ...@@ -72,52 +72,49 @@ charts or filled areas).
horizontal = s.bars.horizontal, horizontal = s.bars.horizontal,
withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y),
withsteps = withlines && s.lines.steps, withsteps = withlines && s.lines.steps,
fromgap = true,
keyOffset = horizontal ? 1 : 0, keyOffset = horizontal ? 1 : 0,
accumulateOffset = horizontal ? 0 : 1, accumulateOffset = horizontal ? 0 : 1,
i = 0, j = 0, l, m; i = 0, j = 0, l, m;
while (true) { while (true) {
// browse all points from the current series and from the previous series
if (i >= points.length && j >= otherpoints.length) if (i >= points.length && j >= otherpoints.length)
break; break;
// newpoints will replace current series with
// as many points as different timestamps we have in the 2 (current & previous) series
l = newpoints.length; l = newpoints.length;
px = points[i + keyOffset];
py = points[i + accumulateOffset]; if (i < points.length && points[i] == null) {
qx = otherpoints[j + keyOffset]; // copy gaps
qy = otherpoints[j + accumulateOffset]; for (m = 0; m < ps; ++m)
bottom = 0; newpoints.push(points[i + m]);
if (i < points.length && px == null) {
// let's ignore null points from current series, nothing to do with them
i += ps; 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) { else if (i >= points.length) {
// no more points in the current series, simply take the remaining points // take the remaining points from the previous series
// from the previous series so that next series will correctly stack
for (m = 0; m < ps; ++m) for (m = 0; m < ps; ++m)
newpoints.push(otherpoints[j + m]); newpoints.push(otherpoints[j + m]);
bottom = qy; if (withbottom)
newpoints[l + 2] = otherpoints[j + accumulateOffset];
j += otherps; j += otherps;
} }
else if (j >= otherpoints.length) { else if (j >= otherpoints.length) {
// no more points in the previous series, of course let's take // take the remaining points from the current series
// the remaining points from the current series
for (m = 0; m < ps; ++m) for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]); newpoints.push(points[i + m]);
i += ps; i += ps;
} }
else if (j < otherpoints.length && otherpoints[j] == null) {
// ignore point
j += otherps;
}
else { 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) { if (px == qx) {
// so take the point from the current series and skip the previous' one
for (m = 0; m < ps; ++m) for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]); newpoints.push(points[i + m]);
...@@ -127,23 +124,27 @@ charts or filled areas). ...@@ -127,23 +124,27 @@ charts or filled areas).
i += ps; i += ps;
j += otherps; j += otherps;
} }
// next available point with the smallest timestamp is from the previous series
else if (px > qx) { else if (px > qx) {
// so take the point from the previous series so that next series will correctly stack // take the point from the previous series so that next series will correctly stack
for (m = 0; m < ps; ++m) if (i == 0) {
newpoints.push(otherpoints[j + m]); for (m = 0; m < ps; ++m)
newpoints.push(otherpoints[j + m]);
// we might be able to interpolate bottom = qy;
if (i > 0 && points[i - ps] != null) }
newpoints[l + accumulateOffset] += py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); // we got past point below, might need to
// insert interpolated extra point
bottom = qy; 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; j += otherps;
} }
// (px < qx) next available point with the smallest timestamp is from the current series else { // px < qx
else {
// so of course let's take the point from the current series
for (m = 0; m < ps; ++m) for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]); newpoints.push(points[i + m]);
...@@ -156,10 +157,22 @@ charts or filled areas). ...@@ -156,10 +157,22 @@ charts or filled areas).
i += ps; i += ps;
} }
}
if (l != newpoints.length && withbottom) fromgap = false;
newpoints[l + 2] = bottom;
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; datapoints.points = newpoints;
......
...@@ -5,28 +5,32 @@ ...@@ -5,28 +5,32 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<title>Grafana</title> <title>Grafana - Error</title>
<link href='[[.AppSubUrl]]/public/css/fonts.min.css' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.dark.min.css">
<link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.dark.min.css" title="Dark">
<link rel="icon" type="image/png" href="[[.AppSubUrl]]/public/img/fav32.png"> <link rel="icon" type="image/png" href="[[.AppSubUrl]]/public/img/fav32.png">
<base href="[[.AppSubUrl]]/" />
</head> </head>
<body> <body>
<div class="gf-box" style="margin: 200px auto 0 auto; width: 500px;"> <div class="page-container">
<div class="gf-box-header"> <div class="page-header">
<span class="gf-box-title"> <h1>
Server side error :( Server side error :(
</span> </h1>
</div> </div>
<div class="gf-box-body"> <h4>[[.Title]]</h4>
<h4>[[.Title]]</h4>
[[.ErrorMsg]] <pre>[[.ErrorMsg]]</pre>
</div>
</div>
</body> </div>
</body>
</html> </html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment