diff --git a/CHANGELOG.md b/CHANGELOG.md index 0acd4b158c2..bb0d2e026b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # 5.0.0 (unreleased / master branch) +Grafana v5.0 is going to be the biggest and most foundational release Grafana has ever had, coming with a ton of UX improvements, a new dashboard grid engine, dashboard folders, user teams and permissions. Checkout out this [video preview](https://www.youtube.com/watch?v=BC_YRNpqj5k) of Grafana v5. + ### New Features - **Dashboards** Dashboard folders, [#1611](https://github.com/grafana/grafana/issues/1611) - **Teams** User groups (teams) implemented. Can be used in folder & dashboard permission list. diff --git a/README.md b/README.md index 5358cd3f3d5..069958d9031 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB. ![](http://docs.grafana.org/assets/img/features/dashboard_ex1.png) +## Grafana v5 Alpha Preview +Grafana master is now v5.0 alpha. This is going to be the biggest and most foundational release Grafana has ever had, coming with a ton of UX improvements, a new dashboard grid engine, dashboard folders, user teams and permissions. Checkout out this [video preview](https://www.youtube.com/watch?v=BC_YRNpqj5k) of Grafana v5. + ## Installation Head to [docs.grafana.org](http://docs.grafana.org/installation/) and [download](https://grafana.com/get) the latest release. diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index d9f048e0371..7fae7441b6d 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -45,7 +45,14 @@ To simplify syntax and to allow for dynamic parts, like date range filters, the Macro example | Description ------------ | ------------- +*$__time(dateColumn)* | Will be replaced by an expression to convert to a UNIX timestamp and rename the column to `time_sec`. For example, *UNIX_TIMESTAMP(dateColumn) as time_sec* *$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn > FROM_UNIXTIME(1494410783) AND dateColumn < FROM_UNIXTIME(1494497183)* +*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *FROM_UNIXTIME(1494410783)* +*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *FROM_UNIXTIME(1494497183)* +*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed) as time_sec,* +*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* +*$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* +*$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo. @@ -99,6 +106,19 @@ GROUP BY metric1, UNIX_TIMESTAMP(time_date_time) DIV 300 ORDER BY time_sec asc ``` +Example with $__timeGroup macro: + +```sql +SELECT + $__timeGroup(time_date_time,'5m') as time_sec, + min(value_double) as value, + metric_name as metric +FROM test_data +WHERE $__timeFilter(time_date_time) +GROUP BY 1, metric_name +ORDER BY 1 +``` + Currently, there is no support for a dynamic group by time based on time range & panel width. This is something we plan to add. diff --git a/docs/sources/features/panels/singlestat.md b/docs/sources/features/panels/singlestat.md index 5e2cb36600b..510642337ff 100644 --- a/docs/sources/features/panels/singlestat.md +++ b/docs/sources/features/panels/singlestat.md @@ -47,7 +47,7 @@ The coloring options of the Singlestat Panel config allow you to dynamically cha 2. **Thresholds**: Change the background and value colors dynamically within the panel, depending on the Singlestat value. The threshold field accepts **2 comma-separated** values which represent 3 ranges that correspond to the three colors directly to the right. For example: if the thresholds are 70, 90 then the first color represents < 70, the second color represents between 70 and 90 and the third color represents > 90. 3. **Colors**: Select a color and opacity 4. **Value**: This checkbox applies the configured thresholds and colors to the summary stat. -5. **Invert order**: This link toggles the threshold color order.
For example: Green, Orange, Red () will become Red, Orange, Green (). +5. **Invert order**: This link toggles the threshold color order.
For example: Green, Orange, Red () will become Red, Orange, Green (). ### Spark Lines diff --git a/docs/sources/http_api/user.md b/docs/sources/http_api/user.md index ba8afd4db22..134c1842851 100644 --- a/docs/sources/http_api/user.md +++ b/docs/sources/http_api/user.md @@ -156,7 +156,7 @@ HTTP/1.1 200 Content-Type: application/json { - "email": "user@mygraf.com" + "email": "user@mygraf.com", "name": "admin", "login": "admin", "theme": "light", @@ -409,4 +409,4 @@ HTTP/1.1 200 Content-Type: application/json {"message":"Dashboard unstarred"} -``` \ No newline at end of file +``` diff --git a/package.json b/package.json index 6bcd140cc1b..847f1738e45 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", @@ -76,7 +76,7 @@ "postcss-browser-reporter": "^0.5.0", "postcss-loader": "^2.0.6", "postcss-reporter": "^5.0.0", - "prettier": "1.7.3", + "prettier": "1.9.2", "react-test-renderer": "^16.0.0", "sass-lint": "^1.10.2", "sass-loader": "^6.0.6", @@ -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 --single-quote --trailing-comma es5 --write", + "git add" + ], + "*.scss": [ + "prettier --single-quote --write", + "git add" + ] }, "license": "Apache-2.0", "dependencies": { diff --git a/pkg/api/index.go b/pkg/api/index.go index 6629c2f5ca5..1b836356189 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -92,7 +92,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Text: "Create", Id: "create", Icon: "fa fa-fw fa-plus", - Url: setting.AppSubUrl + "dashboard/new", + Url: setting.AppSubUrl + "/dashboard/new", Children: []*dtos.NavLink{ {Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"}, {Text: "Folder", SubTitle: "Create a new folder to organize your dashboards", Id: "folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboards/folder/new"}, diff --git a/pkg/middleware/org_redirect.go b/pkg/middleware/org_redirect.go index a5f90d60e47..9dd764be1bb 100644 --- a/pkg/middleware/org_redirect.go +++ b/pkg/middleware/org_redirect.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "strconv" + "strings" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" @@ -41,7 +42,7 @@ func OrgRedirect() macaron.Handler { return } - newUrl := setting.ToAbsUrl(fmt.Sprintf("%s?%s", c.Req.URL.Path, c.Req.URL.Query().Encode())) - c.Redirect(newUrl, 302) + newURL := setting.ToAbsUrl(fmt.Sprintf("%s?%s", strings.TrimPrefix(c.Req.URL.Path, "/"), c.Req.URL.Query().Encode())) + c.Redirect(newURL, 302) } } 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/notifiers/pushover.go b/pkg/services/alerting/notifiers/pushover.go index 6f8d0fb99e6..067c02a4a5d 100644 --- a/pkg/services/alerting/notifiers/pushover.go +++ b/pkg/services/alerting/notifiers/pushover.go @@ -133,6 +133,7 @@ func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Error("Failed get rule link", "error", err) return err } + message := evalContext.Rule.Message for idx, evt := range evalContext.EvalMatches { message += fmt.Sprintf("\n%s: %v", evt.Metric, evt.Value) @@ -146,6 +147,9 @@ func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error { if evalContext.ImagePublicUrl != "" { message += fmt.Sprintf("\nShow graph image", evalContext.ImagePublicUrl) } + if message == "" { + message = "Notification message missing (Set a notification message to replace this text.)" + } q := url.Values{} q.Add("user", this.UserKey) 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/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 31a42a7b3b3..0b6b60a5e11 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -345,8 +345,9 @@ func GetDashboards(query *m.GetDashboardsQuery) error { func GetDashboardsByPluginId(query *m.GetDashboardsByPluginIdQuery) error { var dashboards = make([]*m.Dashboard, 0) + whereExpr := "org_id=? AND plugin_id=? AND is_folder=" + dialect.BooleanStr(false) - err := x.Where("org_id=? AND plugin_id=? AND is_folder=0", query.OrgId, query.PluginId).Find(&dashboards) + err := x.Where(whereExpr, query.OrgId, query.PluginId).Find(&dashboards) query.Result = dashboards if err != nil { diff --git a/pkg/services/sqlstore/dashboard_acl.go b/pkg/services/sqlstore/dashboard_acl.go index 3ab0361d175..3b0c89e02ef 100644 --- a/pkg/services/sqlstore/dashboard_acl.go +++ b/pkg/services/sqlstore/dashboard_acl.go @@ -170,7 +170,12 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error { FROM dashboard_acl as da, dashboard as dash LEFT JOIN dashboard folder on dash.folder_id = folder.id - WHERE dash.id = ? AND (dash.has_acl = 0 or folder.has_acl = 0) AND da.dashboard_id = -1 + WHERE + dash.id = ? AND ( + dash.has_acl = ` + dialect.BooleanStr(false) + ` or + folder.has_acl = ` + dialect.BooleanStr(false) + ` + ) AND + da.dashboard_id = -1 ` query.Result = make([]*m.DashboardAclInfoDTO, 0) diff --git a/pkg/services/sqlstore/migrations/team_mig.go b/pkg/services/sqlstore/migrations/team_mig.go index cc479097f9b..374972e5449 100644 --- a/pkg/services/sqlstore/migrations/team_mig.go +++ b/pkg/services/sqlstore/migrations/team_mig.go @@ -7,7 +7,7 @@ func addTeamMigrations(mg *Migrator) { Name: "team", Columns: []*Column{ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, - {Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false}, {Name: "org_id", Type: DB_BigInt}, {Name: "created", Type: DB_DateTime, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false}, diff --git a/pkg/services/sqlstore/search_builder.go b/pkg/services/sqlstore/search_builder.go index 6a5e8e60b54..ddf192da5ff 100644 --- a/pkg/services/sqlstore/search_builder.go +++ b/pkg/services/sqlstore/search_builder.go @@ -175,14 +175,14 @@ func (sb *SearchBuilder) buildSearchWhereClause() { } if sb.signedInUser.OrgRole != m.ROLE_ADMIN { - allowedDashboardsSubQuery := ` AND (dashboard.has_acl = 0 OR dashboard.id in ( + allowedDashboardsSubQuery := ` AND (dashboard.has_acl = ` + dialect.BooleanStr(false) + ` OR dashboard.id in ( SELECT distinct d.id AS DashboardId FROM dashboard AS d LEFT JOIN dashboard_acl as da on d.folder_id = da.dashboard_id or d.id = da.dashboard_id LEFT JOIN team_member as ugm on ugm.team_id = da.team_id LEFT JOIN org_user ou on ou.role = da.role WHERE - d.has_acl = 1 and + d.has_acl = ` + dialect.BooleanStr(true) + ` and (da.user_id = ? or ugm.user_id = ? or ou.id is not null) and d.org_id = ? ) @@ -198,11 +198,11 @@ func (sb *SearchBuilder) buildSearchWhereClause() { } if sb.whereTypeFolder { - sb.sql.WriteString(" AND dashboard.is_folder = 1") + sb.sql.WriteString(" AND dashboard.is_folder = " + dialect.BooleanStr(true)) } if sb.whereTypeDash { - sb.sql.WriteString(" AND dashboard.is_folder = 0") + sb.sql.WriteString(" AND dashboard.is_folder = " + dialect.BooleanStr(false)) } if len(sb.whereFolderIds) > 0 { diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index b9d4d5b6a80..1e1e855b123 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -127,7 +127,7 @@ func init() { "AWS/Events": {"RuleName"}, "AWS/Firehose": {"DeliveryStreamName"}, "AWS/IoT": {"Protocol"}, - "AWS/Kinesis": {"StreamName", "ShardID"}, + "AWS/Kinesis": {"StreamName", "ShardId"}, "AWS/KinesisAnalytics": {"Flow", "Id", "Application"}, "AWS/Lambda": {"FunctionName", "Resource", "Version", "Alias"}, "AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"}, diff --git a/public/app/app.ts b/public/app/app.ts index b76dceb4943..f3e1fa5133e 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -21,12 +21,12 @@ import _ from 'lodash'; import moment from 'moment'; // add move to lodash for backward compatabiltiy -_.move = function (array, fromIndex, toIndex) { +_.move = function(array, fromIndex, toIndex) { array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]); return array; }; -import {coreModule, registerAngularDirectives} from './core/core'; +import { coreModule, registerAngularDirectives } from './core/core'; export class GrafanaApp { registerFunctions: any; @@ -54,36 +54,49 @@ export class GrafanaApp { moment.locale(config.bootData.user.locale); - app.config(($locationProvider, $controllerProvider, $compileProvider, $filterProvider, $httpProvider, $provide) => { - // pre assing bindings before constructor calls - $compileProvider.preAssignBindingsEnabled(true); + app.config( + ( + $locationProvider, + $controllerProvider, + $compileProvider, + $filterProvider, + $httpProvider, + $provide + ) => { + // pre assing bindings before constructor calls + $compileProvider.preAssignBindingsEnabled(true); - if (config.buildInfo.env !== 'development') { - $compileProvider.debugInfoEnabled(false); + if (config.buildInfo.env !== 'development') { + $compileProvider.debugInfoEnabled(false); + } + + $httpProvider.useApplyAsync(true); + + this.registerFunctions.controller = $controllerProvider.register; + this.registerFunctions.directive = $compileProvider.directive; + this.registerFunctions.factory = $provide.factory; + this.registerFunctions.service = $provide.service; + this.registerFunctions.filter = $filterProvider.register; + + $provide.decorator('$http', [ + '$delegate', + '$templateCache', + function($delegate, $templateCache) { + var get = $delegate.get; + $delegate.get = function(url, config) { + if (url.match(/\.html$/)) { + // some template's already exist in the cache + if (!$templateCache.get(url)) { + url += '?v=' + new Date().getTime(); + } + } + return get(url, config); + }; + return $delegate; + }, + ]); } - - $httpProvider.useApplyAsync(true); - - this.registerFunctions.controller = $controllerProvider.register; - this.registerFunctions.directive = $compileProvider.directive; - this.registerFunctions.factory = $provide.factory; - this.registerFunctions.service = $provide.service; - this.registerFunctions.filter = $filterProvider.register; - - $provide.decorator("$http", ["$delegate", "$templateCache", function($delegate, $templateCache) { - var get = $delegate.get; - $delegate.get = function(url, config) { - if (url.match(/\.html$/)) { - // some template's already exist in the cache - if (!$templateCache.get(url)) { - url += "?v=" + new Date().getTime(); - } - } - return get(url, config); - }; - return $delegate; - }]); - }); + ); this.ngModuleDependencies = [ 'grafana.core', @@ -95,10 +108,17 @@ export class GrafanaApp { 'pasvaz.bindonce', 'ui.bootstrap', 'ui.bootstrap.tpls', - 'react' + 'react', ]; - var module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes']; + var module_types = [ + 'controllers', + 'directives', + 'factories', + 'services', + 'filters', + 'routes', + ]; _.each(module_types, type => { var moduleName = 'grafana.' + type; @@ -113,20 +133,22 @@ export class GrafanaApp { var preBootRequires = [System.import('app/features/all')]; - Promise.all(preBootRequires).then(() => { - // disable tool tip animation - $.fn.tooltip.defaults.animation = false; - // bootstrap the app - angular.bootstrap(document, this.ngModuleDependencies).invoke(() => { - _.each(this.preBootModules, module => { - _.extend(module, this.registerFunctions); - }); + Promise.all(preBootRequires) + .then(() => { + // disable tool tip animation + $.fn.tooltip.defaults.animation = false; + // bootstrap the app + angular.bootstrap(document, this.ngModuleDependencies).invoke(() => { + _.each(this.preBootModules, module => { + _.extend(module, this.registerFunctions); + }); - this.preBootModules = null; + this.preBootModules = null; + }); + }) + .catch(function(err) { + console.log('Application boot failed:', err); }); - }).catch(function(err) { - console.log('Application boot failed:', err); - }); } } diff --git a/public/app/core/app_events.ts b/public/app/core/app_events.ts index b9a77a5f7b7..26dd74bcb00 100644 --- a/public/app/core/app_events.ts +++ b/public/app/core/app_events.ts @@ -1,4 +1,4 @@ -import {Emitter} from './utils/emitter'; +import { Emitter } from './utils/emitter'; var appEvents = new Emitter(); export default appEvents; diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx index fb09ed29085..9b45267a8e5 100644 --- a/public/app/core/components/PageHeader/PageHeader.tsx +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import { NavModel, NavModelItem } from '../../nav_model_srv'; -import classNames from 'classnames'; -import appEvents from 'app/core/app_events'; +import React from "react"; +import { NavModel, NavModelItem } from "../../nav_model_srv"; +import classNames from "classnames"; +import appEvents from "app/core/app_events"; export interface IProps { model: NavModel; @@ -9,12 +9,12 @@ export interface IProps { function TabItem(tab: NavModelItem) { if (tab.hideFromTabs) { - return (null); + return null; } let tabClasses = classNames({ - 'gf-tabs-link': true, - active: tab.active, + "gf-tabs-link": true, + active: tab.active }); return ( @@ -28,8 +28,9 @@ function TabItem(tab: NavModelItem) { } function SelectOption(navItem: NavModelItem) { - if (navItem.hideFromTabs) { // TODO: Rename hideFromTabs => hideFromNav - return (null); + if (navItem.hideFromTabs) { + // TODO: Rename hideFromTabs => hideFromNav + return null; } return ( @@ -39,14 +40,22 @@ function SelectOption(navItem: NavModelItem) { ); } -function Navigation({main}: {main: NavModelItem}) { - return (); +function Navigation({ main }: { main: NavModelItem }) { + return ( + + ); } -function SelectNav({main, customCss}: {main: NavModelItem, customCss: string}) { +function SelectNav({ + main, + customCss +}: { + main: NavModelItem; + customCss: string; +}) { const defaultSelectedItem = main.children.find(navItem => { return navItem.active === true; }); @@ -54,17 +63,32 @@ function SelectNav({main, customCss}: {main: NavModelItem, customCss: string}) { const gotoUrl = evt => { var element = evt.target; var url = element.options[element.selectedIndex].value; - appEvents.emit('location-change', {href: url}); + appEvents.emit("location-change", { href: url }); }; - return (); + return ( +
+
+ ); } -function Tabs({main, customCss}: {main: NavModelItem, customCss: string}) { - return ; +function Tabs({ main, customCss }: { main: NavModelItem; customCss: string }) { + return ( + + ); } export default class PageHeader extends React.Component { @@ -77,7 +101,11 @@ export default class PageHeader extends React.Component { for (let i = 0; i < breadcrumbs.length; i++) { const bc = breadcrumbs[i]; if (bc.url) { - breadcrumbsResult.push({bc.title}); + breadcrumbsResult.push( + + {bc.title} + + ); } else { breadcrumbsResult.push( / {bc.title}); } @@ -95,12 +123,15 @@ export default class PageHeader extends React.Component {
{main.text &&

{main.text}

} - {main.breadcrumbs && main.breadcrumbs.length > 0 && ( -

- {this.renderBreadcrumb(main.breadcrumbs)} -

) - } - {main.subTitle &&
{main.subTitle}
} + {main.breadcrumbs && + main.breadcrumbs.length > 0 && ( +

+ {this.renderBreadcrumb(main.breadcrumbs)} +

+ )} + {main.subTitle && ( +
{main.subTitle}
+ )} {main.subType && (
diff --git a/public/app/core/components/code_editor/code_editor.ts b/public/app/core/components/code_editor/code_editor.ts index cc3b1e46ad4..36d6c1b1020 100644 --- a/public/app/core/components/code_editor/code_editor.ts +++ b/public/app/core/components/code_editor/code_editor.ts @@ -38,10 +38,12 @@ import 'brace/mode/sql'; import 'brace/snippets/sql'; import 'brace/mode/markdown'; import 'brace/snippets/markdown'; +import 'brace/mode/json'; +import 'brace/snippets/json'; -const DEFAULT_THEME_DARK = "ace/theme/grafana-dark"; -const DEFAULT_THEME_LIGHT = "ace/theme/textmate"; -const DEFAULT_MODE = "text"; +const DEFAULT_THEME_DARK = 'ace/theme/grafana-dark'; +const DEFAULT_THEME_LIGHT = 'ace/theme/textmate'; +const DEFAULT_MODE = 'text'; const DEFAULT_MAX_LINES = 10; const DEFAULT_TAB_SIZE = 2; const DEFAULT_BEHAVIOURS = true; @@ -54,7 +56,9 @@ function link(scope, elem, attrs) { let maxLines = attrs.maxLines || DEFAULT_MAX_LINES; let showGutter = attrs.showGutter !== undefined; let tabSize = attrs.tabSize || DEFAULT_TAB_SIZE; - let behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIOURS; + let behavioursEnabled = attrs.behavioursEnabled + ? attrs.behavioursEnabled === 'true' + : DEFAULT_BEHAVIOURS; // Initialize editor let aceElem = elem.get(0); @@ -68,7 +72,7 @@ function link(scope, elem, attrs) { behavioursEnabled: behavioursEnabled, highlightActiveLine: false, showPrintMargin: false, - autoScrollEditorIntoView: true // this is needed if editor is inside scrollable page + autoScrollEditorIntoView: true, // this is needed if editor is inside scrollable page }; // Set options @@ -84,12 +88,12 @@ function link(scope, elem, attrs) { setEditorContent(scope.content); // Add classes - elem.addClass("gf-code-editor"); - let textarea = elem.find("textarea"); + elem.addClass('gf-code-editor'); + let textarea = elem.find('textarea'); textarea.addClass('gf-form-input'); if (scope.codeEditorFocus) { - setTimeout(function () { + setTimeout(function() { textarea.focus(); var domEl = textarea[0]; if (domEl.setSelectionRange) { @@ -100,7 +104,7 @@ function link(scope, elem, attrs) { } // Event handlers - editorSession.on('change', (e) => { + editorSession.on('change', e => { scope.$apply(() => { let newValue = codeEditor.getValue(); scope.content = newValue; @@ -121,25 +125,25 @@ function link(scope, elem, attrs) { scope.onChange(); }); - scope.$on("$destroy", () => { + scope.$on('$destroy', () => { codeEditor.destroy(); }); // Keybindings codeEditor.commands.addCommand({ name: 'executeQuery', - bindKey: {win: 'Ctrl-Enter', mac: 'Command-Enter'}, + bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter' }, exec: () => { scope.onChange(); - } + }, }); function setLangMode(lang) { - ace.acequire("ace/ext/language_tools"); + ace.acequire('ace/ext/language_tools'); codeEditor.setOptions({ enableBasicAutocompletion: true, enableLiveAutocompletion: true, - enableSnippets: true + enableSnippets: true, }); if (scope.getCompleter()) { @@ -173,13 +177,13 @@ export function codeEditorDirective() { restrict: 'E', template: editorTemplate, scope: { - content: "=", - datasource: "=", - codeEditorFocus: "<", - onChange: "&", - getCompleter: "&" + content: '=', + datasource: '=', + codeEditorFocus: '<', + onChange: '&', + getCompleter: '&', }, - link: link + link: link, }; } diff --git a/public/app/core/components/colorpicker/spectrum_picker.ts b/public/app/core/components/colorpicker/spectrum_picker.ts index 183cebffe2b..45d367bfbe8 100644 --- a/public/app/core/components/colorpicker/spectrum_picker.ts +++ b/public/app/core/components/colorpicker/spectrum_picker.ts @@ -12,13 +12,14 @@ export function spectrumPicker() { require: 'ngModel', scope: true, replace: true, - template: '', + template: + '', link: function(scope, element, attrs, ngModel) { scope.ngModel = ngModel; - scope.onColorChange = (color) => { + scope.onColorChange = color => { ngModel.$setViewValue(color); }; - } + }, }; } coreModule.directive('spectrumPicker', spectrumPicker); diff --git a/public/app/core/components/dashboard_selector.ts b/public/app/core/components/dashboard_selector.ts index ac8b30e7733..379fd441a19 100644 --- a/public/app/core/components/dashboard_selector.ts +++ b/public/app/core/components/dashboard_selector.ts @@ -9,15 +9,14 @@ export class DashboardSelectorCtrl { options: any; /** @ngInject */ - constructor(private backendSrv) { - } + constructor(private backendSrv) {} $onInit() { - this.options = [{value: 0, text: 'Default'}]; + this.options = [{ value: 0, text: 'Default' }]; - return this.backendSrv.search({starred: true}).then(res => { + return this.backendSrv.search({ starred: true }).then(res => { res.forEach(dash => { - this.options.push({value: dash.id, text: dash.title}); + this.options.push({ value: dash.id, text: dash.title }); }); }); } @@ -31,8 +30,8 @@ export function dashboardSelector() { controllerAs: 'ctrl', template: template, scope: { - model: '=' - } + model: '=', + }, }; } diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index 364d61cfcdb..f8d715579a5 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -4,8 +4,12 @@ import coreModule from '../../core_module'; function typeaheadMatcher(item) { var str = this.query; - if (str[0] === '/') { str = str.substring(1); } - if (str[str.length - 1] === '/') { str = str.substring(0, str.length-1); } + if (str[0] === '/') { + str = str.substring(1); + } + if (str[str.length - 1] === '/') { + str = str.substring(0, str.length - 1); + } return item.toLowerCase().match(str.toLowerCase()); } @@ -28,19 +32,26 @@ export class FormDropdownCtrl { lookupText: boolean; /** @ngInject **/ - constructor(private $scope, $element, private $sce, private templateSrv, private $q) { + constructor( + private $scope, + $element, + private $sce, + private templateSrv, + private $q + ) { this.inputElement = $element.find('input').first(); this.linkElement = $element.find('a').first(); this.linkMode = true; this.cancelBlur = null; // listen to model changes - $scope.$watch("ctrl.model", this.modelChanged.bind(this)); + $scope.$watch('ctrl.model', this.modelChanged.bind(this)); if (this.labelMode) { this.cssClasses = 'gf-form-label ' + this.cssClass; } else { - this.cssClasses = 'gf-form-input gf-form-input--dropdown ' + this.cssClass; + this.cssClasses = + 'gf-form-input gf-form-input--dropdown ' + this.cssClass; } this.inputElement.attr('data-provide', 'typeahead'); @@ -55,7 +66,7 @@ export class FormDropdownCtrl { // modify typeahead lookup // this = typeahead var typeahead = this.inputElement.data('typeahead'); - typeahead.lookup = function () { + typeahead.lookup = function() { this.query = this.$element.val() || ''; var items = this.source(this.query, $.proxy(this.process, this)); return items ? this.process(items) : items; @@ -80,7 +91,7 @@ export class FormDropdownCtrl { } getOptionsInternal(query) { - var result = this.getOptions({$query: query}); + var result = this.getOptions({ $query: query }); if (this.isPromiseLike(result)) { return result; } @@ -88,7 +99,7 @@ export class FormDropdownCtrl { } isPromiseLike(obj) { - return obj && (typeof obj.then === 'function'); + return obj && typeof obj.then === 'function'; } modelChanged() { @@ -97,8 +108,8 @@ export class FormDropdownCtrl { } else { // if we have text use it if (this.lookupText) { - this.getOptionsInternal("").then(options => { - var item = _.find(options, {value: this.model}); + this.getOptionsInternal('').then(options => { + var item = _.find(options, { value: this.model }); this.updateDisplay(item ? item.text : this.model); }); } else { @@ -140,7 +151,9 @@ export class FormDropdownCtrl { } switchToLink(fromClick) { - if (this.linkMode && !fromClick) { return; } + if (this.linkMode && !fromClick) { + return; + } clearTimeout(this.cancelBlur); this.cancelBlur = null; @@ -164,7 +177,7 @@ export class FormDropdownCtrl { } this.$scope.$apply(() => { - var option = _.find(this.optionCache, {text: text}); + var option = _.find(this.optionCache, { text: text }); if (option) { if (_.isObject(this.model)) { @@ -186,20 +199,24 @@ export class FormDropdownCtrl { // property is synced with outerscope this.$scope.$$postDigest(() => { this.$scope.$apply(() => { - this.onChange({$option: option}); + this.onChange({ $option: option }); }); }); - }); } updateDisplay(text) { this.text = text; - this.display = this.$sce.trustAsHtml(this.templateSrv.highlightVariablesAsHtml(text)); + this.display = this.$sce.trustAsHtml( + this.templateSrv.highlightVariablesAsHtml(text) + ); } open() { - this.inputElement.css('width', (Math.max(this.linkElement.width(), 80) + 16) + 'px'); + this.inputElement.css( + 'width', + Math.max(this.linkElement.width(), 80) + 16 + 'px' + ); this.inputElement.show(); this.inputElement.focus(); @@ -215,7 +232,7 @@ export class FormDropdownCtrl { } } -const template = ` +const template = ` inActiveTimeLimit) { + if (new Date().getTime() - lastActivity > inActiveTimeLimit) { activeUser = false; body.addClass('user-activity-low'); // hide sidemenu @@ -148,7 +208,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop sidemenuHidden = true; body.removeClass('sidemenu-open'); $timeout(function() { - $rootScope.$broadcast("render"); + $rootScope.$broadcast('render'); }, 100); } } @@ -165,7 +225,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop sidemenuHidden = false; body.addClass('sidemenu-open'); $timeout(function() { - $rootScope.$broadcast("render"); + $rootScope.$broadcast('render'); }, 100); } } @@ -209,7 +269,10 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop // hide search if (body.find('.search-container').length > 0) { - if (target.parents('.search-results-container, .search-field-wrapper').length === 0) { + if ( + target.parents('.search-results-container, .search-field-wrapper') + .length === 0 + ) { scope.$apply(function() { scope.appEvent('hide-dash-search'); }); @@ -218,11 +281,14 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop // hide popovers var popover = elem.find('.popover'); - if (popover.length > 0 && target.parents('.graph-legend').length === 0) { + if ( + popover.length > 0 && + target.parents('.graph-legend').length === 0 + ) { popover.hide(); } }); - } + }, }; } diff --git a/public/app/core/components/help/help.ts b/public/app/core/components/help/help.ts index bf84b43b5f9..a594b95bb4d 100644 --- a/public/app/core/components/help/help.ts +++ b/public/app/core/components/help/help.ts @@ -11,39 +11,45 @@ export class HelpCtrl { constructor() { this.tabIndex = 0; this.shortcuts = { - 'Global': [ - {keys: ['g', 'h'], description: 'Go to Home Dashboard'}, - {keys: ['g', 'p'], description: 'Go to Profile'}, - {keys: ['s', 'o'], description: 'Open search'}, - {keys: ['s', 's'], description: 'Open search with starred filter'}, - {keys: ['s', 't'], description: 'Open search in tags view'}, - {keys: ['esc'], description: 'Exit edit/setting views'}, + Global: [ + { keys: ['g', 'h'], description: 'Go to Home Dashboard' }, + { keys: ['g', 'p'], description: 'Go to Profile' }, + { keys: ['s', 'o'], description: 'Open search' }, + { keys: ['s', 's'], description: 'Open search with starred filter' }, + { keys: ['s', 't'], description: 'Open search in tags view' }, + { keys: ['esc'], description: 'Exit edit/setting views' }, ], - 'Dashboard': [ - {keys: ['mod+s'], description: 'Save dashboard'}, - {keys: ['mod+h'], description: 'Hide row controls'}, - {keys: ['d', 'r'], description: 'Refresh all panels'}, - {keys: ['d', 's'], description: 'Dashboard settings'}, - {keys: ['d', 'v'], description: 'Toggle in-active / view mode'}, - {keys: ['d', 'k'], description: 'Toggle kiosk mode (hides top nav)'}, - {keys: ['d', 'E'], description: 'Expand all rows'}, - {keys: ['d', 'C'], description: 'Collapse all rows'}, - {keys: ['mod+o'], description: 'Toggle shared graph crosshair'}, + Dashboard: [ + { keys: ['mod+s'], description: 'Save dashboard' }, + { keys: ['mod+h'], description: 'Hide row controls' }, + { keys: ['d', 'r'], description: 'Refresh all panels' }, + { keys: ['d', 's'], description: 'Dashboard settings' }, + { keys: ['d', 'v'], description: 'Toggle in-active / view mode' }, + { keys: ['d', 'k'], description: 'Toggle kiosk mode (hides top nav)' }, + { keys: ['d', 'E'], description: 'Expand all rows' }, + { keys: ['d', 'C'], description: 'Collapse all rows' }, + { keys: ['mod+o'], description: 'Toggle shared graph crosshair' }, ], 'Focused Panel': [ - {keys: ['e'], description: 'Toggle panel edit view'}, - {keys: ['v'], description: 'Toggle panel fullscreen view'}, - {keys: ['p', 's'], description: 'Open Panel Share Modal'}, - {keys: ['p', 'r'], description: 'Remove Panel'}, + { keys: ['e'], description: 'Toggle panel edit view' }, + { keys: ['v'], description: 'Toggle panel fullscreen view' }, + { keys: ['p', 's'], description: 'Open Panel Share Modal' }, + { keys: ['p', 'r'], description: 'Remove Panel' }, ], 'Focused Row': [ - {keys: ['r', 'c'], description: 'Collapse Row'}, - {keys: ['r', 'r'], description: 'Remove Row'}, + { keys: ['r', 'c'], description: 'Collapse Row' }, + { keys: ['r', 'r'], description: 'Remove Row' }, ], 'Time Range': [ - {keys: ['t', 'z'], description: 'Zoom out time range'}, - {keys: ['t', ''], description: 'Move time range back'}, - {keys: ['t', ''], description: 'Move time range forward'}, + { keys: ['t', 'z'], description: 'Zoom out time range' }, + { + keys: ['t', ''], + description: 'Move time range back', + }, + { + keys: ['t', ''], + description: 'Move time range forward', + }, ], }; } diff --git a/public/app/core/components/info_popover.ts b/public/app/core/components/info_popover.ts index 954e84a3baa..9d2d13c9f01 100644 --- a/public/app/core/components/info_popover.ts +++ b/public/app/core/components/info_popover.ts @@ -26,10 +26,10 @@ export function infoPopover() { } transclude(function(clone, newScope) { - var content = document.createElement("div"); + var content = document.createElement('div'); content.className = 'markdown-html'; - _.each(clone, (node) => { + _.each(clone, node => { content.appendChild(node); }); @@ -43,22 +43,21 @@ export function infoPopover() { tetherOptions: { offset: offset, constraints: [ - { - to: 'window', - attachment: 'together', - pin: true - } - ], - } + { + to: 'window', + attachment: 'together', + pin: true, + }, + ], + }, }); var unbind = scope.$on('$destroy', function() { drop.destroy(); unbind(); }); - }); - } + }, }; } diff --git a/public/app/core/components/json_explorer/helpers.ts b/public/app/core/components/json_explorer/helpers.ts index 7c4429d7c76..69fd422fff1 100644 --- a/public/app/core/components/json_explorer/helpers.ts +++ b/public/app/core/components/json_explorer/helpers.ts @@ -5,7 +5,7 @@ * Escapes `"` charachters from string */ function escapeString(str: string): string { - return str.replace('"', '\"'); + return str.replace('"', '"'); } /* @@ -13,7 +13,7 @@ function escapeString(str: string): string { */ export function isObject(value: any): boolean { var type = typeof value; - return !!value && (type === 'object'); + return !!value && type === 'object'; } /* @@ -29,11 +29,11 @@ export function getObjectName(object: Object): string { return 'Object'; } if (typeof object === 'object' && !object.constructor) { - return 'Object'; + return 'Object'; } const funcNameRegex = /function ([^(]*)/; - const results = (funcNameRegex).exec((object).constructor.toString()); + const results = funcNameRegex.exec(object.constructor.toString()); if (results && results.length > 1) { return results[1]; } else { @@ -45,27 +45,33 @@ export function getObjectName(object: Object): string { * Gets type of an object. Returns "null" for null objects */ export function getType(object: Object): string { - if (object === null) { return 'null'; } + if (object === null) { + return 'null'; + } return typeof object; } /* * Generates inline preview for a JavaScript object based on a value */ -export function getValuePreview (object: Object, value: string): string { +export function getValuePreview(object: Object, value: string): string { var type = getType(object); - if (type === 'null' || type === 'undefined') { return type; } + if (type === 'null' || type === 'undefined') { + return type; + } if (type === 'string') { value = '"' + escapeString(value) + '"'; } if (type === 'function') { - // Remove content of the function - return object.toString() + return ( + object + .toString() .replace(/[\r\n]/g, '') - .replace(/\{.*\}/, '') + '{…}'; + .replace(/\{.*\}/, '') + '{…}' + ); } return value; } @@ -97,7 +103,11 @@ export function cssClass(className: string): string { * Creates a new DOM element wiht given type and class * TODO: move me to helpers */ -export function createElement(type: string, className?: string, content?: Element|string): Element { +export function createElement( + type: string, + className?: string, + content?: Element | string +): Element { const el = document.createElement(type); if (className) { el.classList.add(cssClass(className)); diff --git a/public/app/core/components/json_explorer/json_explorer.ts b/public/app/core/components/json_explorer/json_explorer.ts index a7079477abc..5c879bcee0f 100644 --- a/public/app/core/components/json_explorer/json_explorer.ts +++ b/public/app/core/components/json_explorer/json_explorer.ts @@ -7,7 +7,7 @@ import { getType, getValuePreview, cssClass, - createElement + createElement, } from './helpers'; import _ from 'lodash'; @@ -19,7 +19,12 @@ const JSON_DATE_REGEX = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; // When toggleing, don't animated removal or addition of more than a few items const MAX_ANIMATED_TOGGLE_ITEMS = 10; -const requestAnimationFrame = window.requestAnimationFrame || function(cb: ()=>void) { cb(); return 0; }; +const requestAnimationFrame = + window.requestAnimationFrame || + function(cb: () => void) { + cb(); + return 0; + }; export interface JsonExplorerConfig { animateOpen?: boolean; @@ -30,18 +35,16 @@ export interface JsonExplorerConfig { const _defaultConfig: JsonExplorerConfig = { animateOpen: true, animateClose: true, - theme: null + theme: null, }; - /** * @class JsonExplorer * * JsonExplorer allows you to render JSON objects in HTML with a * **collapsible** navigation. -*/ + */ export class JsonExplorer { - // Hold the open state after the toggler is used private _isOpen: boolean = null; @@ -77,9 +80,13 @@ export class JsonExplorer { * * @param {string} [key=undefined] The key that this object in it's parent * context - */ - constructor(public json: any, private open = 1, private config: JsonExplorerConfig = _defaultConfig, private key?: string) { - } + */ + constructor( + public json: any, + private open = 1, + private config: JsonExplorerConfig = _defaultConfig, + private key?: string + ) {} /* * is formatter open? @@ -103,17 +110,19 @@ export class JsonExplorer { * is this a date string? */ private get isDate(): boolean { - return (this.type === 'string') && + return ( + this.type === 'string' && (DATE_STRING_REGEX.test(this.json) || - JSON_DATE_REGEX.test(this.json) || - PARTIAL_DATE_REGEX.test(this.json)); + JSON_DATE_REGEX.test(this.json) || + PARTIAL_DATE_REGEX.test(this.json)) + ); } /* * is this a URL string? */ private get isUrl(): boolean { - return this.type === 'string' && (this.json.indexOf('http') === 0); + return this.type === 'string' && this.json.indexOf('http') === 0; } /* @@ -142,7 +151,9 @@ export class JsonExplorer { * is this an empty object or array? */ private get isEmpty(): boolean { - return this.isEmptyObject || (this.keys && !this.keys.length && this.isArray); + return ( + this.isEmptyObject || (this.keys && !this.keys.length && this.isArray) + ); } /* @@ -174,7 +185,7 @@ export class JsonExplorer { */ private get keys(): string[] { if (this.isObject) { - return Object.keys(this.json).map((key)=> key ? key : '""'); + return Object.keys(this.json).map(key => (key ? key : '""')); } else { return []; } @@ -183,7 +194,7 @@ export class JsonExplorer { /** * Toggles `isOpen` state * - */ + */ toggleOpen() { this.isOpen = !this.isOpen; @@ -198,17 +209,17 @@ export class JsonExplorer { } /** - * Open all children up to a certain depth. - * Allows actions such as expand all/collapse all - * - */ + * Open all children up to a certain depth. + * Allows actions such as expand all/collapse all + * + */ openAtDepth(depth = 1) { if (depth < 0) { return; } this.open = depth; - this.isOpen = (depth !== 0); + this.isOpen = depth !== 0; if (this.element) { this.removeChildren(false); @@ -223,8 +234,11 @@ export class JsonExplorer { } isNumberArray() { - return (this.json.length > 0 && this.json.length < 4) && - (_.isNumber(this.json[0]) || _.isNumber(this.json[1])); + return ( + this.json.length > 0 && + this.json.length < 4 && + (_.isNumber(this.json[0]) || _.isNumber(this.json[1])) + ); } renderArray() { @@ -235,13 +249,17 @@ export class JsonExplorer { if (this.isNumberArray()) { this.json.forEach((val, index) => { if (index > 0) { - arrayWrapperSpan.appendChild(createElement('span', 'array-comma', ',')); + arrayWrapperSpan.appendChild( + createElement('span', 'array-comma', ',') + ); } arrayWrapperSpan.appendChild(createElement('span', 'number', val)); }); this.skipChildren = true; } else { - arrayWrapperSpan.appendChild(createElement('span', 'number', (this.json.length))); + arrayWrapperSpan.appendChild( + createElement('span', 'number', this.json.length) + ); } arrayWrapperSpan.appendChild(createElement('span', 'bracket', ']')); @@ -280,7 +298,11 @@ export class JsonExplorer { const objectWrapperSpan = createElement('span'); // get constructor name and append it to wrapper span - var constructorName = createElement('span', 'constructor-name', this.constructorName); + var constructorName = createElement( + 'span', + 'constructor-name', + this.constructorName + ); objectWrapperSpan.appendChild(constructorName); // if it's an array append the array specific elements like brackets and length @@ -294,7 +316,6 @@ export class JsonExplorer { togglerLink.appendChild(value); // Primitive values } else { - // make a value holder element const value = this.isUrl ? createElement('a') : createElement('span'); @@ -366,17 +387,24 @@ export class JsonExplorer { /** * Appends all the children to children element * Animated option is used when user triggers this via a click - */ + */ appendChildren(animated = false) { const children = this.element.querySelector(`div.${cssClass('children')}`); - if (!children || this.isEmpty) { return; } + if (!children || this.isEmpty) { + return; + } if (animated) { let index = 0; - const addAChild = ()=> { + const addAChild = () => { const key = this.keys[index]; - const formatter = new JsonExplorer(this.json[key], this.open - 1, this.config, key); + const formatter = new JsonExplorer( + this.json[key], + this.open - 1, + this.config, + key + ); children.appendChild(formatter.render()); index += 1; @@ -391,10 +419,14 @@ export class JsonExplorer { }; requestAnimationFrame(addAChild); - } else { this.keys.forEach(key => { - const formatter = new JsonExplorer(this.json[key], this.open - 1, this.config, key); + const formatter = new JsonExplorer( + this.json[key], + this.open - 1, + this.config, + key + ); children.appendChild(formatter.render()); }); } @@ -403,13 +435,15 @@ export class JsonExplorer { /** * Removes all the children from children element * Animated option is used when user triggers this via a click - */ + */ removeChildren(animated = false) { - const childrenElement = this.element.querySelector(`div.${cssClass('children')}`) as HTMLDivElement; + const childrenElement = this.element.querySelector( + `div.${cssClass('children')}` + ) as HTMLDivElement; if (animated) { let childrenRemoved = 0; - const removeAChild = ()=> { + const removeAChild = () => { if (childrenElement && childrenElement.children.length) { childrenElement.removeChild(childrenElement.children[0]); childrenRemoved += 1; diff --git a/public/app/core/components/jsontree/jsontree.ts b/public/app/core/components/jsontree/jsontree.ts index 52fb64e1c87..e127d7b14a9 100644 --- a/public/app/core/components/jsontree/jsontree.ts +++ b/public/app/core/components/jsontree/jsontree.ts @@ -1,22 +1,23 @@ import coreModule from 'app/core/core_module'; -import {JsonExplorer} from '../json_explorer/json_explorer'; +import { JsonExplorer } from '../json_explorer/json_explorer'; -coreModule.directive('jsonTree', [function jsonTreeDirective() { - return{ - restrict: 'E', - scope: { - object: '=', - startExpanded: '@', - rootName: '@', - }, - link: function(scope, elem) { +coreModule.directive('jsonTree', [ + function jsonTreeDirective() { + return { + restrict: 'E', + scope: { + object: '=', + startExpanded: '@', + rootName: '@', + }, + link: function(scope, elem) { + var jsonExp = new JsonExplorer(scope.object, 3, { + animateOpen: true, + }); - var jsonExp = new JsonExplorer(scope.object, 3, { - animateOpen: true - }); - - const html = jsonExp.render(true); - elem.html(html); - } - }; -}]); + const html = jsonExp.render(true); + elem.html(html); + }, + }; + }, +]); diff --git a/public/app/core/components/layout_selector/layout_selector.ts b/public/app/core/components/layout_selector/layout_selector.ts index 98f806cd63e..91a3afea250 100644 --- a/public/app/core/components/layout_selector/layout_selector.ts +++ b/public/app/core/components/layout_selector/layout_selector.ts @@ -31,7 +31,6 @@ export class LayoutSelectorCtrl { store.set('grafana.list.layout.mode', 'grid'); this.$rootScope.appEvent('layout-mode-changed', 'grid'); } - } /** @ngInject **/ @@ -56,12 +55,16 @@ export function layoutMode($rootScope) { var className = 'card-list-layout-' + layout; elem.addClass(className); - $rootScope.onAppEvent('layout-mode-changed', (evt, newLayout) => { - elem.removeClass(className); - className = 'card-list-layout-' + newLayout; - elem.addClass(className); - }, scope); - } + $rootScope.onAppEvent( + 'layout-mode-changed', + (evt, newLayout) => { + elem.removeClass(className); + className = 'card-list-layout-' + newLayout; + elem.addClass(className); + }, + scope + ); + }, }; } diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.html b/public/app/core/components/manage_dashboards/manage_dashboards.html index f8c96a78b2e..90091f3032c 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.html +++ b/public/app/core/components/manage_dashboards/manage_dashboards.html @@ -60,20 +60,24 @@ switch-class="gf-form-switch--transparent gf-form-switch--search-result-filter-row__checkbox" />
- +
+ +