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"