diff --git a/package.json b/package.json index 8197a695e72..8ba62f97a25 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Grafana Labs" }, "name": "grafana", - "version": "5.0.0", + "version": "5.0.2", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" diff --git a/pkg/api/api.go b/pkg/api/api.go index 1b7e30e34ad..5b3cde09fd5 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -66,6 +66,7 @@ func (hs *HttpServer) registerRoutes() { r.Get("/plugins/:id/page/:page", reqSignedIn, Index) r.Get("/d/:uid/:slug", reqSignedIn, Index) + r.Get("/d/:uid", reqSignedIn, Index) r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardUrl, Index) r.Get("/dashboard/script/*", reqSignedIn, Index) r.Get("/dashboard-solo/snapshot/*", Index) @@ -150,11 +151,11 @@ func (hs *HttpServer) registerRoutes() { apiRoute.Group("/teams", func(teamsRoute RouteRegister) { teamsRoute.Get("/:teamId", wrap(GetTeamById)) teamsRoute.Get("/search", wrap(SearchTeams)) - teamsRoute.Post("/", quota("teams"), bind(m.CreateTeamCommand{}), wrap(CreateTeam)) + teamsRoute.Post("/", bind(m.CreateTeamCommand{}), wrap(CreateTeam)) teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam)) teamsRoute.Delete("/:teamId", wrap(DeleteTeamById)) teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers)) - teamsRoute.Post("/:teamId/members", quota("teams"), bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember)) + teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember)) teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember)) }, reqOrgAdmin) diff --git a/pkg/components/renderer/renderer.go b/pkg/components/renderer/renderer.go index 313f7892707..26751ddd5c7 100644 --- a/pkg/components/renderer/renderer.go +++ b/pkg/components/renderer/renderer.go @@ -72,7 +72,9 @@ func RenderToPng(params *RenderOpts) (string, error) { localDomain = setting.HttpAddr } - url := fmt.Sprintf("%s://%s:%s/%s", setting.Protocol, localDomain, setting.HttpPort, params.Path) + // &render=1 signals to the legacy redirect layer to + // avoid redirect these requests. + url := fmt.Sprintf("%s://%s:%s/%s&render=1", setting.Protocol, localDomain, setting.HttpPort, params.Path) binPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, executable)) scriptPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, "render.js")) diff --git a/pkg/middleware/dashboard_redirect.go b/pkg/middleware/dashboard_redirect.go index 4a3812fb8a2..c3262253ac9 100644 --- a/pkg/middleware/dashboard_redirect.go +++ b/pkg/middleware/dashboard_redirect.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" "gopkg.in/macaron.v1" ) @@ -36,9 +37,14 @@ func RedirectFromLegacyDashboardUrl() macaron.Handler { func RedirectFromLegacyDashboardSoloUrl() macaron.Handler { return func(c *Context) { slug := c.Params("slug") + renderRequest := c.QueryBool("render") if slug != "" { if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil { + if renderRequest && strings.Contains(url, setting.AppSubUrl) { + url = strings.Replace(url, setting.AppSubUrl, "", 1) + } + url = strings.Replace(url, "/d/", "/d-solo/", 1) url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery) c.Redirect(url, 301) diff --git a/pkg/middleware/session.go b/pkg/middleware/session.go index 4de111ff3d2..5389ad36bdf 100644 --- a/pkg/middleware/session.go +++ b/pkg/middleware/session.go @@ -113,6 +113,18 @@ type SessionWrapper struct { } func (s *SessionWrapper) Start(c *Context) error { + // See https://github.com/grafana/grafana/issues/11155 for details on why + // a recover and retry is needed + defer func() error { + if err := recover(); err != nil { + var retryErr error + s.session, retryErr = s.manager.Start(c.Context) + return retryErr + } + + return nil + }() + var err error s.session, err = s.manager.Start(c.Context) return err diff --git a/pkg/plugins/dashboard_importer.go b/pkg/plugins/dashboard_importer.go index 53012f3e817..fb4d63a1fe4 100644 --- a/pkg/plugins/dashboard_importer.go +++ b/pkg/plugins/dashboard_importer.go @@ -80,7 +80,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error { User: cmd.User, } - savedDash, err := dashboards.NewService().SaveDashboard(dto) + savedDash, err := dashboards.NewService().ImportDashboard(dto) if err != nil { return err diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index 5206c81642e..2ae26c1a382 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -74,6 +74,21 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json) for _, panelObj := range jsonWithPanels.Get("panels").MustArray() { panel := simplejson.NewFromAny(panelObj) + + collapsedJson, collapsed := panel.CheckGet("collapsed") + // check if the panel is collapsed + if collapsed && collapsedJson.MustBool() { + + // extract alerts from sub panels for collapsed panels + als, err := e.GetAlertFromPanels(panel) + if err != nil { + return nil, err + } + + alerts = append(alerts, als...) + continue + } + jsonAlert, hasAlert := panel.CheckGet("alert") if !hasAlert { diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index f8b678e66bd..3bda6c771fb 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -22,6 +22,7 @@ func TestAlertRuleExtraction(t *testing.T) { defaultDs := &m.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true} graphite2Ds := &m.DataSource{Id: 15, OrgId: 1, Name: "graphite2"} influxDBDs := &m.DataSource{Id: 16, OrgId: 1, Name: "InfluxDB"} + prom := &m.DataSource{Id: 17, OrgId: 1, Name: "Prometheus"} bus.AddHandler("test", func(query *m.GetDataSourcesQuery) error { query.Result = []*m.DataSource{defaultDs, graphite2Ds} @@ -38,6 +39,10 @@ func TestAlertRuleExtraction(t *testing.T) { if query.Name == influxDBDs.Name { query.Result = influxDBDs } + if query.Name == prom.Name { + query.Result = prom + } + return nil }) @@ -214,5 +219,26 @@ func TestAlertRuleExtraction(t *testing.T) { } }) }) + + Convey("Should be able to extract collapsed panels", func() { + json, err := ioutil.ReadFile("./test-data/collapsed-panels.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 be able to extract collapsed alerts", func() { + So(len(alerts), ShouldEqual, 4) + }) + }) }) } diff --git a/pkg/services/alerting/test-data/collapsed-panels.json b/pkg/services/alerting/test-data/collapsed-panels.json new file mode 100644 index 00000000000..29109788955 --- /dev/null +++ b/pkg/services/alerting/test-data/collapsed-panels.json @@ -0,0 +1,597 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 127, + "links": [], + "panels": [ + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 9, + "title": "Row title", + "type": "row" + }, + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 200 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "frequency": "10s", + "handler": 1, + "name": "Panel Title alert", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 10, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "go_goroutines", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{job}}", + "refId": "A" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 200 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Panel Title", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 14, + "limit": 10, + "links": [], + "onlyAlertsOnDashboard": true, + "show": "current", + "sortOrder": 1, + "stateFilter": [], + "title": "Panel Title", + "type": "alertlist" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 10 + }, + "id": 6, + "panels": [ + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 200 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "frequency": "10s", + "handler": 1, + "name": "Panel 2 alert", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 11 + }, + "id": 11, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "go_goroutines", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{job}}", + "refId": "A" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 200 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Panel 2", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 200 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "frequency": "10s", + "handler": 1, + "name": "Panel 4 alert", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 11 + }, + "id": 15, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "go_goroutines", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{job}}", + "refId": "A" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 200 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Panel 4", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "title": "Row title", + "type": "row" + }, + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 11 + }, + "id": 4, + "title": "Row title", + "type": "row" + }, + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 200 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "frequency": "10s", + "handler": 1, + "name": "Panel 3 alert", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 12 + }, + "id": 12, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "go_goroutines", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{job}}", + "refId": "A" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 200 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Panel 3", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "schemaVersion": 16, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "New dashboard Copy", + "uid": "6v5pg36zk", + "version": 17 +} diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/dashboard_service.go index 1f39394d757..02a6ffc8330 100644 --- a/pkg/services/dashboards/dashboard_service.go +++ b/pkg/services/dashboards/dashboard_service.go @@ -13,6 +13,7 @@ import ( // DashboardService service for operating on dashboards type DashboardService interface { SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) + ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) } // DashboardProvisioningService service for operating on provisioned dashboards @@ -214,6 +215,20 @@ func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Da return cmd.Result, nil } +func (dr *dashboardServiceImpl) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) { + cmd, err := dr.buildSaveDashboardCommand(dto, false) + if err != nil { + return nil, err + } + + err = bus.Dispatch(cmd) + if err != nil { + return nil, err + } + + return cmd.Result, nil +} + type FakeDashboardService struct { SaveDashboardResult *models.Dashboard SaveDashboardError error @@ -230,6 +245,10 @@ func (s *FakeDashboardService) SaveDashboard(dto *SaveDashboardDTO) (*models.Das return s.SaveDashboardResult, s.SaveDashboardError } +func (s *FakeDashboardService) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) { + return s.SaveDashboard(dto) +} + func MockDashboardService(mock *FakeDashboardService) { NewService = func() DashboardService { return mock diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index cab45b5aff3..487e9b27b22 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -22,6 +22,12 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { reloadOnSearch: false, pageClass: 'page-dashboard', }) + .when('/d/:uid', { + templateUrl: 'public/app/partials/dashboard.html', + controller: 'LoadDashboardCtrl', + reloadOnSearch: false, + pageClass: 'page-dashboard', + }) .when('/dashboard/:type/:slug', { templateUrl: 'public/app/partials/dashboard.html', controller: 'LoadDashboardCtrl', @@ -98,6 +104,11 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { controller: 'FolderDashboardsCtrl', controllerAs: 'ctrl', }) + .when('/dashboards/f/:uid', { + templateUrl: 'public/app/features/dashboard/partials/folder_dashboards.html', + controller: 'FolderDashboardsCtrl', + controllerAs: 'ctrl', + }) .when('/org', { templateUrl: 'public/app/features/org/partials/orgDetails.html', controller: 'OrgDetailsCtrl',