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

This commit is contained in:
Torkel Ödegaard 2016-11-18 16:31:54 +01:00
commit fd512457d8
93 changed files with 854 additions and 1344 deletions

View File

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

View File

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

View File

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

View File

@ -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.

View File

@ -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`.

View File

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

View File

@ -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.

View File

@ -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`

View File

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

View File

@ -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

View File

@ -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 {

View File

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

View File

@ -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 {

View File

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

View File

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

View File

@ -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))
} }

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -1,6 +1,8 @@
package alerting package alerting
import ( import (
"strconv"
"strings"
"time" "time"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
@ -21,7 +23,10 @@ func NewEvalHandler() *DefaultEvalHandler {
func (e *DefaultEvalHandler) Eval(context *EvalContext) { func (e *DefaultEvalHandler) Eval(context *EvalContext) {
firing := true firing := true
for _, condition := range context.Rule.Conditions { conditionEvals := ""
for i := 0; i < len(context.Rule.Conditions); i++ {
condition := context.Rule.Conditions[i]
cr, err := condition.Eval(context) cr, err := condition.Eval(context)
if err != nil { if err != nil {
context.Error = err context.Error = err
@ -32,15 +37,23 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
break break
} }
// break if result has not triggered yet // 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

View File

@ -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) {
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")
}) })
}) })
} }

View File

@ -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
} }

View File

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

View File

@ -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
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);

View File

@ -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;

View File

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

View File

@ -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>');
}
});
}
};
});
});

View File

@ -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);

View File

@ -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');

View File

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

View File

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

View File

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

View File

@ -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">

View File

@ -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);

View File

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

View File

@ -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;
}); });

View File

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

View File

@ -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 {
this.events = events; this.events = events;
this.meta = meta; this.meta = meta;
this.rows = rows; this.rows = rows;
this.templating.list = variables;
return copy; return copy;
} }
@ -233,7 +237,6 @@ export class DashboardModel {
} }
duplicatePanel(panel, row) { duplicatePanel(panel, row) {
var rowIndex = _.indexOf(this.rows, row);
var newPanel = angular.copy(panel); var newPanel = angular.copy(panel);
newPanel.id = this.getNextPanelId(); newPanel.id = this.getNextPanelId();
@ -241,9 +244,9 @@ export class DashboardModel {
delete newPanel.repeatIteration; delete newPanel.repeatIteration;
delete newPanel.repeatPanelId; delete newPanel.repeatPanelId;
delete newPanel.scopedVars; delete newPanel.scopedVars;
delete newPanel.alert;
var currentRow = this.rows[rowIndex]; row.addPanel(newPanel);
currentRow.panels.push(newPanel);
return newPanel; return newPanel;
} }

View File

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

View File

@ -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>

View File

@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {
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});

View File

@ -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>

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -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>

View File

@ -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;
} }

View File

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

View File

@ -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 = {};

View File

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

View File

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

View File

@ -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>

View File

@ -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="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>

View File

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

View File

@ -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"
}, },

View File

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

View File

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

View File

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

View File

@ -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';
} }

View File

@ -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 {
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;
} }

View File

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

View File

@ -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;
}
}

View File

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

View File

@ -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;
}
}

View File

@ -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 {

View File

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

View File

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

View File

@ -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;
} }

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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";

View File

@ -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;

View File

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

View File

@ -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> </div>
</body>
</body>
</html> </html>