diff --git a/pkg/api/api.go b/pkg/api/api.go index 2d45868e58d..6d2207971e7 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -15,6 +15,8 @@ func (hs *HttpServer) registerRoutes() { reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true}) reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN) reqOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN) + redirectFromLegacyDashboardUrl := middleware.RedirectFromLegacyDashboardUrl() + redirectFromLegacyDashboardSoloUrl := middleware.RedirectFromLegacyDashboardSoloUrl() quota := middleware.Quota bind := binding.Bind @@ -63,9 +65,13 @@ func (hs *HttpServer) registerRoutes() { r.Get("/plugins/:id/edit", reqSignedIn, Index) r.Get("/plugins/:id/page/:page", reqSignedIn, Index) - r.Get("/dashboard/*", reqSignedIn, Index) + r.Get("/d/:uid/:slug", reqSignedIn, Index) + r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardUrl, Index) + r.Get("/dashboard/script/*", reqSignedIn, Index) r.Get("/dashboard-solo/snapshot/*", Index) - r.Get("/dashboard-solo/*", reqSignedIn, Index) + r.Get("/d-solo/:uid/:slug", reqSignedIn, Index) + r.Get("/dashboard-solo/db/:slug", reqSignedIn, redirectFromLegacyDashboardSoloUrl, Index) + r.Get("/dashboard-solo/script/*", reqSignedIn, Index) r.Get("/import/dashboard", reqSignedIn, Index) r.Get("/dashboards/", reqSignedIn, Index) r.Get("/dashboards/*", reqSignedIn, Index) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 77d3d8882f5..b85387495b8 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -238,8 +238,22 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { return ApiError(500, "Invalid alert data. Cannot save dashboard", err) } + var url string + if dash.IsFolder { + url = m.GetFolderUrl(dashboard.Uid, dashboard.Slug) + } else { + url = m.GetDashboardUrl(dashboard.Uid, dashboard.Slug) + } + c.TimeRequest(metrics.M_Api_Dashboard_Save) - return Json(200, util.DynMap{"status": "success", "slug": dashboard.Slug, "version": dashboard.Version, "id": dashboard.Id, "uid": dashboard.Uid}) + return Json(200, util.DynMap{ + "status": "success", + "slug": dashboard.Slug, + "version": dashboard.Version, + "id": dashboard.Id, + "uid": dashboard.Uid, + "url": url, + }) } func GetHomeDashboard(c *middleware.Context) Response { diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index cc718229f96..0bd586f6067 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -180,13 +180,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) { - CallPostDashboard(sc) - So(sc.resp.Code, ShouldEqual, 200) - result := sc.ToJson() - So(result.Get("status").MustString(), ShouldEqual, "success") - So(result.Get("id").MustInt64(), ShouldBeGreaterThan, 0) - So(result.Get("uid").MustString(), ShouldNotBeNil) - So(result.Get("slug").MustString(), ShouldNotBeNil) + CallPostDashboardShouldReturnSuccess(sc) }) Convey("When saving a dashboard folder in another folder", func() { @@ -423,13 +417,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) { - CallPostDashboard(sc) - So(sc.resp.Code, ShouldEqual, 200) - result := sc.ToJson() - So(result.Get("status").MustString(), ShouldEqual, "success") - So(result.Get("id").MustInt64(), ShouldBeGreaterThan, 0) - So(result.Get("uid").MustString(), ShouldNotBeNil) - So(result.Get("slug").MustString(), ShouldNotBeNil) + CallPostDashboardShouldReturnSuccess(sc) }) }) @@ -544,13 +532,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) { - CallPostDashboard(sc) - So(sc.resp.Code, ShouldEqual, 200) - result := sc.ToJson() - So(result.Get("status").MustString(), ShouldEqual, "success") - So(result.Get("id").MustInt64(), ShouldBeGreaterThan, 0) - So(result.Get("uid").MustString(), ShouldNotBeNil) - So(result.Get("slug").MustString(), ShouldNotBeNil) + CallPostDashboardShouldReturnSuccess(sc) }) }) @@ -678,6 +660,18 @@ func CallPostDashboard(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() } +func CallPostDashboardShouldReturnSuccess(sc *scenarioContext) { + CallPostDashboard(sc) + + So(sc.resp.Code, ShouldEqual, 200) + result := sc.ToJson() + So(result.Get("status").MustString(), ShouldEqual, "success") + So(result.Get("id").MustInt64(), ShouldBeGreaterThan, 0) + So(result.Get("uid").MustString(), ShouldNotBeNil) + So(result.Get("slug").MustString(), ShouldNotBeNil) + So(result.Get("url").MustString(), ShouldNotBeNil) +} + func postDashboardScenario(desc string, url string, routePattern string, role m.RoleType, cmd m.SaveDashboardCommand, fn scenarioFunc) { Convey(desc+" "+url, func() { defer bus.ClearBusHandlers() diff --git a/pkg/middleware/dashboard_redirect.go b/pkg/middleware/dashboard_redirect.go new file mode 100644 index 00000000000..1ca4ef741c6 --- /dev/null +++ b/pkg/middleware/dashboard_redirect.go @@ -0,0 +1,46 @@ +package middleware + +import ( + "strings" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + "gopkg.in/macaron.v1" +) + +func getDashboardUrlBySlug(orgId int64, slug string) (string, error) { + query := m.GetDashboardQuery{Slug: slug, OrgId: orgId} + + if err := bus.Dispatch(&query); err != nil { + return "", m.ErrDashboardNotFound + } + + return m.GetDashboardUrl(query.Result.Uid, query.Result.Slug), nil +} + +func RedirectFromLegacyDashboardUrl() macaron.Handler { + return func(c *Context) { + slug := c.Params("slug") + + if slug != "" { + if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil { + c.Redirect(url, 301) + return + } + } + } +} + +func RedirectFromLegacyDashboardSoloUrl() macaron.Handler { + return func(c *Context) { + slug := c.Params("slug") + + if slug != "" { + if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil { + url = strings.Replace(url, "/d/", "/d-solo/", 1) + c.Redirect(url, 301) + return + } + } + } +} diff --git a/pkg/middleware/dashboard_redirect_test.go b/pkg/middleware/dashboard_redirect_test.go new file mode 100644 index 00000000000..bff4ee2253c --- /dev/null +++ b/pkg/middleware/dashboard_redirect_test.go @@ -0,0 +1,54 @@ +package middleware + +import ( + "strings" + "testing" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +func TestMiddlewareDashboardRedirect(t *testing.T) { + Convey("Given the dashboard redirect middleware", t, func() { + bus.ClearBusHandlers() + redirectFromLegacyDashboardUrl := RedirectFromLegacyDashboardUrl() + redirectFromLegacyDashboardSoloUrl := RedirectFromLegacyDashboardSoloUrl() + + fakeDash := m.NewDashboard("Child dash") + fakeDash.Id = 1 + fakeDash.FolderId = 1 + fakeDash.HasAcl = false + + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { + query.Result = fakeDash + return nil + }) + + middlewareScenario("GET dashboard by legacy url", func(sc *scenarioContext) { + sc.m.Get("/dashboard/db/:slug", redirectFromLegacyDashboardUrl, sc.defaultHandler) + + sc.fakeReqWithParams("GET", "/dashboard/db/dash", map[string]string{}).exec() + + Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() { + So(sc.resp.Code, ShouldEqual, 301) + redirectUrl, _ := sc.resp.Result().Location() + So(redirectUrl.Path, ShouldEqual, m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug)) + }) + }) + + middlewareScenario("GET dashboard solo by legacy url", func(sc *scenarioContext) { + sc.m.Get("/dashboard-solo/db/:slug", redirectFromLegacyDashboardSoloUrl, sc.defaultHandler) + + sc.fakeReqWithParams("GET", "/dashboard-solo/db/dash", map[string]string{}).exec() + + Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() { + So(sc.resp.Code, ShouldEqual, 301) + redirectUrl, _ := sc.resp.Result().Location() + expectedUrl := m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug) + expectedUrl = strings.Replace(expectedUrl, "/d/", "/d-solo/", 1) + So(redirectUrl.Path, ShouldEqual, expectedUrl) + }) + }) + }) +} diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 0d9e0e5b973..ffd8e8a0af0 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -399,6 +399,20 @@ func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext { return sc } +func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map[string]string) *scenarioContext { + sc.resp = httptest.NewRecorder() + req, err := http.NewRequest(method, url, nil) + q := req.URL.Query() + for k, v := range queryParams { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + So(err, ShouldBeNil) + sc.req = req + + return sc +} + func (sc *scenarioContext) handler(fn handlerFunc) *scenarioContext { sc.handlerFunc = fn return sc diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 3204db0ce5d..9ade4264bb3 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -225,6 +225,10 @@ export class BackendSrv { return this.get('/api/dashboards/' + type + '/' + slug); } + getDashboardByUid(uid: string) { + return this.get(`/api/dashboards/uid/${uid}`); + } + saveDashboard(dash, options) { options = options || {}; diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts index a909b4af09f..a0989e74aed 100644 --- a/public/app/core/services/search_srv.ts +++ b/public/app/core/services/search_srv.ts @@ -41,10 +41,7 @@ export class SearchSrv { .map(orderId => { return _.find(result, { id: orderId }); }) - .filter(hit => hit && !hit.isStarred) - .map(hit => { - return this.transformToViewModel(hit); - }); + .filter(hit => hit && !hit.isStarred); }); } @@ -81,17 +78,12 @@ export class SearchSrv { score: -2, expanded: this.starredIsOpen, toggle: this.toggleStarred.bind(this), - items: result.map(this.transformToViewModel), + items: result, }; } }); } - private transformToViewModel(hit) { - hit.url = 'dashboard/db/' + hit.slug; - return hit; - } - search(options) { let sections: any = {}; let promises = []; @@ -181,7 +173,7 @@ export class SearchSrv { } section.expanded = true; - section.items.push(this.transformToViewModel(hit)); + section.items.push(hit); } } @@ -198,7 +190,7 @@ export class SearchSrv { }; return this.backendSrv.search(query).then(results => { - section.items = _.map(results, this.transformToViewModel); + section.items = results; return Promise.resolve(section); }); } diff --git a/public/app/features/dashboard/dashboard_loader_srv.ts b/public/app/features/dashboard/dashboard_loader_srv.ts index e4def4385e1..9e458c4e4bb 100644 --- a/public/app/features/dashboard/dashboard_loader_srv.ts +++ b/public/app/features/dashboard/dashboard_loader_srv.ts @@ -35,18 +35,18 @@ export class DashboardLoaderSrv { }; } - loadDashboard(type, slug) { + loadDashboard(type, slug, uid) { var promise; if (type === 'script') { promise = this._loadScriptedDashboard(slug); } else if (type === 'snapshot') { - promise = this.backendSrv.get('/api/snapshots/' + this.$routeParams.slug).catch(() => { + promise = this.backendSrv.get('/api/snapshots/' + slug).catch(() => { return this._dashboardLoadFailed('Snapshot not found', true); }); } else { promise = this.backendSrv - .getDashboard(this.$routeParams.type, this.$routeParams.slug) + .getDashboardByUid(uid) .then(result => { if (result.meta.isFolder) { this.$rootScope.appEvent('alert-error', ['Dashboard not found']); diff --git a/public/app/features/dashboard/dashboard_srv.ts b/public/app/features/dashboard/dashboard_srv.ts index 708ba047d2b..07fc14f2f2e 100644 --- a/public/app/features/dashboard/dashboard_srv.ts +++ b/public/app/features/dashboard/dashboard_srv.ts @@ -73,9 +73,8 @@ export class DashboardSrv { postSave(clone, data) { this.dash.version = data.version; - var dashboardUrl = '/dashboard/db/' + data.slug; - if (dashboardUrl !== this.$location.path()) { - this.$location.url(dashboardUrl); + if (data.url !== this.$location.path()) { + this.$location.url(data.url); } this.$rootScope.appEvent('dashboard-saved', this.dash); diff --git a/public/app/features/dashboard/shareModalCtrl.ts b/public/app/features/dashboard/shareModalCtrl.ts index 508658cd53b..8e30eaef91e 100644 --- a/public/app/features/dashboard/shareModalCtrl.ts +++ b/public/app/features/dashboard/shareModalCtrl.ts @@ -74,6 +74,7 @@ export class ShareModalCtrl { $scope.shareUrl = linkSrv.addParamsToUrl(baseUrl, params); var soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/'); + soloUrl = soloUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/'); delete params.fullscreen; delete params.edit; soloUrl = linkSrv.addParamsToUrl(soloUrl, params); @@ -84,6 +85,7 @@ export class ShareModalCtrl { config.appSubUrl + '/dashboard-solo/', config.appSubUrl + '/render/dashboard-solo/' ); + $scope.imageUrl = $scope.imageUrl.replace(config.appSubUrl + '/d-solo/', config.appSubUrl + '/render/d-solo/'); $scope.imageUrl += '&width=1000'; $scope.imageUrl += '&height=500'; $scope.imageUrl += '&tz=UTC' + encodeURIComponent(moment().format('Z')); diff --git a/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts b/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts index 8bf8f53ed46..fc70a54a41c 100644 --- a/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts +++ b/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts @@ -43,12 +43,23 @@ describe('ShareModalCtrl', function() { }); it('should generate render url', function() { - ctx.$location.$$absUrl = 'http://dashboards.grafana.com/dashboard/db/my-dash'; + ctx.$location.$$absUrl = 'http://dashboards.grafana.com/d/abcdefghi/my-dash'; ctx.scope.panel = { id: 22 }; ctx.scope.init(); - var base = 'http://dashboards.grafana.com/render/dashboard-solo/db/my-dash'; + var base = 'http://dashboards.grafana.com/render/d-solo/abcdefghi/my-dash'; + var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC'; + expect(ctx.scope.imageUrl).to.contain(base + params); + }); + + it('should generate render url for scripted dashboard', function() { + ctx.$location.$$absUrl = 'http://dashboards.grafana.com/dashboard/script/my-dash.js'; + + ctx.scope.panel = { id: 22 }; + + ctx.scope.init(); + var base = 'http://dashboards.grafana.com/render/dashboard-solo/script/my-dash.js'; var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC'; expect(ctx.scope.imageUrl).to.contain(base + params); }); diff --git a/public/app/features/panel/solo_panel_ctrl.ts b/public/app/features/panel/solo_panel_ctrl.ts index f141f89eb80..575c9e4c3b9 100644 --- a/public/app/features/panel/solo_panel_ctrl.ts +++ b/public/app/features/panel/solo_panel_ctrl.ts @@ -2,7 +2,7 @@ import angular from 'angular'; export class SoloPanelCtrl { /** @ngInject */ - constructor($scope, $routeParams, $location, dashboardLoaderSrv, contextSrv) { + constructor($scope, $routeParams, $location, dashboardLoaderSrv, contextSrv, backendSrv) { var panelId; $scope.init = function() { @@ -13,7 +13,17 @@ export class SoloPanelCtrl { $scope.onAppEvent('dashboard-initialized', $scope.initPanelScope); - dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug).then(function(result) { + // if no uid, redirect to new route based on slug + if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) { + backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => { + if (res) { + $location.path(res.meta.url.replace('/d/', '/d-solo/')); + } + }); + return; + } + + dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(function(result) { result.meta.soloMode = true; $scope.initDashboard(result, $scope); }); diff --git a/public/app/plugins/panel/dashlist/module.html b/public/app/plugins/panel/dashlist/module.html index 7f71811ac08..8fa3e7ef71f 100644 --- a/public/app/plugins/panel/dashlist/module.html +++ b/public/app/plugins/panel/dashlist/module.html @@ -4,7 +4,7 @@ {{group.header}}