diff --git a/package.json b/package.json
index 3f3d67ca691..17817d60ef8 100644
--- a/package.json
+++ b/package.json
@@ -65,7 +65,7 @@
"karma-sinon": "^1.0.5",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^2.0.4",
- "lint-staged": "^4.2.3",
+ "lint-staged": "^6.0.0",
"load-grunt-tasks": "3.5.2",
"mocha": "^4.0.1",
"ng-annotate-loader": "^0.6.1",
@@ -103,7 +103,17 @@
"lint": "tslint -c tslint.json --project tsconfig.json --type-check",
"karma": "node ./node_modules/grunt-cli/bin/grunt karma:dev",
"jest": "node ./node_modules/jest-cli/bin/jest.js --notify --watch",
- "precommit": "node ./node_modules/grunt-cli/bin/grunt precommit"
+ "precommit": "lint-staged && node ./node_modules/grunt-cli/bin/grunt precommit"
+ },
+ "lint-staged": {
+ "*.{ts,tsx}": [
+ "prettier --write",
+ "git add"
+ ],
+ "*.scss": [
+ "prettier --write",
+ "git add"
+ ]
},
"license": "Apache-2.0",
"dependencies": {
diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go
index b8579ffdc6a..a609824cbc8 100644
--- a/pkg/services/alerting/extractor.go
+++ b/pkg/services/alerting/extractor.go
@@ -69,6 +69,90 @@ func copyJson(in *simplejson.Json) (*simplejson.Json, error) {
return simplejson.NewJson(rawJson)
}
+func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json) ([]*m.Alert, error) {
+ alerts := make([]*m.Alert, 0)
+
+ for _, panelObj := range jsonWithPanels.Get("panels").MustArray() {
+ panel := simplejson.NewFromAny(panelObj)
+ jsonAlert, hasAlert := panel.CheckGet("alert")
+
+ if !hasAlert {
+ continue
+ }
+
+ panelId, err := panel.Get("id").Int64()
+ if err != nil {
+ return nil, fmt.Errorf("panel id is required. err %v", err)
+ }
+
+ // backward compatibility check, can be removed later
+ enabled, hasEnabled := jsonAlert.CheckGet("enabled")
+ if hasEnabled && enabled.MustBool() == false {
+ continue
+ }
+
+ frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString())
+ if err != nil {
+ return nil, ValidationError{Reason: "Could not parse frequency"}
+ }
+
+ alert := &m.Alert{
+ DashboardId: e.Dash.Id,
+ OrgId: e.OrgId,
+ PanelId: panelId,
+ Id: jsonAlert.Get("id").MustInt64(),
+ Name: jsonAlert.Get("name").MustString(),
+ Handler: jsonAlert.Get("handler").MustInt64(),
+ Message: jsonAlert.Get("message").MustString(),
+ Frequency: frequency,
+ }
+
+ for _, condition := range jsonAlert.Get("conditions").MustArray() {
+ jsonCondition := simplejson.NewFromAny(condition)
+
+ jsonQuery := jsonCondition.Get("query")
+ queryRefId := jsonQuery.Get("params").MustArray()[0].(string)
+ panelQuery := findPanelQueryByRefId(panel, queryRefId)
+
+ if panelQuery == nil {
+ reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefId)
+ return nil, ValidationError{Reason: reason}
+ }
+
+ dsName := ""
+ if panelQuery.Get("datasource").MustString() != "" {
+ dsName = panelQuery.Get("datasource").MustString()
+ } else if panel.Get("datasource").MustString() != "" {
+ dsName = panel.Get("datasource").MustString()
+ }
+
+ if datasource, err := e.lookupDatasourceId(dsName); err != nil {
+ return nil, err
+ } else {
+ jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
+ }
+
+ if interval, err := panel.Get("interval").String(); err == nil {
+ panelQuery.Set("interval", interval)
+ }
+
+ jsonQuery.Set("model", panelQuery.Interface())
+ }
+
+ alert.Settings = jsonAlert
+
+ // validate
+ _, err = NewRuleFromDBAlert(alert)
+ if err == nil && alert.ValidToSave() {
+ alerts = append(alerts, alert)
+ } else {
+ return nil, err
+ }
+ }
+
+ return alerts, nil
+}
+
func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
e.log.Debug("GetAlerts")
@@ -78,86 +162,27 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
}
alerts := make([]*m.Alert, 0)
- for _, rowObj := range dashboardJson.Get("rows").MustArray() {
- row := simplejson.NewFromAny(rowObj)
- for _, panelObj := range row.Get("panels").MustArray() {
- panel := simplejson.NewFromAny(panelObj)
- jsonAlert, hasAlert := panel.CheckGet("alert")
-
- if !hasAlert {
- continue
- }
-
- panelId, err := panel.Get("id").Int64()
+ // We extract alerts from rows to be backwards compatible
+ // with the old dashboard json model.
+ rows := dashboardJson.Get("rows").MustArray()
+ if len(rows) > 0 {
+ for _, rowObj := range rows {
+ row := simplejson.NewFromAny(rowObj)
+ a, err := e.GetAlertFromPanels(row)
if err != nil {
- return nil, fmt.Errorf("panel id is required. err %v", err)
- }
-
- // backward compatibility check, can be removed later
- enabled, hasEnabled := jsonAlert.CheckGet("enabled")
- if hasEnabled && enabled.MustBool() == false {
- continue
- }
-
- frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString())
- if err != nil {
- return nil, ValidationError{Reason: "Could not parse frequency"}
- }
-
- alert := &m.Alert{
- DashboardId: e.Dash.Id,
- OrgId: e.OrgId,
- PanelId: panelId,
- Id: jsonAlert.Get("id").MustInt64(),
- Name: jsonAlert.Get("name").MustString(),
- Handler: jsonAlert.Get("handler").MustInt64(),
- Message: jsonAlert.Get("message").MustString(),
- Frequency: frequency,
- }
-
- for _, condition := range jsonAlert.Get("conditions").MustArray() {
- jsonCondition := simplejson.NewFromAny(condition)
-
- jsonQuery := jsonCondition.Get("query")
- queryRefId := jsonQuery.Get("params").MustArray()[0].(string)
- panelQuery := findPanelQueryByRefId(panel, queryRefId)
-
- if panelQuery == nil {
- reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefId)
- return nil, ValidationError{Reason: reason}
- }
-
- dsName := ""
- if panelQuery.Get("datasource").MustString() != "" {
- dsName = panelQuery.Get("datasource").MustString()
- } else if panel.Get("datasource").MustString() != "" {
- dsName = panel.Get("datasource").MustString()
- }
-
- if datasource, err := e.lookupDatasourceId(dsName); err != nil {
- return nil, err
- } else {
- jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
- }
-
- if interval, err := panel.Get("interval").String(); err == nil {
- panelQuery.Set("interval", interval)
- }
-
- jsonQuery.Set("model", panelQuery.Interface())
- }
-
- alert.Settings = jsonAlert
-
- // validate
- _, err = NewRuleFromDBAlert(alert)
- if err == nil && alert.ValidToSave() {
- alerts = append(alerts, alert)
- } else {
return nil, err
}
+
+ alerts = append(alerts, a...)
}
+ } else {
+ a, err := e.GetAlertFromPanels(dashboardJson)
+ if err != nil {
+ return nil, err
+ }
+
+ alerts = append(alerts, a...)
}
e.log.Debug("Extracted alerts from dashboard", "alertCount", len(alerts))
diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go
index 68054fab5ef..71f3026025d 100644
--- a/pkg/services/alerting/extractor_test.go
+++ b/pkg/services/alerting/extractor_test.go
@@ -1,12 +1,12 @@
package alerting
import (
+ "io/ioutil"
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
- "github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
@@ -18,10 +18,6 @@ func TestAlertRuleExtraction(t *testing.T) {
return &FakeCondition{}, nil
})
- setting.NewConfigContext(&setting.CommandLineArgs{
- HomePath: "../../../",
- })
-
// mock data
defaultDs := &m.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true}
graphite2Ds := &m.DataSource{Id: 15, OrgId: 1, Name: "graphite2"}
@@ -45,70 +41,8 @@ func TestAlertRuleExtraction(t *testing.T) {
return nil
})
- json := `
- {
- "id": 57,
- "title": "Graphite 4",
- "originalTitle": "Graphite 4",
- "tags": ["graphite"],
- "rows": [
- {
- "panels": [
- {
- "title": "Active desktop users",
- "editable": true,
- "type": "graph",
- "id": 3,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
- }
- ],
- "datasource": null,
- "alert": {
- "name": "name1",
- "message": "desc1",
- "handler": 1,
- "frequency": "60s",
- "conditions": [
- {
- "type": "query",
- "query": {"params": ["A", "5m", "now"]},
- "reducer": {"type": "avg", "params": []},
- "evaluator": {"type": ">", "params": [100]}
- }
- ]
- }
- },
- {
- "title": "Active mobile users",
- "id": 4,
- "targets": [
- {"refId": "A", "target": ""},
- {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
- ],
- "datasource": "graphite2",
- "alert": {
- "name": "name2",
- "message": "desc2",
- "handler": 0,
- "frequency": "60s",
- "severity": "warning",
- "conditions": [
- {
- "type": "query",
- "query": {"params": ["B", "5m", "now"]},
- "reducer": {"type": "avg", "params": []},
- "evaluator": {"type": ">", "params": [100]}
- }
- ]
- }
- }
- ]
- }
- ]
- }`
+ json, err := ioutil.ReadFile("./test-data/graphite-alert.json")
+ So(err, ShouldBeNil)
Convey("Extractor should not modify the original json", func() {
dashJson, err := simplejson.NewJson([]byte(json))
@@ -201,69 +135,8 @@ func TestAlertRuleExtraction(t *testing.T) {
})
Convey("Panels missing id should return error", func() {
- panelWithoutId := `
- {
- "id": 57,
- "title": "Graphite 4",
- "originalTitle": "Graphite 4",
- "tags": ["graphite"],
- "rows": [
- {
- "panels": [
- {
- "title": "Active desktop users",
- "editable": true,
- "type": "graph",
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
- }
- ],
- "datasource": null,
- "alert": {
- "name": "name1",
- "message": "desc1",
- "handler": 1,
- "frequency": "60s",
- "conditions": [
- {
- "type": "query",
- "query": {"params": ["A", "5m", "now"]},
- "reducer": {"type": "avg", "params": []},
- "evaluator": {"type": ">", "params": [100]}
- }
- ]
- }
- },
- {
- "title": "Active mobile users",
- "id": 4,
- "targets": [
- {"refId": "A", "target": ""},
- {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
- ],
- "datasource": "graphite2",
- "alert": {
- "name": "name2",
- "message": "desc2",
- "handler": 0,
- "frequency": "60s",
- "severity": "warning",
- "conditions": [
- {
- "type": "query",
- "query": {"params": ["B", "5m", "now"]},
- "reducer": {"type": "avg", "params": []},
- "evaluator": {"type": ">", "params": [100]}
- }
- ]
- }
- }
- ]
- }
- ]
- }`
+ panelWithoutId, err := ioutil.ReadFile("./test-data/panels-missing-id.json")
+ So(err, ShouldBeNil)
dashJson, err := simplejson.NewJson([]byte(panelWithoutId))
So(err, ShouldBeNil)
@@ -277,292 +150,31 @@ func TestAlertRuleExtraction(t *testing.T) {
})
})
+ Convey("Parse alerts from dashboard without rows", func() {
+ json, err := ioutil.ReadFile("./test-data/v5-dashboard.json")
+ So(err, ShouldBeNil)
+
+ dashJson, err := simplejson.NewJson(json)
+ So(err, ShouldBeNil)
+ dash := m.NewDashboardFromJson(dashJson)
+ extractor := NewDashAlertExtractor(dash, 1)
+
+ alerts, err := extractor.GetAlerts()
+
+ Convey("Get rules without error", func() {
+ So(err, ShouldBeNil)
+ })
+
+ Convey("Should have 2 alert rule", func() {
+ So(len(alerts), ShouldEqual, 2)
+ })
+ })
+
Convey("Parse and validate dashboard containing influxdb alert", func() {
+ json, err := ioutil.ReadFile("./test-data/influxdb-alert.json")
+ So(err, ShouldBeNil)
- json2 := `{
- "id": 4,
- "title": "Influxdb",
- "tags": [
- "apa"
- ],
- "style": "dark",
- "timezone": "browser",
- "editable": true,
- "hideControls": false,
- "sharedCrosshair": false,
- "rows": [
- {
- "collapse": false,
- "editable": true,
- "height": "450px",
- "panels": [
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 10
- ],
- "type": "gt"
- },
- "query": {
- "params": [
- "B",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "avg"
- },
- "type": "query"
- }
- ],
- "frequency": "3s",
- "handler": 1,
- "name": "Influxdb",
- "noDataState": "no_data",
- "notifications": [
- {
- "id": 6
- }
- ]
- },
- "alerting": {},
- "aliasColors": {
- "logins.count.count": "#890F02"
- },
- "bars": false,
- "datasource": "InfluxDB",
- "editable": true,
- "error": false,
- "fill": 1,
- "grid": {},
- "id": 1,
- "interval": ">10s",
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 10,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "groupBy": [
- {
- "params": [
- "$interval"
- ],
- "type": "time"
- },
- {
- "params": [
- "datacenter"
- ],
- "type": "tag"
- },
- {
- "params": [
- "none"
- ],
- "type": "fill"
- }
- ],
- "hide": false,
- "measurement": "logins.count",
- "policy": "default",
- "query": "SELECT 8 * count(\"value\") FROM \"logins.count\" WHERE $timeFilter GROUP BY time($interval), \"datacenter\" fill(none)",
- "rawQuery": true,
- "refId": "B",
- "resultFormat": "time_series",
- "select": [
- [
- {
- "params": [
- "value"
- ],
- "type": "field"
- },
- {
- "params": [],
- "type": "count"
- }
- ]
- ],
- "tags": []
- },
- {
- "groupBy": [
- {
- "params": [
- "$interval"
- ],
- "type": "time"
- },
- {
- "params": [
- "null"
- ],
- "type": "fill"
- }
- ],
- "hide": true,
- "measurement": "cpu",
- "policy": "default",
- "refId": "A",
- "resultFormat": "time_series",
- "select": [
- [
- {
- "params": [
- "value"
- ],
- "type": "field"
- },
- {
- "params": [],
- "type": "mean"
- }
- ],
- [
- {
- "params": [
- "value"
- ],
- "type": "field"
- },
- {
- "params": [],
- "type": "sum"
- }
- ]
- ],
- "tags": []
- }
- ],
- "thresholds": [
- {
- "colorMode": "critical",
- "fill": true,
- "line": true,
- "op": "gt",
- "value": 10
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Panel Title",
- "tooltip": {
- "msResolution": false,
- "ordering": "alphabetical",
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "mode": "time",
- "name": null,
- "show": true,
- "values": []
- },
- "yaxes": [
- {
- "format": "short",
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "editable": true,
- "error": false,
- "id": 2,
- "isNew": true,
- "limit": 10,
- "links": [],
- "show": "current",
- "span": 2,
- "stateFilter": [
- "alerting"
- ],
- "title": "Alert status",
- "type": "alertlist"
- }
- ],
- "title": "Row"
- }
- ],
- "time": {
- "from": "now-5m",
- "to": "now"
- },
- "timepicker": {
- "now": true,
- "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": []
- },
- "schemaVersion": 13,
- "version": 120,
- "links": [],
- "gnetId": null
- }`
-
- dashJson, err := simplejson.NewJson([]byte(json2))
+ dashJson, err := simplejson.NewJson(json)
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson)
extractor := NewDashAlertExtractor(dash, 1)
diff --git a/pkg/services/alerting/test-data/graphite-alert.json b/pkg/services/alerting/test-data/graphite-alert.json
new file mode 100644
index 00000000000..5f23e224f9a
--- /dev/null
+++ b/pkg/services/alerting/test-data/graphite-alert.json
@@ -0,0 +1,63 @@
+{
+ "id": 57,
+ "title": "Graphite 4",
+ "originalTitle": "Graphite 4",
+ "tags": ["graphite"],
+ "rows": [
+ {
+ "panels": [
+ {
+ "title": "Active desktop users",
+ "editable": true,
+ "type": "graph",
+ "id": 3,
+ "targets": [
+ {
+ "refId": "A",
+ "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
+ }
+ ],
+ "datasource": null,
+ "alert": {
+ "name": "name1",
+ "message": "desc1",
+ "handler": 1,
+ "frequency": "60s",
+ "conditions": [
+ {
+ "type": "query",
+ "query": {"params": ["A", "5m", "now"]},
+ "reducer": {"type": "avg", "params": []},
+ "evaluator": {"type": ">", "params": [100]}
+ }
+ ]
+ }
+ },
+ {
+ "title": "Active mobile users",
+ "id": 4,
+ "targets": [
+ {"refId": "A", "target": ""},
+ {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
+ ],
+ "datasource": "graphite2",
+ "alert": {
+ "name": "name2",
+ "message": "desc2",
+ "handler": 0,
+ "frequency": "60s",
+ "severity": "warning",
+ "conditions": [
+ {
+ "type": "query",
+ "query": {"params": ["B", "5m", "now"]},
+ "reducer": {"type": "avg", "params": []},
+ "evaluator": {"type": ">", "params": [100]}
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/pkg/services/alerting/test-data/influxdb-alert.json b/pkg/services/alerting/test-data/influxdb-alert.json
new file mode 100644
index 00000000000..79ca355c5a1
--- /dev/null
+++ b/pkg/services/alerting/test-data/influxdb-alert.json
@@ -0,0 +1,282 @@
+{
+ "id": 4,
+ "title": "Influxdb",
+ "tags": [
+ "apa"
+ ],
+ "style": "dark",
+ "timezone": "browser",
+ "editable": true,
+ "hideControls": false,
+ "sharedCrosshair": false,
+ "rows": [
+ {
+ "collapse": false,
+ "editable": true,
+ "height": "450px",
+ "panels": [
+ {
+ "alert": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [
+ 10
+ ],
+ "type": "gt"
+ },
+ "query": {
+ "params": [
+ "B",
+ "5m",
+ "now"
+ ]
+ },
+ "reducer": {
+ "params": [],
+ "type": "avg"
+ },
+ "type": "query"
+ }
+ ],
+ "frequency": "3s",
+ "handler": 1,
+ "name": "Influxdb",
+ "noDataState": "no_data",
+ "notifications": [
+ {
+ "id": 6
+ }
+ ]
+ },
+ "alerting": {},
+ "aliasColors": {
+ "logins.count.count": "#890F02"
+ },
+ "bars": false,
+ "datasource": "InfluxDB",
+ "editable": true,
+ "error": false,
+ "fill": 1,
+ "grid": {},
+ "id": 1,
+ "interval": ">10s",
+ "isNew": true,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "connected",
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "span": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "groupBy": [
+ {
+ "params": [
+ "$interval"
+ ],
+ "type": "time"
+ },
+ {
+ "params": [
+ "datacenter"
+ ],
+ "type": "tag"
+ },
+ {
+ "params": [
+ "none"
+ ],
+ "type": "fill"
+ }
+ ],
+ "hide": false,
+ "measurement": "logins.count",
+ "policy": "default",
+ "query": "SELECT 8 * count(\"value\") FROM \"logins.count\" WHERE $timeFilter GROUP BY time($interval), \"datacenter\" fill(none)",
+ "rawQuery": true,
+ "refId": "B",
+ "resultFormat": "time_series",
+ "select": [
+ [
+ {
+ "params": [
+ "value"
+ ],
+ "type": "field"
+ },
+ {
+ "params": [],
+ "type": "count"
+ }
+ ]
+ ],
+ "tags": []
+ },
+ {
+ "groupBy": [
+ {
+ "params": [
+ "$interval"
+ ],
+ "type": "time"
+ },
+ {
+ "params": [
+ "null"
+ ],
+ "type": "fill"
+ }
+ ],
+ "hide": true,
+ "measurement": "cpu",
+ "policy": "default",
+ "refId": "A",
+ "resultFormat": "time_series",
+ "select": [
+ [
+ {
+ "params": [
+ "value"
+ ],
+ "type": "field"
+ },
+ {
+ "params": [],
+ "type": "mean"
+ }
+ ],
+ [
+ {
+ "params": [
+ "value"
+ ],
+ "type": "field"
+ },
+ {
+ "params": [],
+ "type": "sum"
+ }
+ ]
+ ],
+ "tags": []
+ }
+ ],
+ "thresholds": [
+ {
+ "colorMode": "critical",
+ "fill": true,
+ "line": true,
+ "op": "gt",
+ "value": 10
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Panel Title",
+ "tooltip": {
+ "msResolution": false,
+ "ordering": "alphabetical",
+ "shared": true,
+ "sort": 0,
+ "value_type": "cumulative"
+ },
+ "type": "graph",
+ "xaxis": {
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ },
+ {
+ "format": "short",
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ]
+ },
+ {
+ "editable": true,
+ "error": false,
+ "id": 2,
+ "isNew": true,
+ "limit": 10,
+ "links": [],
+ "show": "current",
+ "span": 2,
+ "stateFilter": [
+ "alerting"
+ ],
+ "title": "Alert status",
+ "type": "alertlist"
+ }
+ ],
+ "title": "Row"
+ }
+ ],
+ "time": {
+ "from": "now-5m",
+ "to": "now"
+ },
+ "timepicker": {
+ "now": true,
+ "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": []
+ },
+ "schemaVersion": 13,
+ "version": 120,
+ "links": [],
+ "gnetId": null
+ }
\ No newline at end of file
diff --git a/pkg/services/alerting/test-data/panels-missing-id.json b/pkg/services/alerting/test-data/panels-missing-id.json
new file mode 100644
index 00000000000..dad96a18dc1
--- /dev/null
+++ b/pkg/services/alerting/test-data/panels-missing-id.json
@@ -0,0 +1,62 @@
+{
+ "id": 57,
+ "title": "Graphite 4",
+ "originalTitle": "Graphite 4",
+ "tags": ["graphite"],
+ "rows": [
+ {
+ "panels": [
+ {
+ "title": "Active desktop users",
+ "editable": true,
+ "type": "graph",
+ "targets": [
+ {
+ "refId": "A",
+ "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
+ }
+ ],
+ "datasource": null,
+ "alert": {
+ "name": "name1",
+ "message": "desc1",
+ "handler": 1,
+ "frequency": "60s",
+ "conditions": [
+ {
+ "type": "query",
+ "query": {"params": ["A", "5m", "now"]},
+ "reducer": {"type": "avg", "params": []},
+ "evaluator": {"type": ">", "params": [100]}
+ }
+ ]
+ }
+ },
+ {
+ "title": "Active mobile users",
+ "id": 4,
+ "targets": [
+ {"refId": "A", "target": ""},
+ {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
+ ],
+ "datasource": "graphite2",
+ "alert": {
+ "name": "name2",
+ "message": "desc2",
+ "handler": 0,
+ "frequency": "60s",
+ "severity": "warning",
+ "conditions": [
+ {
+ "type": "query",
+ "query": {"params": ["B", "5m", "now"]},
+ "reducer": {"type": "avg", "params": []},
+ "evaluator": {"type": ">", "params": [100]}
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/pkg/services/alerting/test-data/v5-dashboard.json b/pkg/services/alerting/test-data/v5-dashboard.json
new file mode 100644
index 00000000000..da7bbd8d048
--- /dev/null
+++ b/pkg/services/alerting/test-data/v5-dashboard.json
@@ -0,0 +1,60 @@
+{
+ "id": 57,
+ "title": "Graphite 4",
+ "originalTitle": "Graphite 4",
+ "tags": ["graphite"],
+ "panels": [
+ {
+ "title": "Active desktop users",
+ "editable": true,
+ "type": "graph",
+ "id": 3,
+ "targets": [
+ {
+ "refId": "A",
+ "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
+ }
+ ],
+ "datasource": null,
+ "alert": {
+ "name": "name1",
+ "message": "desc1",
+ "handler": 1,
+ "frequency": "60s",
+ "conditions": [
+ {
+ "type": "query",
+ "query": {"params": ["A", "5m", "now"]},
+ "reducer": {"type": "avg", "params": []},
+ "evaluator": {"type": ">", "params": [100]}
+ }
+ ]
+ }
+ },
+ {
+ "title": "Active mobile users",
+ "id": 4,
+ "targets": [
+ {"refId": "A", "target": ""},
+ {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
+ ],
+ "datasource": "graphite2",
+ "alert": {
+ "name": "name2",
+ "message": "desc2",
+ "handler": 0,
+ "frequency": "60s",
+ "severity": "warning",
+ "conditions": [
+ {
+ "type": "query",
+ "query": {"params": ["B", "5m", "now"]},
+ "reducer": {"type": "avg", "params": []},
+ "evaluator": {"type": ">", "params": [100]}
+ }
+ ]
+ }
+
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/public/app/core/services/popover_srv.ts b/public/app/core/services/popover_srv.ts
index bf0dae2c631..08d5c2bedd0 100644
--- a/public/app/core/services/popover_srv.ts
+++ b/public/app/core/services/popover_srv.ts
@@ -57,7 +57,7 @@ function popoverSrv($compile, $rootScope, $timeout) {
openOn: options.openOn,
hoverCloseDelay: 200,
tetherOptions: {
- constraints: [{to: 'scrollParent', attachment: "none both"}]
+ constraints: [{to: 'scrollParent', attachment: 'together'}]
}
});
@@ -79,3 +79,4 @@ function popoverSrv($compile, $rootScope, $timeout) {
}
coreModule.service('popoverSrv', popoverSrv);
+
diff --git a/public/app/features/org/teams_ctrl.ts b/public/app/features/org/teams_ctrl.ts
index 089bb9863b9..d02f3663949 100644
--- a/public/app/features/org/teams_ctrl.ts
+++ b/public/app/features/org/teams_ctrl.ts
@@ -1,7 +1,7 @@
///
-import coreModule from 'app/core/core_module';
-import {appEvents} from 'app/core/core';
+import coreModule from "app/core/core_module";
+import { appEvents } from "app/core/core";
export class TeamsCtrl {
teams: any;
@@ -10,18 +10,23 @@ export class TeamsCtrl {
page = 1;
totalPages: number;
showPaging = false;
- query: any = '';
+ query: any = "";
navModel: any;
/** @ngInject */
constructor(private backendSrv, navModelSrv) {
- this.navModel = navModelSrv.getNav('cfg', 'teams', 0);
+ this.navModel = navModelSrv.getNav("cfg", "teams", 0);
this.get();
}
get() {
- this.backendSrv.get(`/api/teams/search?perpage=${this.perPage}&page=${this.page}&query=${this.query}`)
- .then((result) => {
+ this.backendSrv
+ .get(
+ `/api/teams/search?perpage=${this.perPage}&page=${this.page}&query=${
+ this.query
+ }`
+ )
+ .then(result => {
this.teams = result.teams;
this.page = result.page;
this.perPage = result.perPage;
@@ -29,8 +34,8 @@ export class TeamsCtrl {
this.showPaging = this.totalPages > 1;
this.pages = [];
- for (var i = 1; i < this.totalPages+1; i++) {
- this.pages.push({ page: i, current: i === this.page});
+ for (var i = 1; i < this.totalPages + 1; i++) {
+ this.pages.push({ page: i, current: i === this.page });
}
});
}
@@ -41,9 +46,9 @@ export class TeamsCtrl {
}
deleteTeam(team) {
- appEvents.emit('confirm-modal', {
- title: 'Delete',
- text: 'Are you sure you want to delete Team ' + team.name + '?',
+ appEvents.emit("confirm-modal", {
+ title: "Delete",
+ text: "Are you sure you want to delete Team " + team.name + "?",
yesText: "Delete",
icon: "fa-warning",
onConfirm: () => {
@@ -53,16 +58,15 @@ export class TeamsCtrl {
}
deleteTeamConfirmed(team) {
- this.backendSrv.delete('/api/teams/' + team.id)
- .then(this.get.bind(this));
+ this.backendSrv.delete("/api/teams/" + team.id).then(this.get.bind(this));
}
openTeamModal() {
- appEvents.emit('show-modal', {
- templateHtml: '',
- modalClass: 'modal--narrow'
+ appEvents.emit("show-modal", {
+ templateHtml: "",
+ modalClass: "modal--narrow"
});
}
}
-coreModule.controller('TeamsCtrl', TeamsCtrl);
+coreModule.controller("TeamsCtrl", TeamsCtrl);
diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts
index 791ebf761ae..bb96f1565c4 100644
--- a/public/app/plugins/panel/graph/legend.ts
+++ b/public/app/plugins/panel/graph/legend.ts
@@ -53,7 +53,8 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
$timeout(function() {
popoverSrv.show({
element: el[0],
- position: 'bottom center',
+ position: 'bottom left',
+ targetAttachment: 'top left',
template: '' +
'',
openOn: 'hover',
diff --git a/yarn.lock b/yarn.lock
index d5aa06eaffb..a419e1020c9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -409,6 +409,10 @@ ansistyles@~0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539"
+any-observable@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.2.0.tgz#c67870058003579009083f54ac0abafb5c33d242"
+
anymatch@^1.3.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
@@ -2052,19 +2056,6 @@ core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
-cosmiconfig@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-1.1.0.tgz#0dea0f9804efdfb929fbb1b188e25553ea053d37"
- dependencies:
- graceful-fs "^4.1.2"
- js-yaml "^3.4.3"
- minimist "^1.2.0"
- object-assign "^4.0.1"
- os-homedir "^1.0.1"
- parse-json "^2.2.0"
- pinkie-promise "^2.0.0"
- require-from-string "^1.1.0"
-
cosmiconfig@^2.1.0, cosmiconfig@^2.1.1:
version "2.2.2"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892"
@@ -2077,6 +2068,15 @@ cosmiconfig@^2.1.0, cosmiconfig@^2.1.1:
parse-json "^2.2.0"
require-from-string "^1.1.0"
+cosmiconfig@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-3.1.0.tgz#640a94bf9847f321800403cd273af60665c73397"
+ dependencies:
+ is-directory "^0.3.1"
+ js-yaml "^3.9.0"
+ parse-json "^3.0.0"
+ require-from-string "^2.0.1"
+
cpx@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/cpx/-/cpx-1.5.0.tgz#185be018511d87270dedccc293171e37655ab88f"
@@ -2591,6 +2591,10 @@ decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+dedent@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
+
deep-equal@*:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
@@ -3033,7 +3037,7 @@ errno@^0.1.3, errno@^0.1.4:
dependencies:
prr "~0.0.0"
-error-ex@^1.2.0:
+error-ex@^1.2.0, error-ex@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
dependencies:
@@ -3606,6 +3610,10 @@ find-index@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4"
+find-parent-dir@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54"
+
find-up@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
@@ -4893,6 +4901,12 @@ is-obj@^1.0.0, is-obj@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
+is-observable@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-0.2.0.tgz#b361311d83c6e5d726cabf5e250b0237106f5ae2"
+ dependencies:
+ symbol-observable "^0.2.2"
+
is-odd@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-1.0.0.tgz#3b8a932eb028b3775c39bb09e91767accdb69088"
@@ -5334,7 +5348,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
-js-yaml@^3.4.3, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0:
+js-yaml@^3.4.3, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0, js-yaml@^3.9.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc"
dependencies:
@@ -5729,23 +5743,28 @@ libnpx@~9.6.0:
y18n "^3.2.1"
yargs "^8.0.2"
-lint-staged@^4.2.3:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-4.3.0.tgz#ed0779ad9a42c0dc62bb3244e522870b41125879"
+lint-staged@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-6.0.0.tgz#7ab7d345f2fe302ff196f1de6a005594ace03210"
dependencies:
app-root-path "^2.0.0"
chalk "^2.1.0"
commander "^2.11.0"
- cosmiconfig "^1.1.0"
+ cosmiconfig "^3.1.0"
+ debug "^3.1.0"
+ dedent "^0.7.0"
execa "^0.8.0"
+ find-parent-dir "^0.3.0"
is-glob "^4.0.0"
jest-validate "^21.1.0"
- listr "^0.12.0"
+ listr "^0.13.0"
lodash "^4.17.4"
log-symbols "^2.0.0"
minimatch "^3.0.0"
npm-which "^3.0.1"
p-map "^1.1.1"
+ path-is-inside "^1.0.2"
+ pify "^3.0.0"
staged-git-files "0.0.4"
stringify-object "^3.2.0"
@@ -5753,9 +5772,9 @@ listr-silent-renderer@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e"
-listr-update-renderer@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.2.0.tgz#ca80e1779b4e70266807e8eed1ad6abe398550f9"
+listr-update-renderer@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.4.0.tgz#344d980da2ca2e8b145ba305908f32ae3f4cc8a7"
dependencies:
chalk "^1.1.3"
cli-truncate "^0.2.1"
@@ -5775,25 +5794,26 @@ listr-verbose-renderer@^0.4.0:
date-fns "^1.27.2"
figures "^1.7.0"
-listr@^0.12.0:
- version "0.12.0"
- resolved "https://registry.yarnpkg.com/listr/-/listr-0.12.0.tgz#6bce2c0f5603fa49580ea17cd6a00cc0e5fa451a"
+listr@^0.13.0:
+ version "0.13.0"
+ resolved "https://registry.yarnpkg.com/listr/-/listr-0.13.0.tgz#20bb0ba30bae660ee84cc0503df4be3d5623887d"
dependencies:
chalk "^1.1.3"
cli-truncate "^0.2.1"
figures "^1.7.0"
indent-string "^2.1.0"
+ is-observable "^0.2.0"
is-promise "^2.1.0"
is-stream "^1.1.0"
listr-silent-renderer "^1.1.1"
- listr-update-renderer "^0.2.0"
+ listr-update-renderer "^0.4.0"
listr-verbose-renderer "^0.4.0"
log-symbols "^1.0.2"
log-update "^1.0.2"
ora "^0.2.3"
p-map "^1.1.1"
- rxjs "^5.0.0-beta.11"
- stream-to-observable "^0.1.0"
+ rxjs "^5.4.2"
+ stream-to-observable "^0.2.0"
strip-ansi "^3.0.1"
load-grunt-tasks@3.5.2:
@@ -7147,6 +7167,12 @@ parse-json@^2.2.0:
dependencies:
error-ex "^1.2.0"
+parse-json@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-3.0.0.tgz#fa6f47b18e23826ead32f263e744d0e1e847fb13"
+ dependencies:
+ error-ex "^1.3.1"
+
parse5@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94"
@@ -7218,7 +7244,7 @@ path-is-absolute@^1.0.0, path-is-absolute@^1.0.1, path-is-absolute@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
-path-is-inside@^1.0.1, path-is-inside@~1.0.2:
+path-is-inside@^1.0.1, path-is-inside@^1.0.2, path-is-inside@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
@@ -8362,6 +8388,10 @@ require-from-string@^1.1.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418"
+require-from-string@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.1.tgz#c545233e9d7da6616e9d59adfb39fc9f588676ff"
+
require-main-filename@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
@@ -8478,7 +8508,13 @@ rx-lite@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
-rxjs@^5.0.0-beta.11, rxjs@^5.4.3:
+rxjs@^5.4.2:
+ version "5.5.5"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.5.tgz#e164f11d38eaf29f56f08c3447f74ff02dd84e97"
+ dependencies:
+ symbol-observable "1.0.1"
+
+rxjs@^5.4.3:
version "5.5.2"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.2.tgz#28d403f0071121967f18ad665563255d54236ac3"
dependencies:
@@ -9062,9 +9098,11 @@ stream-shift@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
-stream-to-observable@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.1.0.tgz#45bf1d9f2d7dc09bed81f1c307c430e68b84cffe"
+stream-to-observable@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.2.0.tgz#59d6ea393d87c2c0ddac10aa0d561bc6ba6f0e10"
+ dependencies:
+ any-observable "^0.2.0"
strict-uri-encode@^1.0.0:
version "1.1.0"
@@ -9223,6 +9261,14 @@ swap-case@^1.1.0:
lower-case "^1.1.1"
upper-case "^1.1.1"
+symbol-observable@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"
+
+symbol-observable@^0.2.2:
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40"
+
symbol-observable@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d"