diff --git a/CHANGELOG.md b/CHANGELOG.md index 110c8015a27..4dd0adba4c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,11 @@ -# 4.0-pre (unreleased) +# 4.0-beta2 (unrelased) + +### Bugfixes +* **Graph Panel**: Log base scale on right Y-axis had no effect, max value calc was not applied, [#6534](https://github.com/grafana/grafana/issues/6534) +* **Graph Panel**: Bar width if bars was only used in series override, [#6528](https://github.com/grafana/grafana/issues/6528) +* **UI/Browser**: Fixed issue with page/view header gradient border not showing in Safari, [#6530](https://github.com/grafana/grafana/issues/6530) + +# 4.0-beta1 (2016-11-09) ### Enhancements * **Login**: Adds option to disable username/password logins, closes [#4674](https://github.com/grafana/grafana/issues/4674) @@ -24,7 +31,7 @@ * **SystemD**: Change systemd description, closes [#5971](https://github.com/grafana/grafana/pull/5971) * **lodash upgrade**: Upgraded lodash from 2.4.2 to 4.15.0, this contains a number of breaking changes that could effect plugins. closes [#6021](https://github.com/grafana/grafana/pull/6021) -### Bugfixes +### Bug fixes * **Table Panel**: Fixed problem when switching to Mixed datasource in metrics tab, fixes [#5999](https://github.com/grafana/grafana/pull/5999) * **Playlist**: Fixed problem with play order not matching order defined in playlist, fixes [#5467](https://github.com/grafana/grafana/pull/5467) * **Graph panel**: Fixed problem with auto decimals on y axis when datamin=datamax, fixes [#6070](https://github.com/grafana/grafana/pull/6070) diff --git a/circle.yml b/circle.yml index 39dc626df04..2422715bda3 100644 --- a/circle.yml +++ b/circle.yml @@ -5,7 +5,7 @@ machine: GOPATH: "/home/ubuntu/.go_workspace" ORG_PATH: "github.com/grafana" REPO_PATH: "${ORG_PATH}/grafana" - GODIST: "go1.7.1.linux-amd64.tar.gz" + GODIST: "go1.7.3.linux-amd64.tar.gz" post: - mkdir -p download - test -e download/$GODIST || curl -o download/$GODIST https://storage.googleapis.com/golang/$GODIST diff --git a/conf/defaults.ini b/conf/defaults.ini index b25f3ab6f28..7ead103d027 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -229,7 +229,7 @@ auth_url = https://accounts.google.com/o/oauth2/auth token_url = https://accounts.google.com/o/oauth2/token api_url = https://www.googleapis.com/oauth2/v1/userinfo allowed_domains = -hosted_domain = +hosted_domain = #################################### Grafana.net Auth #################### [auth.grafananet] @@ -390,21 +390,6 @@ global_api_key = -1 global_session = -1 #################################### Alerting ############################ -# docs about alerting can be found in /docs/sources/alerting/ -# __.-/| -# \`o_O' -# =( )= +----------------------------+ -# U| | Alerting is still in alpha | -# /\ /\ / | +----------------------------+ -# ) /^\) ^\/ _)\ | -# ) /^\/ _) \ | -# ) _ / / _) \___|_ -# /\ )/\/ || | )_)\___,|)) -# < > |(,,) )__) | -# || / \)___)\ -# | \____( )___) )____ -# \______(_______;;;)__;;;) - [alerting] # Makes it possible to turn off alert rule execution. execute_alerts = true diff --git a/conf/sample.ini b/conf/sample.ini index e1ed408210a..e8c9c990a63 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -339,21 +339,6 @@ ;path = /var/lib/grafana/dashboards #################################### Alerting ###################################### -# docs about alerting can be found in /docs/sources/alerting/ -# __.-/| -# \`o_O' -# =( )= +----------------------------+ -# U| | Alerting is still in alpha | -# /\ /\ / | +----------------------------+ -# ) /^\) ^\/ _)\ | -# ) /^\/ _) \ | -# ) _ / / _) \___|_ -# /\ )/\/ || | )_)\___,|)) -# < > |(,,) )__) | -# || / \)___)\ -# | \____( )___) )____ -# \______(_______;;;)__;;;) - [alerting] # Makes it possible to turn off alert rule execution. ;execute_alerts = true diff --git a/docs/sources/datasources/influxdb.md b/docs/sources/datasources/influxdb.md index a5eab80af06..01b68d5b7dc 100644 --- a/docs/sources/datasources/influxdb.md +++ b/docs/sources/datasources/influxdb.md @@ -118,7 +118,7 @@ SHOW TAG VALUES WITH KEY = "hostname" WHERE region =~ /$region/ > Always you `regex values` or `regex wildcard` for All format or multi select format. -![](img/docs/influxdb/templating_simple_ex1.png) +![](/img/docs/influxdb/templating_simple_ex1.png) ## Annotations Annotations allows you to overlay rich event information on top of graphs. diff --git a/docs/sources/guides/whats-new-in-v3-1.md b/docs/sources/guides/whats-new-in-v3-1.md index 5dd23dff156..236e77ea266 100644 --- a/docs/sources/guides/whats-new-in-v3-1.md +++ b/docs/sources/guides/whats-new-in-v3-1.md @@ -4,7 +4,7 @@ description = "Feature & improvement highlights for Grafana v3.1" keywords = ["grafana", "new", "documentation", "3.1"] type = "docs" [menu.docs] -name = "Version 3.1 (Latest)" +name = "Version 3.1" identifier = "v3.1" parent = "whatsnew" weight = 1 diff --git a/docs/sources/guides/whats-new-in-v4.md b/docs/sources/guides/whats-new-in-v4.md index 39b2437d054..71a5c0251da 100644 --- a/docs/sources/guides/whats-new-in-v4.md +++ b/docs/sources/guides/whats-new-in-v4.md @@ -4,7 +4,7 @@ description = "Feature & improvement highlights for Grafana v4.0" keywords = ["grafana", "new", "documentation", "4.0"] type = "docs" [menu.docs] -name = "Version 4.0" +name = "Version 4.0 (Latest)" identifier = "v4.0" parent = "whatsnew" weight = -1 diff --git a/docs/sources/http_api/org.md b/docs/sources/http_api/org.md index adb5d5cd31e..36188075124 100644 --- a/docs/sources/http_api/org.md +++ b/docs/sources/http_api/org.md @@ -85,6 +85,34 @@ page_keywords: grafana, admin, http, api, documentation, orgs, organisation } } +## Create Organisation + +`POST /api/org` + +**Example Request**: + + POST /api/org HTTP/1.1 + Accept: application/json + Content-Type: application/json + Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + + { + "name":"New Org." + } + + +**Example Response**: + + HTTP/1.1 200 + Content-Type: application/json + + { + "orgId":"1", + "message":"Organization created" + } + + + ## Update current Organisation `PUT /api/org` diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 2fc727d659f..f3b40b206bf 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -413,7 +413,7 @@ Set to `true` to enable LDAP integration (default: `false`) ### config_file Path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`) -> For details on LDAP Configuration, go to the [LDAP Integration](ldap.md) page. +> For details on LDAP Configuration, go to the [LDAP Integration]({{< relref "ldap.md" >}}) page.
diff --git a/pkg/metrics/publish.go b/pkg/metrics/publish.go index 4255481b8d1..70db3fc0f86 100644 --- a/pkg/metrics/publish.go +++ b/pkg/metrics/publish.go @@ -101,6 +101,7 @@ func sendUsageStats() { metrics["stats.plugins.apps.count"] = len(plugins.Apps) metrics["stats.plugins.panels.count"] = len(plugins.Panels) metrics["stats.plugins.datasources.count"] = len(plugins.DataSources) + metrics["stats.alerts.count"] = statsQuery.Result.AlertCount dsStats := m.GetDataSourceStatsQuery{} if err := bus.Dispatch(&dsStats); err != nil { diff --git a/pkg/models/stats.go b/pkg/models/stats.go index 067dec763e5..09c251b6cd7 100644 --- a/pkg/models/stats.go +++ b/pkg/models/stats.go @@ -5,6 +5,7 @@ type SystemStats struct { UserCount int64 OrgCount int64 PlaylistCount int64 + AlertCount int64 } type DataSourceStats struct { @@ -29,6 +30,7 @@ type AdminStats struct { DataSourceCount int `json:"data_source_count"` PlaylistCount int `json:"playlist_count"` StarredDbCount int `json:"starred_db_count"` + AlertCount int `json:"alert_count"` } type GetAdminStatsQuery struct { diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 809640ed4a7..bdf53798e34 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -26,11 +26,32 @@ type Rule struct { } type ValidationError struct { - Reason string + Reason string + Err error + Alertid int64 + DashboardId int64 + PanelId int64 } func (e ValidationError) Error() string { - return e.Reason + extraInfo := "" + if e.Alertid != 0 { + extraInfo = fmt.Sprintf("%s AlertId: %v", extraInfo, e.Alertid) + } + + if e.PanelId != 0 { + extraInfo = fmt.Sprintf("%s PanelId: %v ", extraInfo, e.PanelId) + } + + if e.DashboardId != 0 { + extraInfo = fmt.Sprintf("%s DashboardId: %v", extraInfo, e.DashboardId) + } + + if e.Err != nil { + return fmt.Sprintf("%s %s%s", e.Err.Error(), e.Reason, extraInfo) + } + + return fmt.Sprintf("Failed to extract alert.Reason: %s %s", e.Reason, extraInfo) } var ( @@ -83,7 +104,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { for _, v := range ruleDef.Settings.Get("notifications").MustArray() { jsonModel := simplejson.NewFromAny(v) if id, err := jsonModel.Get("id").Int64(); err != nil { - return nil, ValidationError{Reason: "Invalid notification schema"} + return nil, ValidationError{Reason: "Invalid notification schema", DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId} } else { model.Notifications = append(model.Notifications, id) } @@ -93,10 +114,10 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { conditionModel := simplejson.NewFromAny(condition) conditionType := conditionModel.Get("type").MustString() if factory, exist := conditionFactories[conditionType]; !exist { - return nil, ValidationError{Reason: "Unknown alert condition: " + conditionType} + return nil, ValidationError{Reason: "Unknown alert condition: " + conditionType, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId} } else { if queryCondition, err := factory(conditionModel, index); err != nil { - return nil, err + return nil, ValidationError{Err: err, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId} } else { model.Conditions = append(model.Conditions, queryCondition) } diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go index 7580996ad57..dd1a8111332 100644 --- a/pkg/services/sqlstore/stats.go +++ b/pkg/services/sqlstore/stats.go @@ -39,7 +39,11 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error { ( SELECT COUNT(*) FROM ` + dialect.Quote("playlist") + ` - ) AS playlist_count + ) AS playlist_count, + ( + SELECT COUNT(*) + FROM ` + dialect.Quote("alert") + ` + ) AS alert_count ` var stats m.SystemStats @@ -85,7 +89,11 @@ func GetAdminStats(query *m.GetAdminStatsQuery) error { ( SELECT COUNT(DISTINCT ` + dialect.Quote("dashboard_id") + ` ) FROM ` + dialect.Quote("star") + ` - ) AS starred_db_count + ) AS starred_db_count, + ( + SELECT COUNT(*) + FROM ` + dialect.Quote("alert") + ` + ) AS alert_count ` var stats m.AdminStats diff --git a/pkg/tsdb/influxdb/influxdb.go b/pkg/tsdb/influxdb/influxdb.go index 959f12f708f..5043a2deffc 100644 --- a/pkg/tsdb/influxdb/influxdb.go +++ b/pkg/tsdb/influxdb/influxdb.go @@ -18,7 +18,6 @@ import ( type InfluxDBExecutor struct { *tsdb.DataSourceInfo QueryParser *InfluxdbQueryParser - QueryBuilder *QueryBuilder ResponseParser *ResponseParser } @@ -26,7 +25,6 @@ func NewInfluxDBExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor { return &InfluxDBExecutor{ DataSourceInfo: dsInfo, QueryParser: &InfluxdbQueryParser{}, - QueryBuilder: &QueryBuilder{}, ResponseParser: &ResponseParser{}, } } @@ -51,11 +49,16 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, return result.WithError(err) } - if setting.Env == setting.DEV { - glog.Debug("Influxdb query", "raw query", query) + rawQuery, err := query.Build(context) + if err != nil { + return result.WithError(err) } - req, err := e.createRequest(query) + if setting.Env == setting.DEV { + glog.Debug("Influxdb query", "raw query", rawQuery) + } + + req, err := e.createRequest(rawQuery) if err != nil { return result.WithError(err) } @@ -79,29 +82,28 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, return result.WithError(err) } + if response.Err != nil { + return result.WithError(response.Err) + } + result.QueryResults = make(map[string]*tsdb.QueryResult) - result.QueryResults["A"] = e.ResponseParser.Parse(&response) + result.QueryResults["A"] = e.ResponseParser.Parse(&response, query) return result } -func (e *InfluxDBExecutor) getQuery(queries tsdb.QuerySlice, context *tsdb.QueryContext) (string, error) { +func (e *InfluxDBExecutor) getQuery(queries tsdb.QuerySlice, context *tsdb.QueryContext) (*Query, error) { for _, v := range queries { query, err := e.QueryParser.Parse(v.Model, e.DataSourceInfo) if err != nil { - return "", err + return nil, err } - rawQuery, err := e.QueryBuilder.Build(query, context) - if err != nil { - return "", err - } - - return rawQuery, nil + return query, nil } - return "", fmt.Errorf("query request contains no queries") + return nil, fmt.Errorf("query request contains no queries") } func (e *InfluxDBExecutor) createRequest(query string) (*http.Request, error) { diff --git a/pkg/tsdb/influxdb/model_parser.go b/pkg/tsdb/influxdb/model_parser.go index ff8977f925b..410bf8f6e82 100644 --- a/pkg/tsdb/influxdb/model_parser.go +++ b/pkg/tsdb/influxdb/model_parser.go @@ -12,7 +12,8 @@ type InfluxdbQueryParser struct{} func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSourceInfo) (*Query, error) { policy := model.Get("policy").MustString("default") rawQuery := model.Get("query").MustString("") - interval := model.Get("interval").MustString("") + useRawQuery := model.Get("rawQuery").MustBool(false) + alias := model.Get("alias").MustString("") measurement := model.Get("measurement").MustString("") @@ -36,7 +37,8 @@ func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSo return nil, err } - if interval == "" { + interval := model.Get("interval").MustString("") + if interval == "" && dsInfo.JsonData != nil { dsInterval := dsInfo.JsonData.Get("timeInterval").MustString("") if dsInterval != "" { interval = dsInterval @@ -52,6 +54,8 @@ func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSo Selects: selects, RawQuery: rawQuery, Interval: interval, + Alias: alias, + UseRawQuery: useRawQuery, }, nil } diff --git a/pkg/tsdb/influxdb/model_parser_test.go b/pkg/tsdb/influxdb/model_parser_test.go index 8f43cc7d70f..eae6e163cc0 100644 --- a/pkg/tsdb/influxdb/model_parser_test.go +++ b/pkg/tsdb/influxdb/model_parser_test.go @@ -90,6 +90,7 @@ func TestInfluxdbQueryParser(t *testing.T) { } ] ], + "alias": "serie alias", "tags": [ { "key": "datacenter", @@ -115,6 +116,7 @@ func TestInfluxdbQueryParser(t *testing.T) { So(len(res.Selects), ShouldEqual, 3) So(len(res.Tags), ShouldEqual, 2) So(res.Interval, ShouldEqual, ">20s") + So(res.Alias, ShouldEqual, "serie alias") }) Convey("can part raw query json model", func() { diff --git a/pkg/tsdb/influxdb/models.go b/pkg/tsdb/influxdb/models.go index 7903616ff22..44e05608290 100644 --- a/pkg/tsdb/influxdb/models.go +++ b/pkg/tsdb/influxdb/models.go @@ -8,6 +8,8 @@ type Query struct { GroupBy []*QueryPart Selects []*Select RawQuery string + UseRawQuery bool + Alias string Interval string } diff --git a/pkg/tsdb/influxdb/query_builder.go b/pkg/tsdb/influxdb/query.go similarity index 51% rename from pkg/tsdb/influxdb/query_builder.go rename to pkg/tsdb/influxdb/query.go index a4b122cbbd2..e95b54b0a18 100644 --- a/pkg/tsdb/influxdb/query_builder.go +++ b/pkg/tsdb/influxdb/query.go @@ -5,31 +5,36 @@ import ( "strconv" "strings" + "regexp" + "github.com/grafana/grafana/pkg/tsdb" ) -type QueryBuilder struct{} +var ( + regexpOperatorPattern *regexp.Regexp = regexp.MustCompile(`^\/.*\/$`) + regexpMeasurementPattern *regexp.Regexp = regexp.MustCompile(`^\/.*\/$`) +) -func (qb *QueryBuilder) Build(query *Query, queryContext *tsdb.QueryContext) (string, error) { - if query.RawQuery != "" { +func (query *Query) Build(queryContext *tsdb.QueryContext) (string, error) { + if query.UseRawQuery && query.RawQuery != "" { q := query.RawQuery - q = strings.Replace(q, "$timeFilter", qb.renderTimeFilter(query, queryContext), 1) + q = strings.Replace(q, "$timeFilter", query.renderTimeFilter(queryContext), 1) q = strings.Replace(q, "$interval", tsdb.CalculateInterval(queryContext.TimeRange), 1) return q, nil } - res := qb.renderSelectors(query, queryContext) - res += qb.renderMeasurement(query) - res += qb.renderWhereClause(query) - res += qb.renderTimeFilter(query, queryContext) - res += qb.renderGroupBy(query, queryContext) + res := query.renderSelectors(queryContext) + res += query.renderMeasurement() + res += query.renderWhereClause() + res += query.renderTimeFilter(queryContext) + res += query.renderGroupBy(queryContext) return res, nil } -func (qb *QueryBuilder) renderTags(query *Query) []string { +func (query *Query) renderTags() []string { var res []string for i, tag := range query.Tags { str := "" @@ -43,24 +48,34 @@ func (qb *QueryBuilder) renderTags(query *Query) []string { str += " " } - value := tag.Value - nValue, err := strconv.ParseFloat(tag.Value, 64) - - if tag.Operator == "=~" || tag.Operator == "!~" { - value = fmt.Sprintf("%s", value) - } else if err == nil { - value = fmt.Sprintf("%v", nValue) - } else { - value = fmt.Sprintf("'%s'", value) + //If the operator is missing we fall back to sensible defaults + if tag.Operator == "" { + if regexpOperatorPattern.Match([]byte(tag.Value)) { + tag.Operator = "=~" + } else { + tag.Operator = "=" + } } - res = append(res, fmt.Sprintf(`%s"%s" %s %s`, str, tag.Key, tag.Operator, value)) + textValue := "" + numericValue, err := strconv.ParseFloat(tag.Value, 64) + + // quote value unless regex or number + if tag.Operator == "=~" || tag.Operator == "!~" { + textValue = tag.Value + } else if err == nil { + textValue = fmt.Sprintf("%v", numericValue) + } else { + textValue = fmt.Sprintf("'%s'", tag.Value) + } + + res = append(res, fmt.Sprintf(`%s"%s" %s %s`, str, tag.Key, tag.Operator, textValue)) } return res } -func (qb *QueryBuilder) renderTimeFilter(query *Query, queryContext *tsdb.QueryContext) string { +func (query *Query) renderTimeFilter(queryContext *tsdb.QueryContext) string { from := "now() - " + queryContext.TimeRange.From to := "" @@ -71,7 +86,7 @@ func (qb *QueryBuilder) renderTimeFilter(query *Query, queryContext *tsdb.QueryC return fmt.Sprintf("time > %s%s", from, to) } -func (qb *QueryBuilder) renderSelectors(query *Query, queryContext *tsdb.QueryContext) string { +func (query *Query) renderSelectors(queryContext *tsdb.QueryContext) string { res := "SELECT " var selectors []string @@ -87,19 +102,26 @@ func (qb *QueryBuilder) renderSelectors(query *Query, queryContext *tsdb.QueryCo return res + strings.Join(selectors, ", ") } -func (qb *QueryBuilder) renderMeasurement(query *Query) string { +func (query *Query) renderMeasurement() string { policy := "" if query.Policy == "" || query.Policy == "default" { policy = "" } else { policy = `"` + query.Policy + `".` } - return fmt.Sprintf(` FROM %s"%s"`, policy, query.Measurement) + + measurement := query.Measurement + + if !regexpMeasurementPattern.Match([]byte(measurement)) { + measurement = fmt.Sprintf(`"%s"`, measurement) + } + + return fmt.Sprintf(` FROM %s%s`, policy, measurement) } -func (qb *QueryBuilder) renderWhereClause(query *Query) string { +func (query *Query) renderWhereClause() string { res := " WHERE " - conditions := qb.renderTags(query) + conditions := query.renderTags() res += strings.Join(conditions, " ") if len(conditions) > 0 { res += " AND " @@ -108,7 +130,7 @@ func (qb *QueryBuilder) renderWhereClause(query *Query) string { return res } -func (qb *QueryBuilder) renderGroupBy(query *Query, queryContext *tsdb.QueryContext) string { +func (query *Query) renderGroupBy(queryContext *tsdb.QueryContext) string { groupBy := "" for i, group := range query.GroupBy { if i == 0 { diff --git a/pkg/tsdb/influxdb/query_builder_test.go b/pkg/tsdb/influxdb/query_test.go similarity index 58% rename from pkg/tsdb/influxdb/query_builder_test.go rename to pkg/tsdb/influxdb/query_test.go index d13fe3a3c6f..ee045444c9b 100644 --- a/pkg/tsdb/influxdb/query_builder_test.go +++ b/pkg/tsdb/influxdb/query_test.go @@ -12,7 +12,6 @@ import ( func TestInfluxdbQueryBuilder(t *testing.T) { Convey("Influxdb query builder", t, func() { - builder := QueryBuilder{} qp1, _ := NewQueryPart("field", []string{"value"}) qp2, _ := NewQueryPart("mean", []string{}) @@ -37,7 +36,7 @@ func TestInfluxdbQueryBuilder(t *testing.T) { Interval: "10s", } - rawQuery, err := builder.Build(query, queryContext) + rawQuery, err := query.Build(queryContext) So(err, ShouldBeNil) So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "policy"."cpu" WHERE time > now() - 5m GROUP BY time(10s) fill(null)`) }) @@ -51,23 +50,22 @@ func TestInfluxdbQueryBuilder(t *testing.T) { Interval: "5s", } - rawQuery, err := builder.Build(query, queryContext) + rawQuery, err := query.Build(queryContext) So(err, ShouldBeNil) So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE "hostname" = 'server1' OR "hostname" = 'server2' AND time > now() - 5m GROUP BY time(5s), "datacenter" fill(null)`) }) Convey("can render time range", func() { query := Query{} - builder := &QueryBuilder{} Convey("render from: 2h to now-1h", func() { query := Query{} queryContext := &tsdb.QueryContext{TimeRange: tsdb.NewTimeRange("2h", "now-1h")} - So(builder.renderTimeFilter(&query, queryContext), ShouldEqual, "time > now() - 2h and time < now() - 1h") + So(query.renderTimeFilter(queryContext), ShouldEqual, "time > now() - 2h and time < now() - 1h") }) Convey("render from: 10m", func() { queryContext := &tsdb.QueryContext{TimeRange: tsdb.NewTimeRange("10m", "now")} - So(builder.renderTimeFilter(&query, queryContext), ShouldEqual, "time > now() - 10m") + So(query.renderTimeFilter(queryContext), ShouldEqual, "time > now() - 10m") }) }) @@ -79,29 +77,60 @@ func TestInfluxdbQueryBuilder(t *testing.T) { GroupBy: []*QueryPart{groupBy1, groupBy3}, Interval: "10s", RawQuery: "Raw query", + UseRawQuery: true, } - rawQuery, err := builder.Build(query, queryContext) + rawQuery, err := query.Build(queryContext) So(err, ShouldBeNil) So(rawQuery, ShouldEqual, `Raw query`) }) - Convey("can render regex tags", func() { - query := &Query{Tags: []*Tag{&Tag{Operator: "=~", Value: "value", Key: "key"}}} + Convey("can render normal tags without operator", func() { + query := &Query{Tags: []*Tag{&Tag{Operator: "", Value: `value`, Key: "key"}}} - So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" =~ value`) + So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = 'value'`) + }) + + Convey("can render regex tags without operator", func() { + query := &Query{Tags: []*Tag{&Tag{Operator: "", Value: `/value/`, Key: "key"}}} + + So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" =~ /value/`) + }) + + Convey("can render regex tags", func() { + query := &Query{Tags: []*Tag{&Tag{Operator: "=~", Value: `/value/`, Key: "key"}}} + + So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" =~ /value/`) }) Convey("can render number tags", func() { - query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "1", Key: "key"}}} + query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001", Key: "key"}}} - So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 1`) + So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = 10001`) + }) + + Convey("can render number tags with decimals", func() { + query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001.1", Key: "key"}}} + + So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = 10001.1`) }) Convey("can render string tags", func() { query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "value", Key: "key"}}} - So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 'value'`) + So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = 'value'`) + }) + + Convey("can render regular measurement", func() { + query := &Query{Measurement: `apa`, Policy: "policy"} + + So(query.renderMeasurement(), ShouldEqual, ` FROM "policy"."apa"`) + }) + + Convey("can render regexp measurement", func() { + query := &Query{Measurement: `/apa/`, Policy: "policy"} + + So(query.renderMeasurement(), ShouldEqual, ` FROM "policy"./apa/`) }) }) } diff --git a/pkg/tsdb/influxdb/response_parser.go b/pkg/tsdb/influxdb/response_parser.go index 44afa910b27..8e9fa0022c3 100644 --- a/pkg/tsdb/influxdb/response_parser.go +++ b/pkg/tsdb/influxdb/response_parser.go @@ -3,6 +3,8 @@ package influxdb import ( "encoding/json" "fmt" + "regexp" + "strconv" "strings" "github.com/grafana/grafana/pkg/tsdb" @@ -11,17 +13,25 @@ import ( type ResponseParser struct{} -func (rp *ResponseParser) Parse(response *Response) *tsdb.QueryResult { +var ( + legendFormat *regexp.Regexp +) + +func init() { + legendFormat = regexp.MustCompile(`\[\[(\w+?)*\]\]*|\$\s*(\w+?)*`) +} + +func (rp *ResponseParser) Parse(response *Response, query *Query) *tsdb.QueryResult { queryRes := tsdb.NewQueryResult() for _, result := range response.Results { - queryRes.Series = append(queryRes.Series, rp.transformRows(result.Series, queryRes)...) + queryRes.Series = append(queryRes.Series, rp.transformRows(result.Series, queryRes, query)...) } return queryRes } -func (rp *ResponseParser) transformRows(rows []Row, queryResult *tsdb.QueryResult) tsdb.TimeSeriesSlice { +func (rp *ResponseParser) transformRows(rows []Row, queryResult *tsdb.QueryResult, query *Query) tsdb.TimeSeriesSlice { var result tsdb.TimeSeriesSlice for _, row := range rows { @@ -38,7 +48,7 @@ func (rp *ResponseParser) transformRows(rows []Row, queryResult *tsdb.QueryResul } } result = append(result, &tsdb.TimeSeries{ - Name: rp.formatSerieName(row, column), + Name: rp.formatSerieName(row, column, query), Points: points, }) } @@ -47,7 +57,48 @@ func (rp *ResponseParser) transformRows(rows []Row, queryResult *tsdb.QueryResul return result } -func (rp *ResponseParser) formatSerieName(row Row, column string) string { +func (rp *ResponseParser) formatSerieName(row Row, column string, query *Query) string { + if query.Alias == "" { + return rp.buildSerieNameFromQuery(row, column) + } + + nameSegment := strings.Split(row.Name, ".") + + result := legendFormat.ReplaceAllFunc([]byte(query.Alias), func(in []byte) []byte { + aliasFormat := string(in) + aliasFormat = strings.Replace(aliasFormat, "[[", "", 1) + aliasFormat = strings.Replace(aliasFormat, "]]", "", 1) + aliasFormat = strings.Replace(aliasFormat, "$", "", 1) + + if aliasFormat == "m" || aliasFormat == "measurement" { + return []byte(query.Measurement) + } + if aliasFormat == "col" { + return []byte(column) + } + + pos, err := strconv.Atoi(aliasFormat) + if err == nil && len(nameSegment) >= pos { + return []byte(nameSegment[pos]) + } + + if !strings.HasPrefix(aliasFormat, "tag_") { + return in + } + + tagKey := strings.Replace(aliasFormat, "tag_", "", 1) + tagValue, exist := row.Tags[tagKey] + if exist { + return []byte(tagValue) + } + + return in + }) + + return string(result) +} + +func (rp *ResponseParser) buildSerieNameFromQuery(row Row, column string) string { var tags []string for k, v := range row.Tags { diff --git a/pkg/tsdb/influxdb/response_parser_test.go b/pkg/tsdb/influxdb/response_parser_test.go index b45f98a1fff..67667f565d5 100644 --- a/pkg/tsdb/influxdb/response_parser_test.go +++ b/pkg/tsdb/influxdb/response_parser_test.go @@ -4,56 +4,161 @@ import ( "encoding/json" "testing" + "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" ) func TestInfluxdbResponseParser(t *testing.T) { Convey("Influxdb response parser", t, func() { + Convey("Response parser", func() { + parser := &ResponseParser{} - parser := &ResponseParser{} + setting.NewConfigContext(&setting.CommandLineArgs{ + HomePath: "../../../", + }) - response := &Response{ - Results: []Result{ - Result{ - Series: []Row{ - { - Name: "cpu", - Columns: []string{"time", "mean", "sum"}, - Tags: map[string]string{"datacenter": "America"}, - Values: [][]interface{}{ - {json.Number("111"), json.Number("222"), json.Number("333")}, - {json.Number("111"), json.Number("222"), json.Number("333")}, - {json.Number("111"), json.Number("null"), json.Number("333")}, + response := &Response{ + Results: []Result{ + Result{ + Series: []Row{ + { + Name: "cpu", + Columns: []string{"time", "mean", "sum"}, + Tags: map[string]string{"datacenter": "America"}, + Values: [][]interface{}{ + {json.Number("111"), json.Number("222"), json.Number("333")}, + {json.Number("111"), json.Number("222"), json.Number("333")}, + {json.Number("111"), json.Number("null"), json.Number("333")}, + }, }, }, }, }, - }, - } + } - result := parser.Parse(response) + query := &Query{} - Convey("can parse all series", func() { - So(len(result.Series), ShouldEqual, 2) + result := parser.Parse(response, query) + + Convey("can parse all series", func() { + So(len(result.Series), ShouldEqual, 2) + }) + + Convey("can parse all points", func() { + So(len(result.Series[0].Points), ShouldEqual, 3) + So(len(result.Series[1].Points), ShouldEqual, 3) + }) + + Convey("can parse multi row result", func() { + So(result.Series[0].Points[1][0].Float64, ShouldEqual, float64(222)) + So(result.Series[1].Points[1][0].Float64, ShouldEqual, float64(333)) + }) + + Convey("can parse null points", func() { + So(result.Series[0].Points[2][0].Valid, ShouldBeFalse) + }) + + Convey("can format serie names", func() { + So(result.Series[0].Name, ShouldEqual, "cpu.mean { datacenter: America }") + So(result.Series[1].Name, ShouldEqual, "cpu.sum { datacenter: America }") + }) }) - Convey("can parse all points", func() { - So(len(result.Series[0].Points), ShouldEqual, 3) - So(len(result.Series[1].Points), ShouldEqual, 3) - }) + Convey("Response parser with alias", func() { + parser := &ResponseParser{} - Convey("can parse multi row result", func() { - So(result.Series[0].Points[1][0].Float64, ShouldEqual, float64(222)) - So(result.Series[1].Points[1][0].Float64, ShouldEqual, float64(333)) - }) + response := &Response{ + Results: []Result{ + Result{ + Series: []Row{ + { + Name: "cpu.upc", + Columns: []string{"time", "mean", "sum"}, + Tags: map[string]string{"datacenter": "America"}, + Values: [][]interface{}{ + {json.Number("111"), json.Number("222"), json.Number("333")}, + }, + }, + }, + }, + }, + } - Convey("can parse null points", func() { - So(result.Series[0].Points[2][0].Valid, ShouldBeFalse) - }) + Convey("$ alias", func() { + Convey("simple alias", func() { + query := &Query{Alias: "serie alias"} + result := parser.Parse(response, query) - Convey("can format serie names", func() { - So(result.Series[0].Name, ShouldEqual, "cpu.mean { datacenter: America }") - So(result.Series[1].Name, ShouldEqual, "cpu.sum { datacenter: America }") + So(result.Series[0].Name, ShouldEqual, "serie alias") + }) + + Convey("measurement alias", func() { + query := &Query{Alias: "alias $m $measurement", Measurement: "10m"} + result := parser.Parse(response, query) + + So(result.Series[0].Name, ShouldEqual, "alias 10m 10m") + }) + + Convey("column alias", func() { + query := &Query{Alias: "alias $col", Measurement: "10m"} + result := parser.Parse(response, query) + + So(result.Series[0].Name, ShouldEqual, "alias mean") + So(result.Series[1].Name, ShouldEqual, "alias sum") + }) + + Convey("tag alias", func() { + query := &Query{Alias: "alias $tag_datacenter"} + result := parser.Parse(response, query) + + So(result.Series[0].Name, ShouldEqual, "alias America") + }) + + Convey("segment alias", func() { + query := &Query{Alias: "alias $1"} + result := parser.Parse(response, query) + + So(result.Series[0].Name, ShouldEqual, "alias upc") + }) + + Convey("segment position out of bound", func() { + query := &Query{Alias: "alias $5"} + result := parser.Parse(response, query) + + So(result.Series[0].Name, ShouldEqual, "alias $5") + }) + }) + + Convey("[[]] alias", func() { + Convey("simple alias", func() { + query := &Query{Alias: "serie alias"} + result := parser.Parse(response, query) + + So(result.Series[0].Name, ShouldEqual, "serie alias") + }) + + Convey("measurement alias", func() { + query := &Query{Alias: "alias [[m]] [[measurement]]", Measurement: "10m"} + result := parser.Parse(response, query) + + So(result.Series[0].Name, ShouldEqual, "alias 10m 10m") + }) + + Convey("column alias", func() { + query := &Query{Alias: "alias [[col]]", Measurement: "10m"} + result := parser.Parse(response, query) + + So(result.Series[0].Name, ShouldEqual, "alias mean") + So(result.Series[1].Name, ShouldEqual, "alias sum") + }) + + Convey("tag alias", func() { + query := &Query{Alias: "alias [[tag_datacenter]]"} + result := parser.Parse(response, query) + + So(result.Series[0].Name, ShouldEqual, "alias America") + }) + }) }) }) } diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index ec5b098cbaf..21e1dfdef24 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -24,12 +24,14 @@ func NewPrometheusExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor { } var ( - plog log.Logger + plog log.Logger + legendFormat *regexp.Regexp ) func init() { plog = log.New("tsdb.prometheus") tsdb.RegisterExecutor("prometheus", NewPrometheusExecutor) + legendFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`) } func (e *PrometheusExecutor) getClient() (prometheus.QueryAPI, error) { @@ -79,13 +81,11 @@ func (e *PrometheusExecutor) Execute(ctx context.Context, queries tsdb.QuerySlic } func formatLegend(metric pmodel.Metric, query *PrometheusQuery) string { - reg, _ := regexp.Compile(`\{\{\s*(.+?)\s*\}\}`) - if query.LegendFormat == "" { return metric.String() } - result := reg.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte { + result := legendFormat.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte { labelName := strings.Replace(string(in), "{{", "", 1) labelName = strings.Replace(labelName, "}}", "", 1) labelName = strings.TrimSpace(labelName) diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 567df736a4e..8e9f64fd17f 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -10,7 +10,7 @@ import "./directives/grafana_version_check"; import "./directives/metric_segment"; import "./directives/misc"; import "./directives/ng_model_on_blur"; -import "./directives/password_strenght"; +import "./directives/password_strength"; import "./directives/spectrum_picker"; import "./directives/tags"; import "./directives/value_select_dropdown"; diff --git a/public/app/core/directives/password_strenght.js b/public/app/core/directives/password_strength.js similarity index 100% rename from public/app/core/directives/password_strenght.js rename to public/app/core/directives/password_strength.js diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index d672e0dd0dc..25a99a38ad8 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -102,6 +102,7 @@ export default class TimeSeries { this.stats.min = Number.MAX_VALUE; this.stats.avg = null; this.stats.current = null; + this.stats.timeStep = Number.MAX_VALUE; this.allIsNull = true; this.allIsZero = true; @@ -110,11 +111,22 @@ export default class TimeSeries { var currentTime; var currentValue; var nonNulls = 0; + var previousTime; for (var i = 0; i < this.datapoints.length; i++) { currentValue = this.datapoints[i][0]; currentTime = this.datapoints[i][1]; + // Due to missing values we could have different timeStep all along the series + // so we have to find the minimum one (could occur with aggregators such as ZimSum) + if (previousTime !== undefined) { + let timeStep = currentTime - previousTime; + if (timeStep < this.stats.timeStep) { + this.stats.timeStep = timeStep; + } + } + previousTime = currentTime; + if (currentValue === null) { if (ignoreNulls) { continue; } if (nullAsZero) { @@ -145,10 +157,6 @@ export default class TimeSeries { result.push([currentTime, currentValue]); } - if (this.datapoints.length >= 2) { - this.stats.timeStep = this.datapoints[1][1] - this.datapoints[0][1]; - } - if (this.stats.max === -Number.MAX_VALUE) { this.stats.max = null; } if (this.stats.min === Number.MAX_VALUE) { this.stats.min = null; } diff --git a/public/app/features/admin/partials/stats.html b/public/app/features/admin/partials/stats.html index bcc246b9e82..92e32f4f947 100644 --- a/public/app/features/admin/partials/stats.html +++ b/public/app/features/admin/partials/stats.html @@ -46,6 +46,10 @@ Total starred dashboards {{ctrl.stats.starred_db_count}} + + Total alerts + {{ctrl.stats.alert_count}} + diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js index fde9e8f09ea..105b08c2b17 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.js +++ b/public/app/plugins/datasource/cloudwatch/datasource.js @@ -78,10 +78,10 @@ function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) { } else { period = kbn.interval_to_seconds(templateSrv.replace(target.period, options.scopedVars)); } - if (query.period < 60) { + if (period < 60) { period = 60; } - if (range / query.period >= 1440) { + if (range / period >= 1440) { period = Math.ceil(range / 1440 / 60) * 60; } diff --git a/public/app/plugins/datasource/influxdb/partials/query.options.html b/public/app/plugins/datasource/influxdb/partials/query.options.html index feb8600fcc9..2dd6b4d376b 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.options.html +++ b/public/app/plugins/datasource/influxdb/partials/query.options.html @@ -4,16 +4,15 @@
Group by time interval - - + + Set a low limit by having a greater sign: example: >60s +
- diff --git a/public/app/plugins/panel/alertlist/editor.html b/public/app/plugins/panel/alertlist/editor.html index b2038df34b8..344af7408fc 100644 --- a/public/app/plugins/panel/alertlist/editor.html +++ b/public/app/plugins/panel/alertlist/editor.html @@ -11,6 +11,7 @@ Max items
+
State filter
diff --git a/public/app/plugins/panel/alertlist/module.ts b/public/app/plugins/panel/alertlist/module.ts index 543c5f04833..d03fad49073 100644 --- a/public/app/plugins/panel/alertlist/module.ts +++ b/public/app/plugins/panel/alertlist/module.ts @@ -25,7 +25,8 @@ class AlertListPanel extends PanelCtrl { panelDefaults = { show: 'current', limit: 10, - stateFilter: [] + stateFilter: [], + onlyAlertsOnDashboard: false }; @@ -71,9 +72,13 @@ class AlertListPanel extends PanelCtrl { var params: any = { limit: this.panel.limit, type: 'alert', - newState: this.panel.stateFilter + newState: this.panel.stateFilter, }; + if (this.panel.onlyAlertsOnDashboard) { + params.dashboardId = this.dashboard.id; + } + params.from = dateMath.parse(this.dashboard.time.from).unix() * 1000; params.to = dateMath.parse(this.dashboard.time.to).unix() * 1000; @@ -93,6 +98,10 @@ class AlertListPanel extends PanelCtrl { state: this.panel.stateFilter }; + if (this.panel.onlyAlertsOnDashboard) { + params.dashboardId = this.dashboard.id; + } + this.backendSrv.get(`/api/alerts`, params) .then(res => { this.currentAlerts = _.map(res, al => { diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 239bf9ecc83..73fe37d5cad 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -183,8 +183,10 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { } } + // Series could have different timeSteps, + // let's find the smallest one so that bars are correctly rendered. function getMinTimeStepOfSeries(data) { - var min = 100000000000; + var min = Number.MAX_VALUE; for (let i = 0; i < data.length; i++) { if (!data[i].stats.timeStep) { @@ -295,9 +297,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { break; } default: { - if (panel.bars) { - options.series.bars.barWidth = getMinTimeStepOfSeries(data) / 1.5; - } + options.series.bars.barWidth = getMinTimeStepOfSeries(data) / 1.5; addTimeAxis(options); break; } @@ -460,7 +460,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { show: panel.yaxes[0].show, index: 1, logBase: panel.yaxes[0].logBase || 1, - max: 100, // correct later + max: null }; options.yaxes.push(defaults); @@ -472,6 +472,8 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { secondY.logBase = panel.yaxes[1].logBase || 1; secondY.position = 'right'; options.yaxes.push(secondY); + + applyLogScale(options.yaxes[1], data); configureAxisMode(options.yaxes[1], panel.percentage && panel.stack ? "percent" : panel.yaxes[1].format); } diff --git a/public/app/plugins/panel/graph/graph_tooltip.js b/public/app/plugins/panel/graph/graph_tooltip.js index 697dce0b7ee..40ed293ff4e 100644 --- a/public/app/plugins/panel/graph/graph_tooltip.js +++ b/public/app/plugins/panel/graph/graph_tooltip.js @@ -21,7 +21,9 @@ function ($) { var initial = last*ps; var len = series.datapoints.points.length; for (var j = initial; j < len; j += ps) { + // Special case of a non stepped line, highlight the very last point just before a null point if ((series.datapoints.points[initial] != null && series.datapoints.points[j] == null && ! series.lines.steps) + //normal case || series.datapoints.points[j] > posX) { return Math.max(j - ps, 0)/ps; } @@ -58,11 +60,13 @@ function ($) { series = seriesList[i]; if (!series.data.length || (panel.legend.hideEmpty && series.allIsNull)) { + // Init value & yaxis so that it does not brake series sorting results.push({ hidden: true, value: 0, yaxis: 0 }); continue; } if (!series.data.length || (panel.legend.hideZero && series.allIsZero)) { + // Init value & yaxis so that it does not brake series sorting results.push({ hidden: true, value: 0, yaxis: 0 }); continue; } @@ -71,6 +75,7 @@ function ($) { hoverDistance = pos.x - series.data[hoverIndex][0]; pointTime = series.data[hoverIndex][0]; + // Take the closest point before the cursor, or if it does not exist, the closest after if (! minDistance || (hoverDistance >=0 && (hoverDistance < minDistance || minDistance < 0)) || (hoverDistance < 0 && hoverDistance > minDistance)) { @@ -99,6 +104,7 @@ function ($) { hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex); } + // Be sure we have a yaxis so that it does not brake series sorting yaxis = 0; if (series.yaxis) { yaxis = series.yaxis.n; @@ -116,7 +122,7 @@ function ($) { }); } - // Find point which closer to pointer + // Time of the point closer to pointer results.time = minTime; return results; @@ -189,7 +195,7 @@ function ($) { } var highlightClass = ''; - if (item && i === item.seriesIndex) { + if (item && hoverInfo.index === item.seriesIndex) { highlightClass = 'graph-tooltip-list-item--highlight'; } diff --git a/public/sass/mixins/_mixins.scss b/public/sass/mixins/_mixins.scss index d6f1cf2afc1..78f9d3fca08 100644 --- a/public/sass/mixins/_mixins.scss +++ b/public/sass/mixins/_mixins.scss @@ -335,21 +335,24 @@ } @mixin left-brand-border-gradient() { + border: none; border-image: linear-gradient(rgba(255,213,0,1) 0%, rgba(255,68,0,1) 99%, rgba(255,68,0,1) 100%); border-image-slice: 1; + border-style: solid; border-top: 0; border-right: 0; border-bottom: 0; - border-left: 2px solid transparent; + border-left-width: 2px; } @mixin brand-bottom-border() { border-image: $brand-gradient; border-image-slice: 1; + border-style: solid; border-top: 0; border-right: 0; border-left: 0; - border-bottom: 1px solid transparent; + border-bottom-width: 1px; } diff --git a/public/vendor/flot/jquery.flot.js b/public/vendor/flot/jquery.flot.js index e0941bb24c0..8d61b3e9b28 100644 --- a/public/vendor/flot/jquery.flot.js +++ b/public/vendor/flot/jquery.flot.js @@ -1210,7 +1210,7 @@ Licensed under the MIT license. // middle point has same y points[k + 1] = points[k - ps + 1] || 0; - // if series has null values, let's give the last correct value a nice step + // if series has null values, let's give the last !null value a nice step if(nullify) points[k] = p[0]; diff --git a/public/vendor/flot/jquery.flot.stack.js b/public/vendor/flot/jquery.flot.stack.js index ba2a6b86a2f..5367ff30603 100644 --- a/public/vendor/flot/jquery.flot.stack.js +++ b/public/vendor/flot/jquery.flot.stack.js @@ -78,9 +78,12 @@ charts or filled areas). i = 0, j = 0, l, m; while (true) { + // browse all points from the current series and from the previous series if (i >= points.length && j >= otherpoints.length) break; + // newpoints will replace current series with + // as many points as different timestamps we have in the 2 (current & previous) series l = newpoints.length; px = points[i + keyOffset]; py = points[i + accumulateOffset]; @@ -89,30 +92,32 @@ charts or filled areas). bottom = 0; if (i < points.length && px == null) { - // ignore point + // let's ignore null points from current series, nothing to do with them i += ps; } else if (j < otherpoints.length && qx == null) { - // ignore point + // let's ignore null points from previous series, nothing to do with them j += otherps; } else if (i >= points.length) { - // take the remaining points from the previous series + // no more points in the current series, simply take the remaining points + // from the previous series so that next series will correctly stack for (m = 0; m < ps; ++m) newpoints.push(otherpoints[j + m]); bottom = qy; j += otherps; } else if (j >= otherpoints.length) { - // take the remaining points from the current series + // no more points in the previous series, of course let's take + // the remaining points from the current series for (m = 0; m < ps; ++m) newpoints.push(points[i + m]); i += ps; } else { - // cases where we actually got two points + // next available points from current and previous series have the same timestamp if (px == qx) { - // take the point from the current series and skip the previous' one + // so take the point from the current series and skip the previous' one for (m = 0; m < ps; ++m) newpoints.push(points[i + m]); @@ -122,8 +127,9 @@ charts or filled areas). i += ps; j += otherps; } + // next available point with the smallest timestamp is from the previous series else if (px > qx) { - // take the point from the previous series so that the next series can stack over it + // so take the point from the previous series so that next series will correctly stack for (m = 0; m < ps; ++m) newpoints.push(otherpoints[j + m]); @@ -135,8 +141,9 @@ charts or filled areas). j += otherps; } - else { // px < qx - // take the point from the current series + // (px < qx) next available point with the smallest timestamp is from the current series + else { + // so of course let's take the point from the current series for (m = 0; m < ps; ++m) newpoints.push(points[i + m]);