Merge pull request #10706 from grafana/7883_frontend_step2

WIP: Dashboard & Persistent urls - Frontend Step 2
This commit is contained in:
Marcus Efraimsson 2018-02-01 13:55:03 +01:00 committed by GitHub
commit d8d82c1769
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 305 additions and 114 deletions

View File

@ -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))

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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"`

View File

@ -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
}

View File

@ -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() {

View File

@ -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');
}); });
} }

View File

@ -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,
}, },
}) })

View File

@ -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']);

View File

@ -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) { private deleteFoldersAndDashboards(uids) {
this.backendSrv.deleteDashboards(slugs).then(result => { 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: '=',
}, },
}; };
} }

View File

@ -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.getDashboardByUid(uid).then(fullDash => {
this.delete(`/api/dashboards/db/${slug}`) 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) { for (let uid of dashboardUids) {
tasks.push(this.createTask(this.deleteDashboard.bind(this), true, slug)); 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) { for (let uid of dashboardUids) {
tasks.push(this.createTask(this.moveDashboard.bind(this), true, slug, toFolder)); 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) {

View File

@ -34,10 +34,7 @@ export class BridgeSrv {
}); });
this.$rootScope.$on('$routeChangeSuccess', (evt, data) => { this.$rootScope.$on('$routeChangeSuccess', (evt, data) => {
let angularUrl = this.$location.url(); store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params);
if (store.view.currentUrl !== angularUrl) {
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);
} }
} }

View File

@ -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}`, url: hit.url,
slug: hit.slug,
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}`, url: hit.url,
slug: hit.slug,
items: [], items: [],
icon: 'fa fa-folder-open', icon: 'fa fa-folder-open',
toggle: this.toggleFolder.bind(this), toggle: this.toggleFolder.bind(this),

View File

@ -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,
}, },
]; ];

View File

@ -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']);
this.$location.url(result.meta.url);
var folderUrl = `dashboards/folder/${result.dashboard.id}/${result.meta.slug}`;
this.$location.url(folderUrl);
}); });
} }

View File

@ -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) { constructor(private backendSrv, navModelSrv, private $routeParams, $location) {
if (this.$routeParams.folderId && this.$routeParams.slug) { if (this.$routeParams.uid) {
this.folderId = $routeParams.folderId; 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 => { loader.load(this, this.uid, 'manage-folder-dashboards').then(folder => {
this.folderSlug = result.meta.slug; if ($location.path() !== folder.meta.url) {
$location.path(folder.meta.url).replace();
}
}); });
} }
} }

View File

@ -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}`;
}
} }

View File

@ -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) { constructor(private backendSrv, navModelSrv, private $routeParams, $location) {
if (this.$routeParams.folderId && this.$routeParams.slug) { if (this.$routeParams.uid) {
this.folderId = $routeParams.folderId; this.uid = $routeParams.uid;
new FolderPageLoader(this.backendSrv, this.$routeParams) new FolderPageLoader(this.backendSrv).load(this, this.uid, 'manage-folder-permissions').then(folder => {
.load(this, this.folderId, 'manage-folder-permissions') if ($location.path() !== folder.meta.url) {
.then(result => { $location.path(`${folder.meta.url}/permissions`).replace();
this.dashboard = result.dashboard; }
this.meta = result.meta;
}); this.dashboard = folder.dashboard;
this.meta = folder.meta;
});
} }
} }
} }

View File

@ -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) { if (this.$routeParams.uid) {
this.folderId = $routeParams.folderId; this.uid = $routeParams.uid;
this.folderPageLoader = new FolderPageLoader(this.backendSrv, this.$routeParams); this.folderPageLoader = new FolderPageLoader(this.backendSrv);
this.folderPageLoader.load(this, this.folderId, 'manage-folder-settings').then(result => { this.folderPageLoader.load(this, this.uid, 'manage-folder-settings').then(folder => {
this.dashboard = result.dashboard; if ($location.path() !== folder.meta.url) {
this.meta = result.meta; $location.path(`${folder.meta.url}/settings`).replace();
this.canSave = result.meta.canSave; }
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 (result.url !== this.$location.path()) {
if (folderUrl !== this.$location.path()) { this.$location.url(result.url + '/settings');
this.$location.url(folderUrl + '/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 });
}, },
}); });
} }

View File

@ -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>

View File

@ -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('/');
}); });

View File

@ -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;
} }

View File

@ -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',

View File

@ -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); const res = yield backendSrv.saveFolder(dashboard, options);
self.folder.slug = res.slug; self.folder.url = res.url;
return `dashboards/folder/${self.folder.id}/${res.slug}/settings`;
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);
}), }),
})); }));

View File

@ -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[0].url).toBe(folderUrl);
expect(store.main.children[1].url).toBe(`dashboards/folder/${folderId}/${folderSlug}/permissions`); expect(store.main.children[1].url).toBe(`${folderUrl}/permissions`);
expect(store.main.children[2].url).toBe(`dashboards/folder/${folderId}/${folderSlug}/settings`); expect(store.main.children[2].url).toBe(`${folderUrl}/settings`);
}); });
it('Should set active tab', () => { it('Should set active tab', () => {

View File

@ -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}`;
}

View File

@ -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(),
}; };