Kaynağa Gözat

Merge remote-tracking branch 'origin/7883_frontend_step' into 7883_backend

* origin/7883_frontend_step:
  dashboards: make scripted dashboards work using the old legacy urls
  dashboards: redirect from old url used to load dashboard to new url
  dashboards: add new default frontend route for rendering a dashboard panel
  dashboards: fix links to recently viewed and starred dashboards
  dashboards: use new *url* prop from dashboard search for linking to dashboards
  dashboards: when saving dashboard redirect if url changes
  dashboards: add new default frontend route for loading a dashboard
  dashboards: return url in response to save dashboard. #7883
bergquist 8 yıl önce
ebeveyn
işleme
9aa488c084

+ 8 - 2
pkg/api/api.go

@@ -15,6 +15,8 @@ func (hs *HttpServer) registerRoutes() {
 	reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
 	reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
 	reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
 	reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
 	reqOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
 	reqOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
+	redirectFromLegacyDashboardUrl := middleware.RedirectFromLegacyDashboardUrl()
+	redirectFromLegacyDashboardSoloUrl := middleware.RedirectFromLegacyDashboardSoloUrl()
 	quota := middleware.Quota
 	quota := middleware.Quota
 	bind := binding.Bind
 	bind := binding.Bind
 
 
@@ -63,9 +65,13 @@ func (hs *HttpServer) registerRoutes() {
 	r.Get("/plugins/:id/edit", reqSignedIn, Index)
 	r.Get("/plugins/:id/edit", reqSignedIn, Index)
 	r.Get("/plugins/:id/page/:page", 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/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("/import/dashboard", reqSignedIn, Index)
 	r.Get("/dashboards/", reqSignedIn, Index)
 	r.Get("/dashboards/", reqSignedIn, Index)
 	r.Get("/dashboards/*", reqSignedIn, Index)
 	r.Get("/dashboards/*", reqSignedIn, Index)

+ 15 - 1
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)
 		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)
 	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 {
 func GetHomeDashboard(c *middleware.Context) Response {

+ 15 - 21
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) {
 			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() {
 			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) {
 			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) {
 			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()
 	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) {
 func postDashboardScenario(desc string, url string, routePattern string, role m.RoleType, cmd m.SaveDashboardCommand, fn scenarioFunc) {
 	Convey(desc+" "+url, func() {
 	Convey(desc+" "+url, func() {
 		defer bus.ClearBusHandlers()
 		defer bus.ClearBusHandlers()

+ 46 - 0
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
+			}
+		}
+	}
+}

+ 54 - 0
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)
+			})
+		})
+	})
+}

+ 14 - 0
pkg/middleware/middleware_test.go

@@ -399,6 +399,20 @@ func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
 	return sc
 	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 {
 func (sc *scenarioContext) handler(fn handlerFunc) *scenarioContext {
 	sc.handlerFunc = fn
 	sc.handlerFunc = fn
 	return sc
 	return sc

+ 4 - 0
public/app/core/services/backend_srv.ts

@@ -225,6 +225,10 @@ export class BackendSrv {
     return this.get('/api/dashboards/' + type + '/' + slug);
     return this.get('/api/dashboards/' + type + '/' + slug);
   }
   }
 
 
+  getDashboardByUid(uid: string) {
+    return this.get(`/api/dashboards/uid/${uid}`);
+  }
+
   saveDashboard(dash, options) {
   saveDashboard(dash, options) {
     options = options || {};
     options = options || {};
 
 

+ 4 - 12
public/app/core/services/search_srv.ts

@@ -41,10 +41,7 @@ export class SearchSrv {
         .map(orderId => {
         .map(orderId => {
           return _.find(result, { id: 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,
           score: -2,
           expanded: this.starredIsOpen,
           expanded: this.starredIsOpen,
           toggle: this.toggleStarred.bind(this),
           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) {
   search(options) {
     let sections: any = {};
     let sections: any = {};
     let promises = [];
     let promises = [];
@@ -181,7 +173,7 @@ export class SearchSrv {
       }
       }
 
 
       section.expanded = true;
       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 => {
     return this.backendSrv.search(query).then(results => {
-      section.items = _.map(results, this.transformToViewModel);
+      section.items = results;
       return Promise.resolve(section);
       return Promise.resolve(section);
     });
     });
   }
   }

+ 3 - 3
public/app/features/dashboard/dashboard_loader_srv.ts

@@ -35,18 +35,18 @@ export class DashboardLoaderSrv {
     };
     };
   }
   }
 
 
-  loadDashboard(type, slug) {
+  loadDashboard(type, slug, uid) {
     var promise;
     var promise;
 
 
     if (type === 'script') {
     if (type === 'script') {
       promise = this._loadScriptedDashboard(slug);
       promise = this._loadScriptedDashboard(slug);
     } else if (type === 'snapshot') {
     } 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);
         return this._dashboardLoadFailed('Snapshot not found', true);
       });
       });
     } else {
     } else {
       promise = this.backendSrv
       promise = this.backendSrv
-        .getDashboard(this.$routeParams.type, this.$routeParams.slug)
+        .getDashboardByUid(uid)
         .then(result => {
         .then(result => {
           if (result.meta.isFolder) {
           if (result.meta.isFolder) {
             this.$rootScope.appEvent('alert-error', ['Dashboard not found']);
             this.$rootScope.appEvent('alert-error', ['Dashboard not found']);

+ 2 - 3
public/app/features/dashboard/dashboard_srv.ts

@@ -73,9 +73,8 @@ export class DashboardSrv {
   postSave(clone, data) {
   postSave(clone, data) {
     this.dash.version = data.version;
     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);
     this.$rootScope.appEvent('dashboard-saved', this.dash);

+ 2 - 0
public/app/features/dashboard/shareModalCtrl.ts

@@ -74,6 +74,7 @@ export class ShareModalCtrl {
       $scope.shareUrl = linkSrv.addParamsToUrl(baseUrl, params);
       $scope.shareUrl = linkSrv.addParamsToUrl(baseUrl, params);
 
 
       var soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/');
       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.fullscreen;
       delete params.edit;
       delete params.edit;
       soloUrl = linkSrv.addParamsToUrl(soloUrl, params);
       soloUrl = linkSrv.addParamsToUrl(soloUrl, params);
@@ -84,6 +85,7 @@ export class ShareModalCtrl {
         config.appSubUrl + '/dashboard-solo/',
         config.appSubUrl + '/dashboard-solo/',
         config.appSubUrl + '/render/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 += '&width=1000';
       $scope.imageUrl += '&height=500';
       $scope.imageUrl += '&height=500';
       $scope.imageUrl += '&tz=UTC' + encodeURIComponent(moment().format('Z'));
       $scope.imageUrl += '&tz=UTC' + encodeURIComponent(moment().format('Z'));

+ 13 - 2
public/app/features/dashboard/specs/share_modal_ctrl_specs.ts

@@ -43,12 +43,23 @@ describe('ShareModalCtrl', function() {
     });
     });
 
 
     it('should generate render url', 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.panel = { id: 22 };
 
 
       ctx.scope.init();
       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';
       var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC';
       expect(ctx.scope.imageUrl).to.contain(base + params);
       expect(ctx.scope.imageUrl).to.contain(base + params);
     });
     });

+ 12 - 2
public/app/features/panel/solo_panel_ctrl.ts

@@ -2,7 +2,7 @@ import angular from 'angular';
 
 
 export class SoloPanelCtrl {
 export class SoloPanelCtrl {
   /** @ngInject */
   /** @ngInject */
-  constructor($scope, $routeParams, $location, dashboardLoaderSrv, contextSrv) {
+  constructor($scope, $routeParams, $location, dashboardLoaderSrv, contextSrv, backendSrv) {
     var panelId;
     var panelId;
 
 
     $scope.init = function() {
     $scope.init = function() {
@@ -13,7 +13,17 @@ export class SoloPanelCtrl {
 
 
       $scope.onAppEvent('dashboard-initialized', $scope.initPanelScope);
       $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;
         result.meta.soloMode = true;
         $scope.initDashboard(result, $scope);
         $scope.initDashboard(result, $scope);
       });
       });

+ 1 - 1
public/app/plugins/panel/dashlist/module.html

@@ -4,7 +4,7 @@
       {{group.header}}
       {{group.header}}
     </h6>
     </h6>
     <div class="dashlist-item" ng-repeat="dash in group.list">
     <div class="dashlist-item" ng-repeat="dash in group.list">
-      <a class="dashlist-link dashlist-link-{{dash.type}}" href="dashboard/{{dash.uri}}">
+      <a class="dashlist-link dashlist-link-{{dash.type}}" href="{{dash.url}}">
         <span class="dashlist-title">
         <span class="dashlist-title">
           {{dash.title}}
           {{dash.title}}
         </span>
         </span>

+ 12 - 2
public/app/routes/dashboard_loaders.ts

@@ -5,7 +5,7 @@ export class LoadDashboardCtrl {
   constructor($scope, $routeParams, dashboardLoaderSrv, backendSrv, $location) {
   constructor($scope, $routeParams, dashboardLoaderSrv, backendSrv, $location) {
     $scope.appEvent('dashboard-fetch-start');
     $scope.appEvent('dashboard-fetch-start');
 
 
-    if (!$routeParams.slug) {
+    if (!$routeParams.uid && !$routeParams.slug) {
       backendSrv.get('/api/dashboards/home').then(function(homeDash) {
       backendSrv.get('/api/dashboards/home').then(function(homeDash) {
         if (homeDash.redirectUri) {
         if (homeDash.redirectUri) {
           $location.path('dashboard/' + homeDash.redirectUri);
           $location.path('dashboard/' + homeDash.redirectUri);
@@ -18,7 +18,17 @@ export class LoadDashboardCtrl {
       return;
       return;
     }
     }
 
 
-    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);
+        }
+      });
+      return;
+    }
+
+    dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(function(result) {
       if ($routeParams.keepRows) {
       if ($routeParams.keepRows) {
         result.meta.keepRows = true;
         result.meta.keepRows = true;
       }
       }

+ 12 - 0
public/app/routes/routes.ts

@@ -14,12 +14,24 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
       reloadOnSearch: false,
       reloadOnSearch: false,
       pageClass: 'page-dashboard',
       pageClass: 'page-dashboard',
     })
     })
+    .when('/d/:uid/:slug', {
+      templateUrl: 'public/app/partials/dashboard.html',
+      controller: 'LoadDashboardCtrl',
+      reloadOnSearch: false,
+      pageClass: 'page-dashboard',
+    })
     .when('/dashboard/:type/:slug', {
     .when('/dashboard/:type/:slug', {
       templateUrl: 'public/app/partials/dashboard.html',
       templateUrl: 'public/app/partials/dashboard.html',
       controller: 'LoadDashboardCtrl',
       controller: 'LoadDashboardCtrl',
       reloadOnSearch: false,
       reloadOnSearch: false,
       pageClass: 'page-dashboard',
       pageClass: 'page-dashboard',
     })
     })
+    .when('/d-solo/:uid/:slug', {
+      templateUrl: 'public/app/features/panel/partials/soloPanel.html',
+      controller: 'SoloPanelCtrl',
+      reloadOnSearch: false,
+      pageClass: 'page-dashboard',
+    })
     .when('/dashboard-solo/:type/:slug', {
     .when('/dashboard-solo/:type/:slug', {
       templateUrl: 'public/app/features/panel/partials/soloPanel.html',
       templateUrl: 'public/app/features/panel/partials/soloPanel.html',
       controller: 'SoloPanelCtrl',
       controller: 'SoloPanelCtrl',