瀏覽代碼

Merge pull request #10706 from grafana/7883_frontend_step2

WIP: Dashboard & Persistent urls - Frontend Step 2
Marcus Efraimsson 8 年之前
父節點
當前提交
d8d82c1769
共有 28 個文件被更改,包括 306 次插入115 次删除
  1. 1 0
      pkg/api/api.go
  2. 31 0
      pkg/api/dashboard.go
  3. 124 0
      pkg/api/dashboard_test.go
  4. 10 2
      pkg/models/dashboards.go
  5. 1 1
      pkg/services/search/models.go
  6. 12 1
      pkg/services/sqlstore/dashboard.go
  7. 1 1
      pkg/services/sqlstore/dashboard_test.go
  8. 1 1
      public/app/containers/ManageDashboards/FolderPermissions.tsx
  9. 2 2
      public/app/containers/ManageDashboards/FolderSettings.jest.tsx
  10. 4 2
      public/app/containers/ManageDashboards/FolderSettings.tsx
  11. 9 9
      public/app/core/components/manage_dashboards/manage_dashboards.ts
  12. 22 11
      public/app/core/services/backend_srv.ts
  13. 4 5
      public/app/core/services/bridge_srv.ts
  14. 4 4
      public/app/core/services/search_srv.ts
  15. 8 8
      public/app/core/specs/manage_dashboards.jest.ts
  16. 1 3
      public/app/features/dashboard/create_folder_ctrl.ts
  17. 9 7
      public/app/features/dashboard/folder_dashboards_ctrl.ts
  18. 5 8
      public/app/features/dashboard/folder_page_loader.ts
  19. 12 9
      public/app/features/dashboard/folder_permissions_ctrl.ts
  20. 18 14
      public/app/features/dashboard/folder_settings_ctrl.ts
  21. 1 1
      public/app/features/dashboard/partials/folder_dashboards.html
  22. 1 1
      public/app/features/dashboard/settings/settings.ts
  23. 4 0
      public/app/routes/dashboard_loaders.ts
  24. 3 3
      public/app/routes/routes.ts
  25. 9 8
      public/app/stores/FolderStore/FolderStore.ts
  26. 5 5
      public/app/stores/NavStore/NavStore.jest.ts
  27. 3 9
      public/app/stores/NavStore/NavStore.ts
  28. 1 0
      public/test/mocks/common.ts

+ 1 - 0
pkg/api/api.go

@@ -249,6 +249,7 @@ func (hs *HttpServer) registerRoutes() {
 		// Dashboard
 		// Dashboard
 		apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) {
 		apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) {
 			dashboardRoute.Get("/uid/:uid", wrap(GetDashboard))
 			dashboardRoute.Get("/uid/:uid", wrap(GetDashboard))
+			dashboardRoute.Delete("/uid/:uid", wrap(DeleteDashboardByUid))
 
 
 			dashboardRoute.Get("/db/:slug", wrap(GetDashboard))
 			dashboardRoute.Get("/db/:slug", wrap(GetDashboard))
 			dashboardRoute.Delete("/db/:slug", wrap(DeleteDashboard))
 			dashboardRoute.Delete("/db/:slug", wrap(DeleteDashboard))

+ 31 - 0
pkg/api/dashboard.go

@@ -141,6 +141,16 @@ func getDashboardHelper(orgId int64, slug string, id int64, uid string) (*m.Dash
 }
 }
 
 
 func DeleteDashboard(c *middleware.Context) Response {
 func DeleteDashboard(c *middleware.Context) Response {
+	query := m.GetDashboardsBySlugQuery{OrgId: c.OrgId, Slug: c.Params(":slug")}
+
+	if err := bus.Dispatch(&query); err != nil {
+		return ApiError(500, "Failed to retrieve dashboards by slug", err)
+	}
+
+	if len(query.Result) > 1 {
+		return Json(412, util.DynMap{"status": "multiple-slugs-exists", "message": m.ErrDashboardsWithSameSlugExists.Error()})
+	}
+
 	dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, "")
 	dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, "")
 	if rsp != nil {
 	if rsp != nil {
 		return rsp
 		return rsp
@@ -160,6 +170,26 @@ func DeleteDashboard(c *middleware.Context) Response {
 	return Json(200, resp)
 	return Json(200, resp)
 }
 }
 
 
+func DeleteDashboardByUid(c *middleware.Context) Response {
+	dash, rsp := getDashboardHelper(c.OrgId, "", 0, c.Params(":uid"))
+	if rsp != nil {
+		return rsp
+	}
+
+	guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
+	if canSave, err := guardian.CanSave(); err != nil || !canSave {
+		return dashboardGuardianResponse(err)
+	}
+
+	cmd := m.DeleteDashboardCommand{OrgId: c.OrgId, Id: dash.Id}
+	if err := bus.Dispatch(&cmd); err != nil {
+		return ApiError(500, "Failed to delete dashboard", err)
+	}
+
+	var resp = map[string]interface{}{"title": dash.Title}
+	return Json(200, resp)
+}
+
 func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
 func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
 	cmd.OrgId = c.OrgId
 	cmd.OrgId = c.OrgId
 	cmd.UserId = c.UserId
 	cmd.UserId = c.UserId
@@ -440,6 +470,7 @@ func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboard
 	saveCmd.UserId = c.UserId
 	saveCmd.UserId = c.UserId
 	saveCmd.Dashboard = version.Data
 	saveCmd.Dashboard = version.Data
 	saveCmd.Dashboard.Set("version", dash.Version)
 	saveCmd.Dashboard.Set("version", dash.Version)
+	saveCmd.Dashboard.Set("uid", dash.Uid)
 	saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version)
 	saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version)
 
 
 	return PostDashboard(c, saveCmd)
 	return PostDashboard(c, saveCmd)

+ 124 - 0
pkg/api/dashboard_test.go

@@ -39,6 +39,12 @@ func TestDashboardApiEndpoint(t *testing.T) {
 		fakeDash.FolderId = 1
 		fakeDash.FolderId = 1
 		fakeDash.HasAcl = false
 		fakeDash.HasAcl = false
 
 
+		bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error {
+			dashboards := []*m.Dashboard{fakeDash}
+			query.Result = dashboards
+			return nil
+		})
+
 		var getDashboardQueries []*m.GetDashboardQuery
 		var getDashboardQueries []*m.GetDashboardQuery
 
 
 		bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
 		bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
@@ -117,6 +123,15 @@ func TestDashboardApiEndpoint(t *testing.T) {
 				})
 				})
 			})
 			})
 
 
+			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
+				CallDeleteDashboardByUid(sc)
+				So(sc.resp.Code, ShouldEqual, 403)
+
+				Convey("Should lookup dashboard by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
+				})
+			})
+
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
 				CallGetDashboardVersion(sc)
 				CallGetDashboardVersion(sc)
 				So(sc.resp.Code, ShouldEqual, 403)
 				So(sc.resp.Code, ShouldEqual, 403)
@@ -173,6 +188,15 @@ func TestDashboardApiEndpoint(t *testing.T) {
 				})
 				})
 			})
 			})
 
 
+			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
+				CallDeleteDashboardByUid(sc)
+				So(sc.resp.Code, ShouldEqual, 200)
+
+				Convey("Should lookup dashboard by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
+				})
+			})
+
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
 				CallGetDashboardVersion(sc)
 				CallGetDashboardVersion(sc)
 				So(sc.resp.Code, ShouldEqual, 200)
 				So(sc.resp.Code, ShouldEqual, 200)
@@ -218,6 +242,12 @@ func TestDashboardApiEndpoint(t *testing.T) {
 		fakeDash.HasAcl = true
 		fakeDash.HasAcl = true
 		setting.ViewersCanEdit = false
 		setting.ViewersCanEdit = false
 
 
+		bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error {
+			dashboards := []*m.Dashboard{fakeDash}
+			query.Result = dashboards
+			return nil
+		})
+
 		aclMockResp := []*m.DashboardAclInfoDTO{
 		aclMockResp := []*m.DashboardAclInfoDTO{
 			{
 			{
 				DashboardId: 1,
 				DashboardId: 1,
@@ -299,6 +329,15 @@ func TestDashboardApiEndpoint(t *testing.T) {
 				})
 				})
 			})
 			})
 
 
+			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
+				CallDeleteDashboardByUid(sc)
+				So(sc.resp.Code, ShouldEqual, 403)
+
+				Convey("Should lookup dashboard by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
+				})
+			})
+
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
 				CallGetDashboardVersion(sc)
 				CallGetDashboardVersion(sc)
 				So(sc.resp.Code, ShouldEqual, 403)
 				So(sc.resp.Code, ShouldEqual, 403)
@@ -353,6 +392,15 @@ func TestDashboardApiEndpoint(t *testing.T) {
 				})
 				})
 			})
 			})
 
 
+			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
+				CallDeleteDashboardByUid(sc)
+				So(sc.resp.Code, ShouldEqual, 403)
+
+				Convey("Should lookup dashboard by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
+				})
+			})
+
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
 				CallGetDashboardVersion(sc)
 				CallGetDashboardVersion(sc)
 				So(sc.resp.Code, ShouldEqual, 403)
 				So(sc.resp.Code, ShouldEqual, 403)
@@ -418,6 +466,15 @@ func TestDashboardApiEndpoint(t *testing.T) {
 				})
 				})
 			})
 			})
 
 
+			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
+				CallDeleteDashboardByUid(sc)
+				So(sc.resp.Code, ShouldEqual, 200)
+
+				Convey("Should lookup dashboard by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
+				})
+			})
+
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
 				CallGetDashboardVersion(sc)
 				CallGetDashboardVersion(sc)
 				So(sc.resp.Code, ShouldEqual, 200)
 				So(sc.resp.Code, ShouldEqual, 200)
@@ -482,6 +539,15 @@ func TestDashboardApiEndpoint(t *testing.T) {
 					So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
 					So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
 				})
 				})
 			})
 			})
+
+			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
+				CallDeleteDashboardByUid(sc)
+				So(sc.resp.Code, ShouldEqual, 403)
+
+				Convey("Should lookup dashboard by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
+				})
+			})
 		})
 		})
 
 
 		Convey("When user is an Org Viewer but has an admin permission", func() {
 		Convey("When user is an Org Viewer but has an admin permission", func() {
@@ -533,6 +599,15 @@ func TestDashboardApiEndpoint(t *testing.T) {
 				})
 				})
 			})
 			})
 
 
+			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
+				CallDeleteDashboardByUid(sc)
+				So(sc.resp.Code, ShouldEqual, 200)
+
+				Convey("Should lookup dashboard by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
+				})
+			})
+
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
 				CallGetDashboardVersion(sc)
 				CallGetDashboardVersion(sc)
 				So(sc.resp.Code, ShouldEqual, 200)
 				So(sc.resp.Code, ShouldEqual, 200)
@@ -595,6 +670,15 @@ func TestDashboardApiEndpoint(t *testing.T) {
 				})
 				})
 			})
 			})
 
 
+			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
+				CallDeleteDashboardByUid(sc)
+				So(sc.resp.Code, ShouldEqual, 403)
+
+				Convey("Should lookup dashboard by uid", func() {
+					So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
+				})
+			})
+
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
 			loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
 				CallGetDashboardVersion(sc)
 				CallGetDashboardVersion(sc)
 				So(sc.resp.Code, ShouldEqual, 403)
 				So(sc.resp.Code, ShouldEqual, 403)
@@ -611,6 +695,37 @@ func TestDashboardApiEndpoint(t *testing.T) {
 			})
 			})
 		})
 		})
 	})
 	})
+
+	Convey("Given two dashboards with the same title in different folders", t, func() {
+		dashOne := m.NewDashboard("dash")
+		dashOne.Id = 2
+		dashOne.FolderId = 1
+		dashOne.HasAcl = false
+
+		dashTwo := m.NewDashboard("dash")
+		dashTwo.Id = 4
+		dashTwo.FolderId = 3
+		dashTwo.HasAcl = false
+
+		bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error {
+			dashboards := []*m.Dashboard{dashOne, dashTwo}
+			query.Result = dashboards
+			return nil
+		})
+
+		role := m.ROLE_EDITOR
+
+		loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
+			CallDeleteDashboard(sc)
+
+			Convey("Should result in 412 Precondition failed", func() {
+				So(sc.resp.Code, ShouldEqual, 412)
+				result := sc.ToJson()
+				So(result.Get("status").MustString(), ShouldEqual, "multiple-slugs-exists")
+				So(result.Get("message").MustString(), ShouldEqual, m.ErrDashboardsWithSameSlugExists.Error())
+			})
+		})
+	})
 }
 }
 
 
 func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
 func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
@@ -655,6 +770,15 @@ func CallDeleteDashboard(sc *scenarioContext) {
 	sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
 	sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
 }
 }
 
 
+func CallDeleteDashboardByUid(sc *scenarioContext) {
+	bus.AddHandler("test", func(cmd *m.DeleteDashboardCommand) error {
+		return nil
+	})
+
+	sc.handlerFunc = DeleteDashboardByUid
+	sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
+}
+
 func CallPostDashboard(sc *scenarioContext) {
 func CallPostDashboard(sc *scenarioContext) {
 	bus.AddHandler("test", func(cmd *alerting.ValidateDashboardAlertsCommand) error {
 	bus.AddHandler("test", func(cmd *alerting.ValidateDashboardAlertsCommand) error {
 		return nil
 		return nil

+ 10 - 2
pkg/models/dashboards.go

@@ -22,7 +22,8 @@ var (
 	ErrDashboardFolderCannotHaveParent     = errors.New("A Dashboard Folder cannot be added to another folder")
 	ErrDashboardFolderCannotHaveParent     = errors.New("A Dashboard Folder cannot be added to another folder")
 	ErrDashboardContainsInvalidAlertData   = errors.New("Invalid alert data. Cannot save dashboard")
 	ErrDashboardContainsInvalidAlertData   = errors.New("Invalid alert data. Cannot save dashboard")
 	ErrDashboardFailedToUpdateAlertData    = errors.New("Failed to save alert data")
 	ErrDashboardFailedToUpdateAlertData    = errors.New("Failed to save alert data")
-	ErrDashboardFailedGenerateUniqueUid    = errors.New("Failed to generate unique dashboard uid.")
+	ErrDashboardsWithSameSlugExists        = errors.New("Multiple dashboards with the same slug exists")
+	ErrDashboardFailedGenerateUniqueUid    = errors.New("Failed to generate unique dashboard id")
 )
 )
 
 
 type UpdatePluginDashboardError struct {
 type UpdatePluginDashboardError struct {
@@ -177,7 +178,7 @@ func GetDashboardUrl(uid string, slug string) string {
 
 
 // GetFolderUrl return the html url for a folder
 // GetFolderUrl return the html url for a folder
 func GetFolderUrl(folderUid string, slug string) string {
 func GetFolderUrl(folderUid string, slug string) string {
-	return fmt.Sprintf("%s/f/%v/%s", setting.AppSubUrl, folderUid, slug)
+	return fmt.Sprintf("%s/dashboards/f/%s/%s", setting.AppSubUrl, folderUid, slug)
 }
 }
 
 
 //
 //
@@ -252,6 +253,13 @@ type GetDashboardSlugByIdQuery struct {
 	Result string
 	Result string
 }
 }
 
 
+type GetDashboardsBySlugQuery struct {
+	OrgId int64
+	Slug  string
+
+	Result []*Dashboard
+}
+
 type GetFoldersForSignedInUserQuery struct {
 type GetFoldersForSignedInUserQuery struct {
 	OrgId        int64
 	OrgId        int64
 	SignedInUser *SignedInUser
 	SignedInUser *SignedInUser

+ 1 - 1
pkg/services/search/models.go

@@ -13,10 +13,10 @@ const (
 
 
 type Hit struct {
 type Hit struct {
 	Id          int64    `json:"id"`
 	Id          int64    `json:"id"`
+	Uid         string   `json:"uid"`
 	Title       string   `json:"title"`
 	Title       string   `json:"title"`
 	Uri         string   `json:"uri"`
 	Uri         string   `json:"uri"`
 	Url         string   `json:"url"`
 	Url         string   `json:"url"`
-	Slug        string   `json:"slug"`
 	Type        HitType  `json:"type"`
 	Type        HitType  `json:"type"`
 	Tags        []string `json:"tags"`
 	Tags        []string `json:"tags"`
 	IsStarred   bool     `json:"isStarred"`
 	IsStarred   bool     `json:"isStarred"`

+ 12 - 1
pkg/services/sqlstore/dashboard.go

@@ -314,10 +314,10 @@ func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []Dashboard
 		if !exists {
 		if !exists {
 			hit = &search.Hit{
 			hit = &search.Hit{
 				Id:          item.Id,
 				Id:          item.Id,
+				Uid:         item.Uid,
 				Title:       item.Title,
 				Title:       item.Title,
 				Uri:         "db/" + item.Slug,
 				Uri:         "db/" + item.Slug,
 				Url:         m.GetDashboardFolderUrl(item.IsFolder, item.Uid, item.Slug),
 				Url:         m.GetDashboardFolderUrl(item.IsFolder, item.Uid, item.Slug),
-				Slug:        item.Slug,
 				Type:        getHitType(item),
 				Type:        getHitType(item),
 				FolderId:    item.FolderId,
 				FolderId:    item.FolderId,
 				FolderTitle: item.FolderTitle,
 				FolderTitle: item.FolderTitle,
@@ -550,3 +550,14 @@ func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error {
 	query.Result = slug.Slug
 	query.Result = slug.Slug
 	return nil
 	return nil
 }
 }
+
+func GetDashboardsBySlug(query *m.GetDashboardsBySlugQuery) error {
+	var dashboards = make([]*m.Dashboard, 0)
+
+	if err := x.Where("org_id=? AND slug=?", query.OrgId, query.Slug).Find(&dashboards); err != nil {
+		return err
+	}
+
+	query.Result = dashboards
+	return nil
+}

+ 1 - 1
pkg/services/sqlstore/dashboard_test.go

@@ -146,7 +146,7 @@ func TestDashboardDataAccess(t *testing.T) {
 				So(len(query.Result), ShouldEqual, 1)
 				So(len(query.Result), ShouldEqual, 1)
 				hit := query.Result[0]
 				hit := query.Result[0]
 				So(hit.Type, ShouldEqual, search.DashHitFolder)
 				So(hit.Type, ShouldEqual, search.DashHitFolder)
-				So(hit.Url, ShouldEqual, fmt.Sprintf("/f/%s/%s", savedFolder.Uid, savedFolder.Slug))
+				So(hit.Url, ShouldEqual, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.Uid, savedFolder.Slug))
 			})
 			})
 
 
 			Convey("Should be able to search for a dashboard folder's children", func() {
 			Convey("Should be able to search for a dashboard folder's children", func() {

+ 1 - 1
public/app/containers/ManageDashboards/FolderPermissions.tsx

@@ -16,7 +16,7 @@ export class FolderPermissions extends Component<IContainerProps, any> {
 
 
   loadStore() {
   loadStore() {
     const { nav, folder, view } = this.props;
     const { nav, folder, view } = this.props;
-    return folder.load(view.routeParams.get('slug') as string).then(res => {
+    return folder.load(view.routeParams.get('uid') as string).then(res => {
       return nav.initFolderNav(toJS(folder.folder), 'manage-folder-permissions');
       return nav.initFolderNav(toJS(folder.folder), 'manage-folder-permissions');
     });
     });
   }
   }

+ 2 - 2
public/app/containers/ManageDashboards/FolderSettings.jest.tsx

@@ -9,14 +9,14 @@ describe('FolderSettings', () => {
   let page;
   let page;
 
 
   beforeAll(() => {
   beforeAll(() => {
-    backendSrv.getDashboard.mockReturnValue(
+    backendSrv.getDashboardByUid.mockReturnValue(
       Promise.resolve({
       Promise.resolve({
         dashboard: {
         dashboard: {
           id: 1,
           id: 1,
           title: 'Folder Name',
           title: 'Folder Name',
         },
         },
         meta: {
         meta: {
-          slug: 'folder-name',
+          url: '/dashboards/f/uid/folder-name',
           canSave: true,
           canSave: true,
         },
         },
       })
       })

+ 4 - 2
public/app/containers/ManageDashboards/FolderSettings.tsx

@@ -20,10 +20,12 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
   loadStore() {
   loadStore() {
     const { nav, folder, view } = this.props;
     const { nav, folder, view } = this.props;
 
 
-    return folder.load(view.routeParams.get('slug') as string).then(res => {
+    return folder.load(view.routeParams.get('uid') as string).then(res => {
       this.formSnapshot = getSnapshot(folder);
       this.formSnapshot = getSnapshot(folder);
       this.dashboard = res.dashboard;
       this.dashboard = res.dashboard;
 
 
+      view.updatePathAndQuery(`${res.meta.url}/settings`, {}, {});
+
       return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings');
       return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings');
     });
     });
   }
   }
@@ -51,7 +53,7 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
     folder
     folder
       .saveFolder(this.dashboard, { overwrite: false })
       .saveFolder(this.dashboard, { overwrite: false })
       .then(newUrl => {
       .then(newUrl => {
-        view.updatePathAndQuery(newUrl, '', '');
+        view.updatePathAndQuery(newUrl, {}, {});
 
 
         appEvents.emit('dashboard-saved');
         appEvents.emit('dashboard-saved');
         appEvents.emit('alert-success', ['Folder saved']);
         appEvents.emit('alert-success', ['Folder saved']);

+ 9 - 9
public/app/core/components/manage_dashboards/manage_dashboards.ts

@@ -34,7 +34,7 @@ export class ManageDashboardsCtrl {
 
 
   // used when managing dashboards for a specific folder
   // used when managing dashboards for a specific folder
   folderId?: number;
   folderId?: number;
-  folderSlug?: string;
+  folderUid?: string;
 
 
   // if user can add new folders and/or add new dashboards
   // if user can add new folders and/or add new dashboards
   canSave: boolean;
   canSave: boolean;
@@ -74,11 +74,11 @@ export class ManageDashboardsCtrl {
         return this.initDashboardList(result);
         return this.initDashboardList(result);
       })
       })
       .then(() => {
       .then(() => {
-        if (!this.folderSlug) {
+        if (!this.folderUid) {
           return;
           return;
         }
         }
 
 
-        return this.backendSrv.getDashboard('db', this.folderSlug).then(dash => {
+        return this.backendSrv.getDashboardByUid(this.folderUid).then(dash => {
           this.canSave = dash.meta.canSave;
           this.canSave = dash.meta.canSave;
         });
         });
       });
       });
@@ -130,10 +130,10 @@ export class ManageDashboardsCtrl {
 
 
     for (const section of this.sections) {
     for (const section of this.sections) {
       if (section.checked && section.id !== 0) {
       if (section.checked && section.id !== 0) {
-        selectedDashboards.folders.push(section.slug);
+        selectedDashboards.folders.push(section.uid);
       } else {
       } else {
         const selected = _.filter(section.items, { checked: true });
         const selected = _.filter(section.items, { checked: true });
-        selectedDashboards.dashboards.push(..._.map(selected, 'slug'));
+        selectedDashboards.dashboards.push(..._.map(selected, 'uid'));
       }
       }
     }
     }
 
 
@@ -179,8 +179,8 @@ export class ManageDashboardsCtrl {
     });
     });
   }
   }
 
 
-  private deleteFoldersAndDashboards(slugs) {
-    this.backendSrv.deleteDashboards(slugs).then(result => {
+  private deleteFoldersAndDashboards(uids) {
+    this.backendSrv.deleteDashboards(uids).then(result => {
       const folders = _.filter(result, dash => dash.meta.isFolder);
       const folders = _.filter(result, dash => dash.meta.isFolder);
       const folderCount = folders.length;
       const folderCount = folders.length;
       const dashboards = _.filter(result, dash => !dash.meta.isFolder);
       const dashboards = _.filter(result, dash => !dash.meta.isFolder);
@@ -224,7 +224,7 @@ export class ManageDashboardsCtrl {
 
 
     for (const section of this.sections) {
     for (const section of this.sections) {
       const selected = _.filter(section.items, { checked: true });
       const selected = _.filter(section.items, { checked: true });
-      selectedDashboards.push(..._.map(selected, 'slug'));
+      selectedDashboards.push(..._.map(selected, 'uid'));
     }
     }
 
 
     return selectedDashboards;
     return selectedDashboards;
@@ -334,7 +334,7 @@ export function manageDashboardsDirective() {
     controllerAs: 'ctrl',
     controllerAs: 'ctrl',
     scope: {
     scope: {
       folderId: '=',
       folderId: '=',
-      folderSlug: '=',
+      folderUid: '=',
     },
     },
   };
   };
 }
 }

+ 22 - 11
public/app/core/services/backend_srv.ts

@@ -257,11 +257,22 @@ export class BackendSrv {
     });
     });
   }
   }
 
 
-  deleteDashboard(slug) {
+  saveFolder(dash, options) {
+    options = options || {};
+
+    return this.post('/api/dashboards/db/', {
+      dashboard: dash,
+      isFolder: true,
+      overwrite: options.overwrite === true,
+      message: options.message || '',
+    });
+  }
+
+  deleteDashboard(uid) {
     let deferred = this.$q.defer();
     let deferred = this.$q.defer();
 
 
-    this.getDashboard('db', slug).then(fullDash => {
-      this.delete(`/api/dashboards/db/${slug}`)
+    this.getDashboardByUid(uid).then(fullDash => {
+      this.delete(`/api/dashboards/uid/${uid}`)
         .then(() => {
         .then(() => {
           deferred.resolve(fullDash);
           deferred.resolve(fullDash);
         })
         })
@@ -273,21 +284,21 @@ export class BackendSrv {
     return deferred.promise;
     return deferred.promise;
   }
   }
 
 
-  deleteDashboards(dashboardSlugs) {
+  deleteDashboards(dashboardUids) {
     const tasks = [];
     const tasks = [];
 
 
-    for (let slug of dashboardSlugs) {
-      tasks.push(this.createTask(this.deleteDashboard.bind(this), true, slug));
+    for (let uid of dashboardUids) {
+      tasks.push(this.createTask(this.deleteDashboard.bind(this), true, uid));
     }
     }
 
 
     return this.executeInOrder(tasks, []);
     return this.executeInOrder(tasks, []);
   }
   }
 
 
-  moveDashboards(dashboardSlugs, toFolder) {
+  moveDashboards(dashboardUids, toFolder) {
     const tasks = [];
     const tasks = [];
 
 
-    for (let slug of dashboardSlugs) {
-      tasks.push(this.createTask(this.moveDashboard.bind(this), true, slug, toFolder));
+    for (let uid of dashboardUids) {
+      tasks.push(this.createTask(this.moveDashboard.bind(this), true, uid, toFolder));
     }
     }
 
 
     return this.executeInOrder(tasks, []).then(result => {
     return this.executeInOrder(tasks, []).then(result => {
@@ -299,10 +310,10 @@ export class BackendSrv {
     });
     });
   }
   }
 
 
-  private moveDashboard(slug, toFolder) {
+  private moveDashboard(uid, toFolder) {
     let deferred = this.$q.defer();
     let deferred = this.$q.defer();
 
 
-    this.getDashboard('db', slug).then(fullDash => {
+    this.getDashboardByUid(uid).then(fullDash => {
       const model = new DashboardModel(fullDash.dashboard, fullDash.meta);
       const model = new DashboardModel(fullDash.dashboard, fullDash.meta);
 
 
       if ((!fullDash.meta.folderId && toFolder.id === 0) || fullDash.meta.folderId === toFolder.id) {
       if ((!fullDash.meta.folderId && toFolder.id === 0) || fullDash.meta.folderId === toFolder.id) {

+ 4 - 5
public/app/core/services/bridge_srv.ts

@@ -34,10 +34,7 @@ export class BridgeSrv {
     });
     });
 
 
     this.$rootScope.$on('$routeChangeSuccess', (evt, data) => {
     this.$rootScope.$on('$routeChangeSuccess', (evt, data) => {
-      let angularUrl = this.$location.url();
-      if (store.view.currentUrl !== angularUrl) {
-        store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params);
-      }
+      store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params);
     });
     });
 
 
     reaction(
     reaction(
@@ -45,7 +42,9 @@ export class BridgeSrv {
       currentUrl => {
       currentUrl => {
         let angularUrl = this.$location.url();
         let angularUrl = this.$location.url();
         if (angularUrl !== currentUrl) {
         if (angularUrl !== currentUrl) {
-          this.$location.url(currentUrl);
+          this.$timeout(() => {
+            this.$location.url(currentUrl);
+          });
           console.log('store updating angular $location.url', currentUrl);
           console.log('store updating angular $location.url', currentUrl);
         }
         }
       }
       }

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

@@ -128,12 +128,12 @@ export class SearchSrv {
       if (hit.type === 'dash-folder') {
       if (hit.type === 'dash-folder') {
         sections[hit.id] = {
         sections[hit.id] = {
           id: hit.id,
           id: hit.id,
+          uid: hit.uid,
           title: hit.title,
           title: hit.title,
           expanded: false,
           expanded: false,
           items: [],
           items: [],
           toggle: this.toggleFolder.bind(this),
           toggle: this.toggleFolder.bind(this),
-          url: `dashboards/folder/${hit.id}/${hit.slug}`,
-          slug: hit.slug,
+          url: hit.url,
           icon: 'fa fa-folder',
           icon: 'fa fa-folder',
           score: _.keys(sections).length,
           score: _.keys(sections).length,
         };
         };
@@ -150,9 +150,9 @@ export class SearchSrv {
         if (hit.folderId) {
         if (hit.folderId) {
           section = {
           section = {
             id: hit.folderId,
             id: hit.folderId,
+            uid: hit.uid,
             title: hit.folderTitle,
             title: hit.folderTitle,
-            url: `dashboards/folder/${hit.folderId}/${hit.folderSlug}`,
-            slug: hit.slug,
+            url: hit.url,
             items: [],
             items: [],
             icon: 'fa fa-folder-open',
             icon: 'fa fa-folder-open',
             toggle: this.toggleFolder.bind(this),
             toggle: this.toggleFolder.bind(this),

+ 8 - 8
public/app/core/specs/manage_dashboards.jest.ts

@@ -483,22 +483,22 @@ describe('ManageDashboards', () => {
       ctrl.sections = [
       ctrl.sections = [
         {
         {
           id: 1,
           id: 1,
+          uid: 'folder',
           title: 'folder',
           title: 'folder',
-          items: [{ id: 2, checked: true, slug: 'folder-dash' }],
+          items: [{ id: 2, checked: true, uid: 'folder-dash' }],
           checked: true,
           checked: true,
-          slug: 'folder',
         },
         },
         {
         {
           id: 3,
           id: 3,
           title: 'folder-2',
           title: 'folder-2',
-          items: [{ id: 3, checked: true, slug: 'folder-2-dash' }],
+          items: [{ id: 3, checked: true, uid: 'folder-2-dash' }],
           checked: false,
           checked: false,
-          slug: 'folder-2',
+          uid: 'folder-2',
         },
         },
         {
         {
           id: 0,
           id: 0,
           title: 'Root',
           title: 'Root',
-          items: [{ id: 3, checked: true, slug: 'root-dash' }],
+          items: [{ id: 3, checked: true, uid: 'root-dash' }],
           checked: true,
           checked: true,
         },
         },
       ];
       ];
@@ -535,14 +535,14 @@ describe('ManageDashboards', () => {
         {
         {
           id: 1,
           id: 1,
           title: 'folder',
           title: 'folder',
-          items: [{ id: 2, checked: true, slug: 'dash' }],
+          items: [{ id: 2, checked: true, uid: 'dash' }],
           checked: false,
           checked: false,
-          slug: 'folder',
+          uid: 'folder',
         },
         },
         {
         {
           id: 0,
           id: 0,
           title: 'Root',
           title: 'Root',
-          items: [{ id: 3, checked: true, slug: 'dash-2' }],
+          items: [{ id: 3, checked: true, uid: 'dash-2' }],
           checked: false,
           checked: false,
         },
         },
       ];
       ];

+ 1 - 3
public/app/features/dashboard/create_folder_ctrl.ts

@@ -19,9 +19,7 @@ export class CreateFolderCtrl {
 
 
     return this.backendSrv.createDashboardFolder(this.title).then(result => {
     return this.backendSrv.createDashboardFolder(this.title).then(result => {
       appEvents.emit('alert-success', ['Folder Created', 'OK']);
       appEvents.emit('alert-success', ['Folder Created', 'OK']);
-
-      var folderUrl = `dashboards/folder/${result.dashboard.id}/${result.meta.slug}`;
-      this.$location.url(folderUrl);
+      this.$location.url(result.meta.url);
     });
     });
   }
   }
 
 

+ 9 - 7
public/app/features/dashboard/folder_dashboards_ctrl.ts

@@ -3,17 +3,19 @@ import { FolderPageLoader } from './folder_page_loader';
 export class FolderDashboardsCtrl {
 export class FolderDashboardsCtrl {
   navModel: any;
   navModel: any;
   folderId: number;
   folderId: number;
-  folderSlug: string;
+  uid: string;
 
 
   /** @ngInject */
   /** @ngInject */
-  constructor(private backendSrv, navModelSrv, private $routeParams) {
-    if (this.$routeParams.folderId && this.$routeParams.slug) {
-      this.folderId = $routeParams.folderId;
+  constructor(private backendSrv, navModelSrv, private $routeParams, $location) {
+    if (this.$routeParams.uid) {
+      this.uid = $routeParams.uid;
 
 
-      const loader = new FolderPageLoader(this.backendSrv, this.$routeParams);
+      const loader = new FolderPageLoader(this.backendSrv);
 
 
-      loader.load(this, this.folderId, 'manage-folder-dashboards').then(result => {
-        this.folderSlug = result.meta.slug;
+      loader.load(this, this.uid, 'manage-folder-dashboards').then(folder => {
+        if ($location.path() !== folder.meta.url) {
+          $location.path(folder.meta.url).replace();
+        }
       });
       });
     }
     }
   }
   }

+ 5 - 8
public/app/features/dashboard/folder_page_loader.ts

@@ -1,7 +1,7 @@
 export class FolderPageLoader {
 export class FolderPageLoader {
-  constructor(private backendSrv, private $routeParams) {}
+  constructor(private backendSrv) {}
 
 
-  load(ctrl, folderId, activeChildId) {
+  load(ctrl, uid, activeChildId) {
     ctrl.navModel = {
     ctrl.navModel = {
       main: {
       main: {
         icon: 'fa fa-folder-open',
         icon: 'fa fa-folder-open',
@@ -36,11 +36,12 @@ export class FolderPageLoader {
       },
       },
     };
     };
 
 
-    return this.backendSrv.getDashboard('db', this.$routeParams.slug).then(result => {
+    return this.backendSrv.getDashboardByUid(uid).then(result => {
+      ctrl.folderId = result.dashboard.id;
       const folderTitle = result.dashboard.title;
       const folderTitle = result.dashboard.title;
+      const folderUrl = result.meta.url;
       ctrl.navModel.main.text = folderTitle;
       ctrl.navModel.main.text = folderTitle;
 
 
-      const folderUrl = this.createFolderUrl(folderId, result.meta.slug);
       const dashTab = ctrl.navModel.main.children.find(child => child.id === 'manage-folder-dashboards');
       const dashTab = ctrl.navModel.main.children.find(child => child.id === 'manage-folder-dashboards');
       dashTab.url = folderUrl;
       dashTab.url = folderUrl;
 
 
@@ -57,8 +58,4 @@ export class FolderPageLoader {
       return result;
       return result;
     });
     });
   }
   }
-
-  createFolderUrl(folderId: number, slug: string) {
-    return `dashboards/folder/${folderId}/${slug}`;
-  }
 }
 }

+ 12 - 9
public/app/features/dashboard/folder_permissions_ctrl.ts

@@ -3,20 +3,23 @@ import { FolderPageLoader } from './folder_page_loader';
 export class FolderPermissionsCtrl {
 export class FolderPermissionsCtrl {
   navModel: any;
   navModel: any;
   folderId: number;
   folderId: number;
+  uid: string;
   dashboard: any;
   dashboard: any;
   meta: any;
   meta: any;
 
 
   /** @ngInject */
   /** @ngInject */
-  constructor(private backendSrv, navModelSrv, private $routeParams) {
-    if (this.$routeParams.folderId && this.$routeParams.slug) {
-      this.folderId = $routeParams.folderId;
+  constructor(private backendSrv, navModelSrv, private $routeParams, $location) {
+    if (this.$routeParams.uid) {
+      this.uid = $routeParams.uid;
 
 
-      new FolderPageLoader(this.backendSrv, this.$routeParams)
-        .load(this, this.folderId, 'manage-folder-permissions')
-        .then(result => {
-          this.dashboard = result.dashboard;
-          this.meta = result.meta;
-        });
+      new FolderPageLoader(this.backendSrv).load(this, this.uid, 'manage-folder-permissions').then(folder => {
+        if ($location.path() !== folder.meta.url) {
+          $location.path(`${folder.meta.url}/permissions`).replace();
+        }
+
+        this.dashboard = folder.dashboard;
+        this.meta = folder.meta;
+      });
     }
     }
   }
   }
 }
 }

+ 18 - 14
public/app/features/dashboard/folder_settings_ctrl.ts

@@ -5,6 +5,7 @@ export class FolderSettingsCtrl {
   folderPageLoader: FolderPageLoader;
   folderPageLoader: FolderPageLoader;
   navModel: any;
   navModel: any;
   folderId: number;
   folderId: number;
+  uid: string;
   canSave = false;
   canSave = false;
   dashboard: any;
   dashboard: any;
   meta: any;
   meta: any;
@@ -13,14 +14,18 @@ export class FolderSettingsCtrl {
 
 
   /** @ngInject */
   /** @ngInject */
   constructor(private backendSrv, navModelSrv, private $routeParams, private $location) {
   constructor(private backendSrv, navModelSrv, private $routeParams, private $location) {
-    if (this.$routeParams.folderId && this.$routeParams.slug) {
-      this.folderId = $routeParams.folderId;
-
-      this.folderPageLoader = new FolderPageLoader(this.backendSrv, this.$routeParams);
-      this.folderPageLoader.load(this, this.folderId, 'manage-folder-settings').then(result => {
-        this.dashboard = result.dashboard;
-        this.meta = result.meta;
-        this.canSave = result.meta.canSave;
+    if (this.$routeParams.uid) {
+      this.uid = $routeParams.uid;
+
+      this.folderPageLoader = new FolderPageLoader(this.backendSrv);
+      this.folderPageLoader.load(this, this.uid, 'manage-folder-settings').then(folder => {
+        if ($location.path() !== folder.meta.url) {
+          $location.path(`${folder.meta.url}/settings`).replace();
+        }
+
+        this.dashboard = folder.dashboard;
+        this.meta = folder.meta;
+        this.canSave = folder.meta.canSave;
         this.title = this.dashboard.title;
         this.title = this.dashboard.title;
       });
       });
     }
     }
@@ -36,11 +41,10 @@ export class FolderSettingsCtrl {
     this.dashboard.title = this.title.trim();
     this.dashboard.title = this.title.trim();
 
 
     return this.backendSrv
     return this.backendSrv
-      .saveDashboard(this.dashboard, { overwrite: false })
+      .updateDashboardFolder(this.dashboard, { overwrite: false })
       .then(result => {
       .then(result => {
-        var folderUrl = this.folderPageLoader.createFolderUrl(this.folderId, result.slug);
-        if (folderUrl !== this.$location.path()) {
-          this.$location.url(folderUrl + '/settings');
+        if (result.url !== this.$location.path()) {
+          this.$location.url(result.url + '/settings');
         }
         }
 
 
         appEvents.emit('dashboard-saved');
         appEvents.emit('dashboard-saved');
@@ -65,7 +69,7 @@ export class FolderSettingsCtrl {
       icon: 'fa-trash',
       icon: 'fa-trash',
       yesText: 'Delete',
       yesText: 'Delete',
       onConfirm: () => {
       onConfirm: () => {
-        return this.backendSrv.deleteDashboard(this.meta.slug).then(() => {
+        return this.backendSrv.deleteDashboard(this.dashboard.uid).then(() => {
           appEvents.emit('alert-success', ['Folder Deleted', `${this.dashboard.title} has been deleted`]);
           appEvents.emit('alert-success', ['Folder Deleted', `${this.dashboard.title} has been deleted`]);
           this.$location.url('dashboards');
           this.$location.url('dashboards');
         });
         });
@@ -84,7 +88,7 @@ export class FolderSettingsCtrl {
         yesText: 'Save & Overwrite',
         yesText: 'Save & Overwrite',
         icon: 'fa-warning',
         icon: 'fa-warning',
         onConfirm: () => {
         onConfirm: () => {
-          this.backendSrv.saveDashboard(this.dashboard, { overwrite: true });
+          this.backendSrv.updateDashboardFolder(this.dashboard, { overwrite: true });
         },
         },
       });
       });
     }
     }

+ 1 - 1
public/app/features/dashboard/partials/folder_dashboards.html

@@ -1,5 +1,5 @@
 <page-header ng-if="ctrl.navModel" model="ctrl.navModel"></page-header>
 <page-header ng-if="ctrl.navModel" model="ctrl.navModel"></page-header>
 
 
 <div class="page-container page-body">
 <div class="page-container page-body">
-    <manage-dashboards ng-if="ctrl.folderId && ctrl.folderSlug" folder-id="ctrl.folderId" folder-slug="ctrl.folderSlug" />
+    <manage-dashboards ng-if="ctrl.folderId && ctrl.uid" folder-id="ctrl.folderId" folder-uid="ctrl.uid" />
 </div>
 </div>

+ 1 - 1
public/app/features/dashboard/settings/settings.ts

@@ -182,7 +182,7 @@ export class SettingsCtrl {
   }
   }
 
 
   deleteDashboardConfirmed() {
   deleteDashboardConfirmed() {
-    this.backendSrv.deleteDashboard(this.dashboard.meta.slug).then(() => {
+    this.backendSrv.deleteDashboard(this.dashboard.uid).then(() => {
       appEvents.emit('alert-success', ['Dashboard Deleted', this.dashboard.title + ' has been deleted']);
       appEvents.emit('alert-success', ['Dashboard Deleted', this.dashboard.title + ' has been deleted']);
       this.$location.url('/');
       this.$location.url('/');
     });
     });

+ 4 - 0
public/app/routes/dashboard_loaders.ts

@@ -29,6 +29,10 @@ export class LoadDashboardCtrl {
     }
     }
 
 
     dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(function(result) {
     dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(function(result) {
+      if ($location.path() !== result.meta.url) {
+        $location.path(result.meta.url).replace();
+      }
+
       if ($routeParams.keepRows) {
       if ($routeParams.keepRows) {
         result.meta.keepRows = true;
         result.meta.keepRows = true;
       }
       }

+ 3 - 3
public/app/routes/routes.ts

@@ -81,19 +81,19 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
       controller: 'CreateFolderCtrl',
       controller: 'CreateFolderCtrl',
       controllerAs: 'ctrl',
       controllerAs: 'ctrl',
     })
     })
-    .when('/dashboards/folder/:folderId/:slug/permissions', {
+    .when('/dashboards/f/:uid/:slug/permissions', {
       template: '<react-container />',
       template: '<react-container />',
       resolve: {
       resolve: {
         component: () => FolderPermissions,
         component: () => FolderPermissions,
       },
       },
     })
     })
-    .when('/dashboards/folder/:folderId/:slug/settings', {
+    .when('/dashboards/f/:uid/:slug/settings', {
       template: '<react-container />',
       template: '<react-container />',
       resolve: {
       resolve: {
         component: () => FolderSettings,
         component: () => FolderSettings,
       },
       },
     })
     })
-    .when('/dashboards/folder/:folderId/:slug', {
+    .when('/dashboards/f/:uid/:slug', {
       templateUrl: 'public/app/features/dashboard/partials/folder_dashboards.html',
       templateUrl: 'public/app/features/dashboard/partials/folder_dashboards.html',
       controller: 'FolderDashboardsCtrl',
       controller: 'FolderDashboardsCtrl',
       controllerAs: 'ctrl',
       controllerAs: 'ctrl',

+ 9 - 8
public/app/stores/FolderStore/FolderStore.ts

@@ -2,8 +2,8 @@ import { types, getEnv, flow } from 'mobx-state-tree';
 
 
 export const Folder = types.model('Folder', {
 export const Folder = types.model('Folder', {
   id: types.identifier(types.number),
   id: types.identifier(types.number),
-  slug: types.string,
   title: types.string,
   title: types.string,
+  url: types.string,
   canSave: types.boolean,
   canSave: types.boolean,
   hasChanged: types.boolean,
   hasChanged: types.boolean,
 });
 });
@@ -13,13 +13,13 @@ export const FolderStore = types
     folder: types.maybe(Folder),
     folder: types.maybe(Folder),
   })
   })
   .actions(self => ({
   .actions(self => ({
-    load: flow(function* load(slug: string) {
+    load: flow(function* load(uid: string) {
       const backendSrv = getEnv(self).backendSrv;
       const backendSrv = getEnv(self).backendSrv;
-      const res = yield backendSrv.getDashboard('db', slug);
+      const res = yield backendSrv.getDashboardByUid(uid);
       self.folder = Folder.create({
       self.folder = Folder.create({
         id: res.dashboard.id,
         id: res.dashboard.id,
         title: res.dashboard.title,
         title: res.dashboard.title,
-        slug: res.meta.slug,
+        url: res.meta.url,
         canSave: res.meta.canSave,
         canSave: res.meta.canSave,
         hasChanged: false,
         hasChanged: false,
       });
       });
@@ -35,14 +35,15 @@ export const FolderStore = types
       const backendSrv = getEnv(self).backendSrv;
       const backendSrv = getEnv(self).backendSrv;
       dashboard.title = self.folder.title.trim();
       dashboard.title = self.folder.title.trim();
 
 
-      const res = yield backendSrv.saveDashboard(dashboard, options);
-      self.folder.slug = res.slug;
-      return `dashboards/folder/${self.folder.id}/${res.slug}/settings`;
+      const res = yield backendSrv.saveFolder(dashboard, options);
+      self.folder.url = res.url;
+
+      return `${self.folder.url}/settings`;
     }),
     }),
 
 
     deleteFolder: flow(function* deleteFolder() {
     deleteFolder: flow(function* deleteFolder() {
       const backendSrv = getEnv(self).backendSrv;
       const backendSrv = getEnv(self).backendSrv;
 
 
-      return backendSrv.deleteDashboard(self.folder.slug);
+      return backendSrv.deleteDashboard(self.folder.url);
     }),
     }),
   }));
   }));

+ 5 - 5
public/app/stores/NavStore/NavStore.jest.ts

@@ -3,12 +3,12 @@ import { NavStore } from './NavStore';
 describe('NavStore', () => {
 describe('NavStore', () => {
   const folderId = 1;
   const folderId = 1;
   const folderTitle = 'Folder Name';
   const folderTitle = 'Folder Name';
-  const folderSlug = 'folder-name';
+  const folderUrl = '/dashboards/f/uid/folder-name';
   const canAdmin = true;
   const canAdmin = true;
 
 
   const folder = {
   const folder = {
     id: folderId,
     id: folderId,
-    slug: folderSlug,
+    url: folderUrl,
     title: folderTitle,
     title: folderTitle,
     canAdmin: canAdmin,
     canAdmin: canAdmin,
   };
   };
@@ -33,9 +33,9 @@ describe('NavStore', () => {
 
 
   it('Should set correct urls for each tab', () => {
   it('Should set correct urls for each tab', () => {
     expect(store.main.children.length).toBe(3);
     expect(store.main.children.length).toBe(3);
-    expect(store.main.children[0].url).toBe(`dashboards/folder/${folderId}/${folderSlug}`);
-    expect(store.main.children[1].url).toBe(`dashboards/folder/${folderId}/${folderSlug}/permissions`);
-    expect(store.main.children[2].url).toBe(`dashboards/folder/${folderId}/${folderSlug}/settings`);
+    expect(store.main.children[0].url).toBe(folderUrl);
+    expect(store.main.children[1].url).toBe(`${folderUrl}/permissions`);
+    expect(store.main.children[2].url).toBe(`${folderUrl}/settings`);
   });
   });
 
 
   it('Should set active tab', () => {
   it('Should set active tab', () => {

+ 3 - 9
public/app/stores/NavStore/NavStore.ts

@@ -41,8 +41,6 @@ export const NavStore = types
     },
     },
 
 
     initFolderNav(folder: any, activeChildId: string) {
     initFolderNav(folder: any, activeChildId: string) {
-      const folderUrl = createFolderUrl(folder.id, folder.slug);
-
       let main = {
       let main = {
         icon: 'fa fa-folder-open',
         icon: 'fa fa-folder-open',
         id: 'manage-folder',
         id: 'manage-folder',
@@ -56,21 +54,21 @@ export const NavStore = types
             icon: 'fa fa-fw fa-th-large',
             icon: 'fa fa-fw fa-th-large',
             id: 'manage-folder-dashboards',
             id: 'manage-folder-dashboards',
             text: 'Dashboards',
             text: 'Dashboards',
-            url: folderUrl,
+            url: folder.url,
           },
           },
           {
           {
             active: activeChildId === 'manage-folder-permissions',
             active: activeChildId === 'manage-folder-permissions',
             icon: 'fa fa-fw fa-lock',
             icon: 'fa fa-fw fa-lock',
             id: 'manage-folder-permissions',
             id: 'manage-folder-permissions',
             text: 'Permissions',
             text: 'Permissions',
-            url: folderUrl + '/permissions',
+            url: `${folder.url}/permissions`,
           },
           },
           {
           {
             active: activeChildId === 'manage-folder-settings',
             active: activeChildId === 'manage-folder-settings',
             icon: 'fa fa-fw fa-cog',
             icon: 'fa fa-fw fa-cog',
             id: 'manage-folder-settings',
             id: 'manage-folder-settings',
             text: 'Settings',
             text: 'Settings',
-            url: folderUrl + '/settings',
+            url: `${folder.url}/settings`,
           },
           },
         ],
         ],
       };
       };
@@ -118,7 +116,3 @@ export const NavStore = types
       self.main = NavItem.create(main);
       self.main = NavItem.create(main);
     },
     },
   }));
   }));
-
-function createFolderUrl(folderId: number, slug: string) {
-  return `dashboards/folder/${folderId}/${slug}`;
-}

+ 1 - 0
public/test/mocks/common.ts

@@ -1,6 +1,7 @@
 export const backendSrv = {
 export const backendSrv = {
   get: jest.fn(),
   get: jest.fn(),
   getDashboard: jest.fn(),
   getDashboard: jest.fn(),
+  getDashboardByUid: jest.fn(),
   post: jest.fn(),
   post: jest.fn(),
 };
 };