diff --git a/pkg/api/api.go b/pkg/api/api.go index be721d0bac3..752af7602f5 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -249,6 +249,7 @@ func (hs *HttpServer) registerRoutes() { // Dashboard apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) { dashboardRoute.Get("/uid/:uid", wrap(GetDashboard)) + dashboardRoute.Delete("/uid/:uid", wrap(DeleteDashboardByUid)) dashboardRoute.Get("/db/:slug", wrap(GetDashboard)) dashboardRoute.Delete("/db/:slug", wrap(DeleteDashboard)) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index cf5fae2fd9a..0e38a7587dc 100644 --- a/pkg/api/dashboard.go +++ b/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 { + 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, "") if rsp != nil { return rsp @@ -160,6 +170,26 @@ func DeleteDashboard(c *middleware.Context) Response { 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 { cmd.OrgId = c.OrgId cmd.UserId = c.UserId @@ -440,6 +470,7 @@ func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboard saveCmd.UserId = c.UserId saveCmd.Dashboard = version.Data saveCmd.Dashboard.Set("version", dash.Version) + saveCmd.Dashboard.Set("uid", dash.Uid) saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version) return PostDashboard(c, saveCmd) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 028dba11d35..be15fb41a66 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -39,6 +39,12 @@ func TestDashboardApiEndpoint(t *testing.T) { fakeDash.FolderId = 1 fakeDash.HasAcl = false + bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error { + dashboards := []*m.Dashboard{fakeDash} + query.Result = dashboards + return nil + }) + var getDashboardQueries []*m.GetDashboardQuery 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) { CallGetDashboardVersion(sc) 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) { CallGetDashboardVersion(sc) So(sc.resp.Code, ShouldEqual, 200) @@ -218,6 +242,12 @@ func TestDashboardApiEndpoint(t *testing.T) { fakeDash.HasAcl = true setting.ViewersCanEdit = false + bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error { + dashboards := []*m.Dashboard{fakeDash} + query.Result = dashboards + return nil + }) + aclMockResp := []*m.DashboardAclInfoDTO{ { 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) { CallGetDashboardVersion(sc) 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) { CallGetDashboardVersion(sc) 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) { CallGetDashboardVersion(sc) So(sc.resp.Code, ShouldEqual, 200) @@ -482,6 +539,15 @@ func TestDashboardApiEndpoint(t *testing.T) { 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() { @@ -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) { CallGetDashboardVersion(sc) 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) { CallGetDashboardVersion(sc) 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 { @@ -655,6 +770,15 @@ func CallDeleteDashboard(sc *scenarioContext) { 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) { bus.AddHandler("test", func(cmd *alerting.ValidateDashboardAlertsCommand) error { return nil diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 61f872542cf..51ba62419b2 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -22,7 +22,8 @@ var ( ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder") ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard") 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 { @@ -177,7 +178,7 @@ func GetDashboardUrl(uid string, slug string) string { // GetFolderUrl return the html url for a folder 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 } +type GetDashboardsBySlugQuery struct { + OrgId int64 + Slug string + + Result []*Dashboard +} + type GetFoldersForSignedInUserQuery struct { OrgId int64 SignedInUser *SignedInUser diff --git a/pkg/services/search/models.go b/pkg/services/search/models.go index 6214a854db7..f5d87b1f5c8 100644 --- a/pkg/services/search/models.go +++ b/pkg/services/search/models.go @@ -13,10 +13,10 @@ const ( type Hit struct { Id int64 `json:"id"` + Uid string `json:"uid"` Title string `json:"title"` Uri string `json:"uri"` Url string `json:"url"` - Slug string `json:"slug"` Type HitType `json:"type"` Tags []string `json:"tags"` IsStarred bool `json:"isStarred"` diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 5a3b95e2d05..a4e63a50633 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -314,10 +314,10 @@ func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []Dashboard if !exists { hit = &search.Hit{ Id: item.Id, + Uid: item.Uid, Title: item.Title, Uri: "db/" + item.Slug, Url: m.GetDashboardFolderUrl(item.IsFolder, item.Uid, item.Slug), - Slug: item.Slug, Type: getHitType(item), FolderId: item.FolderId, FolderTitle: item.FolderTitle, @@ -550,3 +550,14 @@ func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error { query.Result = slug.Slug 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 +} diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index 6e240b9585e..0a9a97dbe7a 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -146,7 +146,7 @@ func TestDashboardDataAccess(t *testing.T) { So(len(query.Result), ShouldEqual, 1) hit := query.Result[0] 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() { diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/containers/ManageDashboards/FolderPermissions.tsx index 3214382732a..e6e788b7585 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/containers/ManageDashboards/FolderPermissions.tsx @@ -16,7 +16,7 @@ export class FolderPermissions extends Component { loadStore() { 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'); }); } diff --git a/public/app/containers/ManageDashboards/FolderSettings.jest.tsx b/public/app/containers/ManageDashboards/FolderSettings.jest.tsx index 3355b657f03..bf7b35ed05d 100644 --- a/public/app/containers/ManageDashboards/FolderSettings.jest.tsx +++ b/public/app/containers/ManageDashboards/FolderSettings.jest.tsx @@ -9,14 +9,14 @@ describe('FolderSettings', () => { let page; beforeAll(() => { - backendSrv.getDashboard.mockReturnValue( + backendSrv.getDashboardByUid.mockReturnValue( Promise.resolve({ dashboard: { id: 1, title: 'Folder Name', }, meta: { - slug: 'folder-name', + url: '/dashboards/f/uid/folder-name', canSave: true, }, }) diff --git a/public/app/containers/ManageDashboards/FolderSettings.tsx b/public/app/containers/ManageDashboards/FolderSettings.tsx index ef3377622df..a6349764a14 100644 --- a/public/app/containers/ManageDashboards/FolderSettings.tsx +++ b/public/app/containers/ManageDashboards/FolderSettings.tsx @@ -20,10 +20,12 @@ export class FolderSettings extends React.Component { loadStore() { 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.dashboard = res.dashboard; + view.updatePathAndQuery(`${res.meta.url}/settings`, {}, {}); + return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); }); } @@ -51,7 +53,7 @@ export class FolderSettings extends React.Component { folder .saveFolder(this.dashboard, { overwrite: false }) .then(newUrl => { - view.updatePathAndQuery(newUrl, '', ''); + view.updatePathAndQuery(newUrl, {}, {}); appEvents.emit('dashboard-saved'); appEvents.emit('alert-success', ['Folder saved']); diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts index eba3099e348..d0f905f5f16 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.ts +++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts @@ -34,7 +34,7 @@ export class ManageDashboardsCtrl { // used when managing dashboards for a specific folder folderId?: number; - folderSlug?: string; + folderUid?: string; // if user can add new folders and/or add new dashboards canSave: boolean; @@ -74,11 +74,11 @@ export class ManageDashboardsCtrl { return this.initDashboardList(result); }) .then(() => { - if (!this.folderSlug) { + if (!this.folderUid) { return; } - return this.backendSrv.getDashboard('db', this.folderSlug).then(dash => { + return this.backendSrv.getDashboardByUid(this.folderUid).then(dash => { this.canSave = dash.meta.canSave; }); }); @@ -130,10 +130,10 @@ export class ManageDashboardsCtrl { for (const section of this.sections) { if (section.checked && section.id !== 0) { - selectedDashboards.folders.push(section.slug); + selectedDashboards.folders.push(section.uid); } else { 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 folderCount = folders.length; const dashboards = _.filter(result, dash => !dash.meta.isFolder); @@ -224,7 +224,7 @@ export class ManageDashboardsCtrl { for (const section of this.sections) { const selected = _.filter(section.items, { checked: true }); - selectedDashboards.push(..._.map(selected, 'slug')); + selectedDashboards.push(..._.map(selected, 'uid')); } return selectedDashboards; @@ -334,7 +334,7 @@ export function manageDashboardsDirective() { controllerAs: 'ctrl', scope: { folderId: '=', - folderSlug: '=', + folderUid: '=', }, }; } diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 9ade4264bb3..0a8b305ea53 100644 --- a/public/app/core/services/backend_srv.ts +++ b/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(); - this.getDashboard('db', slug).then(fullDash => { - this.delete(`/api/dashboards/db/${slug}`) + this.getDashboardByUid(uid).then(fullDash => { + this.delete(`/api/dashboards/uid/${uid}`) .then(() => { deferred.resolve(fullDash); }) @@ -273,21 +284,21 @@ export class BackendSrv { return deferred.promise; } - deleteDashboards(dashboardSlugs) { + deleteDashboards(dashboardUids) { 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, []); } - moveDashboards(dashboardSlugs, toFolder) { + moveDashboards(dashboardUids, toFolder) { 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 => { @@ -299,10 +310,10 @@ export class BackendSrv { }); } - private moveDashboard(slug, toFolder) { + private moveDashboard(uid, toFolder) { let deferred = this.$q.defer(); - this.getDashboard('db', slug).then(fullDash => { + this.getDashboardByUid(uid).then(fullDash => { const model = new DashboardModel(fullDash.dashboard, fullDash.meta); if ((!fullDash.meta.folderId && toFolder.id === 0) || fullDash.meta.folderId === toFolder.id) { diff --git a/public/app/core/services/bridge_srv.ts b/public/app/core/services/bridge_srv.ts index f0166d89e9d..ed4abb9b894 100644 --- a/public/app/core/services/bridge_srv.ts +++ b/public/app/core/services/bridge_srv.ts @@ -34,10 +34,7 @@ export class BridgeSrv { }); 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( @@ -45,7 +42,9 @@ export class BridgeSrv { currentUrl => { let angularUrl = this.$location.url(); if (angularUrl !== currentUrl) { - this.$location.url(currentUrl); + this.$timeout(() => { + this.$location.url(currentUrl); + }); console.log('store updating angular $location.url', currentUrl); } } diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts index a0989e74aed..cb9862f8cce 100644 --- a/public/app/core/services/search_srv.ts +++ b/public/app/core/services/search_srv.ts @@ -128,12 +128,12 @@ export class SearchSrv { if (hit.type === 'dash-folder') { sections[hit.id] = { id: hit.id, + uid: hit.uid, title: hit.title, expanded: false, items: [], toggle: this.toggleFolder.bind(this), - url: `dashboards/folder/${hit.id}/${hit.slug}`, - slug: hit.slug, + url: hit.url, icon: 'fa fa-folder', score: _.keys(sections).length, }; @@ -150,9 +150,9 @@ export class SearchSrv { if (hit.folderId) { section = { id: hit.folderId, + uid: hit.uid, title: hit.folderTitle, - url: `dashboards/folder/${hit.folderId}/${hit.folderSlug}`, - slug: hit.slug, + url: hit.url, items: [], icon: 'fa fa-folder-open', toggle: this.toggleFolder.bind(this), diff --git a/public/app/core/specs/manage_dashboards.jest.ts b/public/app/core/specs/manage_dashboards.jest.ts index 03aee78571f..b79c4fa06a3 100644 --- a/public/app/core/specs/manage_dashboards.jest.ts +++ b/public/app/core/specs/manage_dashboards.jest.ts @@ -483,22 +483,22 @@ describe('ManageDashboards', () => { ctrl.sections = [ { id: 1, + uid: 'folder', title: 'folder', - items: [{ id: 2, checked: true, slug: 'folder-dash' }], + items: [{ id: 2, checked: true, uid: 'folder-dash' }], checked: true, - slug: 'folder', }, { id: 3, title: 'folder-2', - items: [{ id: 3, checked: true, slug: 'folder-2-dash' }], + items: [{ id: 3, checked: true, uid: 'folder-2-dash' }], checked: false, - slug: 'folder-2', + uid: 'folder-2', }, { id: 0, title: 'Root', - items: [{ id: 3, checked: true, slug: 'root-dash' }], + items: [{ id: 3, checked: true, uid: 'root-dash' }], checked: true, }, ]; @@ -535,14 +535,14 @@ describe('ManageDashboards', () => { { id: 1, title: 'folder', - items: [{ id: 2, checked: true, slug: 'dash' }], + items: [{ id: 2, checked: true, uid: 'dash' }], checked: false, - slug: 'folder', + uid: 'folder', }, { id: 0, title: 'Root', - items: [{ id: 3, checked: true, slug: 'dash-2' }], + items: [{ id: 3, checked: true, uid: 'dash-2' }], checked: false, }, ]; diff --git a/public/app/features/dashboard/create_folder_ctrl.ts b/public/app/features/dashboard/create_folder_ctrl.ts index 414054232c2..f3d9167278b 100644 --- a/public/app/features/dashboard/create_folder_ctrl.ts +++ b/public/app/features/dashboard/create_folder_ctrl.ts @@ -19,9 +19,7 @@ export class CreateFolderCtrl { return this.backendSrv.createDashboardFolder(this.title).then(result => { 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); }); } diff --git a/public/app/features/dashboard/folder_dashboards_ctrl.ts b/public/app/features/dashboard/folder_dashboards_ctrl.ts index 943ddf8892f..dd0bf7772d8 100644 --- a/public/app/features/dashboard/folder_dashboards_ctrl.ts +++ b/public/app/features/dashboard/folder_dashboards_ctrl.ts @@ -3,17 +3,19 @@ import { FolderPageLoader } from './folder_page_loader'; export class FolderDashboardsCtrl { navModel: any; folderId: number; - folderSlug: string; + uid: string; /** @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(); + } }); } } diff --git a/public/app/features/dashboard/folder_page_loader.ts b/public/app/features/dashboard/folder_page_loader.ts index 9218f723559..81d10068361 100755 --- a/public/app/features/dashboard/folder_page_loader.ts +++ b/public/app/features/dashboard/folder_page_loader.ts @@ -1,7 +1,7 @@ export class FolderPageLoader { - constructor(private backendSrv, private $routeParams) {} + constructor(private backendSrv) {} - load(ctrl, folderId, activeChildId) { + load(ctrl, uid, activeChildId) { ctrl.navModel = { main: { 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 folderUrl = result.meta.url; 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'); dashTab.url = folderUrl; @@ -57,8 +58,4 @@ export class FolderPageLoader { return result; }); } - - createFolderUrl(folderId: number, slug: string) { - return `dashboards/folder/${folderId}/${slug}`; - } } diff --git a/public/app/features/dashboard/folder_permissions_ctrl.ts b/public/app/features/dashboard/folder_permissions_ctrl.ts index 63239c1aadb..4ab91acb3d9 100644 --- a/public/app/features/dashboard/folder_permissions_ctrl.ts +++ b/public/app/features/dashboard/folder_permissions_ctrl.ts @@ -3,20 +3,23 @@ import { FolderPageLoader } from './folder_page_loader'; export class FolderPermissionsCtrl { navModel: any; folderId: number; + uid: string; dashboard: any; meta: any; /** @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; + }); } } } diff --git a/public/app/features/dashboard/folder_settings_ctrl.ts b/public/app/features/dashboard/folder_settings_ctrl.ts index 7bb3d469a86..004ba2efa9f 100644 --- a/public/app/features/dashboard/folder_settings_ctrl.ts +++ b/public/app/features/dashboard/folder_settings_ctrl.ts @@ -5,6 +5,7 @@ export class FolderSettingsCtrl { folderPageLoader: FolderPageLoader; navModel: any; folderId: number; + uid: string; canSave = false; dashboard: any; meta: any; @@ -13,14 +14,18 @@ export class FolderSettingsCtrl { /** @ngInject */ constructor(private backendSrv, navModelSrv, private $routeParams, private $location) { - if (this.$routeParams.folderId && this.$routeParams.slug) { - this.folderId = $routeParams.folderId; + if (this.$routeParams.uid) { + this.uid = $routeParams.uid; - 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; + 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; }); } @@ -36,11 +41,10 @@ export class FolderSettingsCtrl { this.dashboard.title = this.title.trim(); return this.backendSrv - .saveDashboard(this.dashboard, { overwrite: false }) + .updateDashboardFolder(this.dashboard, { overwrite: false }) .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'); @@ -65,7 +69,7 @@ export class FolderSettingsCtrl { icon: 'fa-trash', yesText: 'Delete', 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`]); this.$location.url('dashboards'); }); @@ -84,7 +88,7 @@ export class FolderSettingsCtrl { yesText: 'Save & Overwrite', icon: 'fa-warning', onConfirm: () => { - this.backendSrv.saveDashboard(this.dashboard, { overwrite: true }); + this.backendSrv.updateDashboardFolder(this.dashboard, { overwrite: true }); }, }); } diff --git a/public/app/features/dashboard/partials/folder_dashboards.html b/public/app/features/dashboard/partials/folder_dashboards.html index a03c53233f5..1082bee402e 100644 --- a/public/app/features/dashboard/partials/folder_dashboards.html +++ b/public/app/features/dashboard/partials/folder_dashboards.html @@ -1,5 +1,5 @@
- +
diff --git a/public/app/features/dashboard/settings/settings.ts b/public/app/features/dashboard/settings/settings.ts index 6231a7b7adf..1b3af3fdfe9 100755 --- a/public/app/features/dashboard/settings/settings.ts +++ b/public/app/features/dashboard/settings/settings.ts @@ -182,7 +182,7 @@ export class SettingsCtrl { } 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']); this.$location.url('/'); }); diff --git a/public/app/routes/dashboard_loaders.ts b/public/app/routes/dashboard_loaders.ts index 4ad77512ad1..80e74819069 100644 --- a/public/app/routes/dashboard_loaders.ts +++ b/public/app/routes/dashboard_loaders.ts @@ -29,6 +29,10 @@ export class LoadDashboardCtrl { } 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) { result.meta.keepRows = true; } diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index a83cddee3e1..cab45b5aff3 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -81,19 +81,19 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { controller: 'CreateFolderCtrl', controllerAs: 'ctrl', }) - .when('/dashboards/folder/:folderId/:slug/permissions', { + .when('/dashboards/f/:uid/:slug/permissions', { template: '', resolve: { component: () => FolderPermissions, }, }) - .when('/dashboards/folder/:folderId/:slug/settings', { + .when('/dashboards/f/:uid/:slug/settings', { template: '', resolve: { component: () => FolderSettings, }, }) - .when('/dashboards/folder/:folderId/:slug', { + .when('/dashboards/f/:uid/:slug', { templateUrl: 'public/app/features/dashboard/partials/folder_dashboards.html', controller: 'FolderDashboardsCtrl', controllerAs: 'ctrl', diff --git a/public/app/stores/FolderStore/FolderStore.ts b/public/app/stores/FolderStore/FolderStore.ts index d0216e32ad7..6f14e7221f8 100644 --- a/public/app/stores/FolderStore/FolderStore.ts +++ b/public/app/stores/FolderStore/FolderStore.ts @@ -2,8 +2,8 @@ import { types, getEnv, flow } from 'mobx-state-tree'; export const Folder = types.model('Folder', { id: types.identifier(types.number), - slug: types.string, title: types.string, + url: types.string, canSave: types.boolean, hasChanged: types.boolean, }); @@ -13,13 +13,13 @@ export const FolderStore = types folder: types.maybe(Folder), }) .actions(self => ({ - load: flow(function* load(slug: string) { + load: flow(function* load(uid: string) { const backendSrv = getEnv(self).backendSrv; - const res = yield backendSrv.getDashboard('db', slug); + const res = yield backendSrv.getDashboardByUid(uid); self.folder = Folder.create({ id: res.dashboard.id, title: res.dashboard.title, - slug: res.meta.slug, + url: res.meta.url, canSave: res.meta.canSave, hasChanged: false, }); @@ -35,14 +35,15 @@ export const FolderStore = types const backendSrv = getEnv(self).backendSrv; 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() { const backendSrv = getEnv(self).backendSrv; - return backendSrv.deleteDashboard(self.folder.slug); + return backendSrv.deleteDashboard(self.folder.url); }), })); diff --git a/public/app/stores/NavStore/NavStore.jest.ts b/public/app/stores/NavStore/NavStore.jest.ts index b1ad820910d..43d4496c858 100644 --- a/public/app/stores/NavStore/NavStore.jest.ts +++ b/public/app/stores/NavStore/NavStore.jest.ts @@ -3,12 +3,12 @@ import { NavStore } from './NavStore'; describe('NavStore', () => { const folderId = 1; const folderTitle = 'Folder Name'; - const folderSlug = 'folder-name'; + const folderUrl = '/dashboards/f/uid/folder-name'; const canAdmin = true; const folder = { id: folderId, - slug: folderSlug, + url: folderUrl, title: folderTitle, canAdmin: canAdmin, }; @@ -33,9 +33,9 @@ describe('NavStore', () => { it('Should set correct urls for each tab', () => { 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', () => { diff --git a/public/app/stores/NavStore/NavStore.ts b/public/app/stores/NavStore/NavStore.ts index d04c5bf0259..86348c00487 100644 --- a/public/app/stores/NavStore/NavStore.ts +++ b/public/app/stores/NavStore/NavStore.ts @@ -41,8 +41,6 @@ export const NavStore = types }, initFolderNav(folder: any, activeChildId: string) { - const folderUrl = createFolderUrl(folder.id, folder.slug); - let main = { icon: 'fa fa-folder-open', id: 'manage-folder', @@ -56,21 +54,21 @@ export const NavStore = types icon: 'fa fa-fw fa-th-large', id: 'manage-folder-dashboards', text: 'Dashboards', - url: folderUrl, + url: folder.url, }, { active: activeChildId === 'manage-folder-permissions', icon: 'fa fa-fw fa-lock', id: 'manage-folder-permissions', text: 'Permissions', - url: folderUrl + '/permissions', + url: `${folder.url}/permissions`, }, { active: activeChildId === 'manage-folder-settings', icon: 'fa fa-fw fa-cog', id: 'manage-folder-settings', text: 'Settings', - url: folderUrl + '/settings', + url: `${folder.url}/settings`, }, ], }; @@ -118,7 +116,3 @@ export const NavStore = types self.main = NavItem.create(main); }, })); - -function createFolderUrl(folderId: number, slug: string) { - return `dashboards/folder/${folderId}/${slug}`; -} diff --git a/public/test/mocks/common.ts b/public/test/mocks/common.ts index da63381ddf4..3f80227435d 100644 --- a/public/test/mocks/common.ts +++ b/public/test/mocks/common.ts @@ -1,6 +1,7 @@ export const backendSrv = { get: jest.fn(), getDashboard: jest.fn(), + getDashboardByUid: jest.fn(), post: jest.fn(), };