mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #10683 from grafana/7883_new_url_structure
New dashboard/folder url structure
This commit is contained in:
commit
f158a604a0
8
Gopkg.lock
generated
8
Gopkg.lock
generated
@ -412,6 +412,12 @@
|
||||
revision = "9e8dc3f972df6c8fcc0375ef492c24d0bb204857"
|
||||
version = "1.6.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/teris-io/shortid"
|
||||
packages = ["."]
|
||||
revision = "771a37caa5cf0c81f585d7b6df4dfc77e0615b5c"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/uber/jaeger-client-go"
|
||||
packages = [
|
||||
@ -625,6 +631,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "98e8d8f5fb21fe448aeb3db41c9fed85fe3bf80400e553211cf39a9c05720e01"
|
||||
inputs-digest = "4de68f1342ba98a637ec8ca7496aeeae2021bf9e4c7c80db7924e14709151a62"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
@ -193,3 +193,7 @@ ignored = [
|
||||
non-go = true
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/teris-io/shortid"
|
||||
|
@ -105,7 +105,8 @@ func transformToDTOs(alerts []*models.Alert, c *middleware.Context) ([]*dtos.Ale
|
||||
for _, alert := range alertDTOs {
|
||||
for _, dash := range dashboardsQuery.Result {
|
||||
if alert.DashboardId == dash.Id {
|
||||
alert.DashbboardUri = "db/" + dash.Slug
|
||||
alert.DashbboardUri = dash.GenerateUrl()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,8 @@ func (hs *HttpServer) registerRoutes() {
|
||||
reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
|
||||
reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
|
||||
reqOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
|
||||
redirectFromLegacyDashboardUrl := middleware.RedirectFromLegacyDashboardUrl()
|
||||
redirectFromLegacyDashboardSoloUrl := middleware.RedirectFromLegacyDashboardSoloUrl()
|
||||
quota := middleware.Quota
|
||||
bind := binding.Bind
|
||||
|
||||
@ -63,9 +65,13 @@ func (hs *HttpServer) registerRoutes() {
|
||||
r.Get("/plugins/:id/edit", reqSignedIn, Index)
|
||||
r.Get("/plugins/:id/page/:page", reqSignedIn, Index)
|
||||
|
||||
r.Get("/dashboard/*", reqSignedIn, Index)
|
||||
r.Get("/d/:uid/:slug", reqSignedIn, Index)
|
||||
r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardUrl, Index)
|
||||
r.Get("/dashboard/script/*", reqSignedIn, Index)
|
||||
r.Get("/dashboard-solo/snapshot/*", Index)
|
||||
r.Get("/dashboard-solo/*", reqSignedIn, Index)
|
||||
r.Get("/d-solo/:uid/:slug", reqSignedIn, Index)
|
||||
r.Get("/dashboard-solo/db/:slug", reqSignedIn, redirectFromLegacyDashboardSoloUrl, Index)
|
||||
r.Get("/dashboard-solo/script/*", reqSignedIn, Index)
|
||||
r.Get("/import/dashboard", reqSignedIn, Index)
|
||||
r.Get("/dashboards/", reqSignedIn, Index)
|
||||
r.Get("/dashboards/*", reqSignedIn, Index)
|
||||
@ -242,6 +248,9 @@ 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))
|
||||
|
||||
|
@ -44,7 +44,7 @@ func dashboardGuardianResponse(err error) Response {
|
||||
}
|
||||
|
||||
func GetDashboard(c *middleware.Context) Response {
|
||||
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0)
|
||||
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, c.Params(":uid"))
|
||||
if rsp != nil {
|
||||
return rsp
|
||||
}
|
||||
@ -89,6 +89,7 @@ func GetDashboard(c *middleware.Context) Response {
|
||||
IsFolder: dash.IsFolder,
|
||||
FolderId: dash.FolderId,
|
||||
FolderTitle: "Root",
|
||||
Url: dash.GetUrl(),
|
||||
}
|
||||
|
||||
// lookup folder title
|
||||
@ -124,8 +125,15 @@ func getUserLogin(userId int64) string {
|
||||
}
|
||||
}
|
||||
|
||||
func getDashboardHelper(orgId int64, slug string, id int64) (*m.Dashboard, Response) {
|
||||
query := m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgId}
|
||||
func getDashboardHelper(orgId int64, slug string, id int64, uid string) (*m.Dashboard, Response) {
|
||||
var query m.GetDashboardQuery
|
||||
|
||||
if len(uid) > 0 {
|
||||
query = m.GetDashboardQuery{Uid: uid, Id: id, OrgId: orgId}
|
||||
} else {
|
||||
query = m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgId}
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return nil, ApiError(404, "Dashboard not found", err)
|
||||
}
|
||||
@ -133,7 +141,37 @@ func getDashboardHelper(orgId int64, slug string, id int64) (*m.Dashboard, Respo
|
||||
}
|
||||
|
||||
func DeleteDashboard(c *middleware.Context) Response {
|
||||
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0)
|
||||
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
|
||||
}
|
||||
|
||||
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 DeleteDashboardByUid(c *middleware.Context) Response {
|
||||
dash, rsp := getDashboardHelper(c.OrgId, "", 0, c.Params(":uid"))
|
||||
if rsp != nil {
|
||||
return rsp
|
||||
}
|
||||
@ -208,7 +246,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err == m.ErrDashboardWithSameNameExists {
|
||||
if err == m.ErrDashboardWithSameUIDExists {
|
||||
return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()})
|
||||
}
|
||||
if err == m.ErrDashboardVersionMismatch {
|
||||
@ -232,8 +270,17 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
|
||||
}
|
||||
|
||||
dashboard.IsFolder = dash.IsFolder
|
||||
|
||||
c.TimeRequest(metrics.M_Api_Dashboard_Save)
|
||||
return Json(200, util.DynMap{"status": "success", "slug": dashboard.Slug, "version": dashboard.Version, "id": dashboard.Id})
|
||||
return Json(200, util.DynMap{
|
||||
"status": "success",
|
||||
"slug": dashboard.Slug,
|
||||
"version": dashboard.Version,
|
||||
"id": dashboard.Id,
|
||||
"uid": dashboard.Uid,
|
||||
"url": dashboard.GetUrl(),
|
||||
})
|
||||
}
|
||||
|
||||
func GetHomeDashboard(c *middleware.Context) Response {
|
||||
@ -400,7 +447,7 @@ func CalculateDashboardDiff(c *middleware.Context, apiOptions dtos.CalculateDiff
|
||||
|
||||
// RestoreDashboardVersion restores a dashboard to the given version.
|
||||
func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboardVersionCommand) Response {
|
||||
dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId"))
|
||||
dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId"), "")
|
||||
if rsp != nil {
|
||||
return rsp
|
||||
}
|
||||
@ -423,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)
|
||||
|
@ -39,8 +39,17 @@ 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 {
|
||||
query.Result = fakeDash
|
||||
getDashboardQueries = append(getDashboardQueries, query)
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -77,9 +86,13 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
Convey("When user is an Org Viewer", func() {
|
||||
role := m.ROLE_VIEWER
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
dash := GetDashboardShouldReturn200(sc)
|
||||
|
||||
Convey("Should lookup dashboard by slug", func() {
|
||||
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
|
||||
})
|
||||
|
||||
Convey("Should not be able to edit or save dashboard", func() {
|
||||
So(dash.Meta.CanEdit, ShouldBeFalse)
|
||||
So(dash.Meta.CanSave, ShouldBeFalse)
|
||||
@ -87,9 +100,36 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
dash := GetDashboardShouldReturn200(sc)
|
||||
|
||||
Convey("Should lookup dashboard by uid", func() {
|
||||
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
|
||||
})
|
||||
|
||||
Convey("Should not be able to edit or save dashboard", func() {
|
||||
So(dash.Meta.CanEdit, ShouldBeFalse)
|
||||
So(dash.Meta.CanSave, ShouldBeFalse)
|
||||
So(dash.Meta.CanAdmin, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
CallDeleteDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
|
||||
Convey("Should lookup dashboard by slug", func() {
|
||||
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")
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
|
||||
@ -111,9 +151,13 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
Convey("When user is an Org Editor", func() {
|
||||
role := m.ROLE_EDITOR
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
dash := GetDashboardShouldReturn200(sc)
|
||||
|
||||
Convey("Should lookup dashboard by slug", func() {
|
||||
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
|
||||
})
|
||||
|
||||
Convey("Should be able to edit or save dashboard", func() {
|
||||
So(dash.Meta.CanEdit, ShouldBeTrue)
|
||||
So(dash.Meta.CanSave, ShouldBeTrue)
|
||||
@ -121,9 +165,36 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
dash := GetDashboardShouldReturn200(sc)
|
||||
|
||||
Convey("Should lookup dashboard by uid", func() {
|
||||
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
|
||||
})
|
||||
|
||||
Convey("Should be able to edit or save dashboard", func() {
|
||||
So(dash.Meta.CanEdit, ShouldBeTrue)
|
||||
So(dash.Meta.CanSave, ShouldBeTrue)
|
||||
So(dash.Meta.CanAdmin, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
CallDeleteDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
|
||||
Convey("Should lookup dashboard by slug", func() {
|
||||
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, 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) {
|
||||
@ -137,8 +208,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
})
|
||||
|
||||
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
|
||||
CallPostDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
CallPostDashboardShouldReturnSuccess(sc)
|
||||
})
|
||||
|
||||
Convey("When saving a dashboard folder in another folder", func() {
|
||||
@ -172,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,
|
||||
@ -185,8 +261,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
var getDashboardQueries []*m.GetDashboardQuery
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
|
||||
query.Result = fakeDash
|
||||
getDashboardQueries = append(getDashboardQueries, query)
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -215,18 +294,48 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
Convey("When user is an Org Viewer and has no permissions for this dashboard", func() {
|
||||
role := m.ROLE_VIEWER
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
sc.handlerFunc = GetDashboard
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
Convey("Should lookup dashboard by slug", func() {
|
||||
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
|
||||
})
|
||||
|
||||
Convey("Should be denied access", func() {
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
sc.handlerFunc = GetDashboard
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
Convey("Should lookup dashboard by uid", func() {
|
||||
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
|
||||
})
|
||||
|
||||
Convey("Should be denied access", func() {
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
CallDeleteDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
|
||||
Convey("Should lookup dashboard by slug", func() {
|
||||
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")
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
|
||||
@ -248,18 +357,48 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
Convey("When user is an Org Editor and has no permissions for this dashboard", func() {
|
||||
role := m.ROLE_EDITOR
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
sc.handlerFunc = GetDashboard
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
Convey("Should lookup dashboard by slug", func() {
|
||||
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
|
||||
})
|
||||
|
||||
Convey("Should be denied access", func() {
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
sc.handlerFunc = GetDashboard
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
Convey("Should lookup dashboard by uid", func() {
|
||||
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
|
||||
})
|
||||
|
||||
Convey("Should be denied access", func() {
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
CallDeleteDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
|
||||
Convey("Should lookup dashboard by slug", func() {
|
||||
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")
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
|
||||
@ -290,9 +429,13 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
dash := GetDashboardShouldReturn200(sc)
|
||||
|
||||
Convey("Should lookup dashboard by slug", func() {
|
||||
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
|
||||
})
|
||||
|
||||
Convey("Should be able to get dashboard with edit rights", func() {
|
||||
So(dash.Meta.CanEdit, ShouldBeTrue)
|
||||
So(dash.Meta.CanSave, ShouldBeTrue)
|
||||
@ -300,9 +443,36 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
dash := GetDashboardShouldReturn200(sc)
|
||||
|
||||
Convey("Should lookup dashboard by uid", func() {
|
||||
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
|
||||
})
|
||||
|
||||
Convey("Should be able to get dashboard with edit rights", func() {
|
||||
So(dash.Meta.CanEdit, ShouldBeTrue)
|
||||
So(dash.Meta.CanSave, ShouldBeTrue)
|
||||
So(dash.Meta.CanAdmin, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
CallDeleteDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
|
||||
Convey("Should lookup dashboard by slug", func() {
|
||||
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, 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) {
|
||||
@ -316,8 +486,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
})
|
||||
|
||||
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
|
||||
CallPostDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
CallPostDashboardShouldReturnSuccess(sc)
|
||||
})
|
||||
})
|
||||
|
||||
@ -334,9 +503,13 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
dash := GetDashboardShouldReturn200(sc)
|
||||
|
||||
Convey("Should lookup dashboard by slug", func() {
|
||||
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
|
||||
})
|
||||
|
||||
Convey("Should be able to get dashboard with edit rights but can save should be false", func() {
|
||||
So(dash.Meta.CanEdit, ShouldBeTrue)
|
||||
So(dash.Meta.CanSave, ShouldBeFalse)
|
||||
@ -344,9 +517,36 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
dash := GetDashboardShouldReturn200(sc)
|
||||
|
||||
Convey("Should lookup dashboard by uid", func() {
|
||||
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
|
||||
})
|
||||
|
||||
Convey("Should be able to get dashboard with edit rights but can save should be false", func() {
|
||||
So(dash.Meta.CanEdit, ShouldBeTrue)
|
||||
So(dash.Meta.CanSave, ShouldBeFalse)
|
||||
So(dash.Meta.CanAdmin, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
CallDeleteDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
|
||||
Convey("Should lookup dashboard by slug", func() {
|
||||
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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -362,9 +562,13 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
dash := GetDashboardShouldReturn200(sc)
|
||||
|
||||
Convey("Should lookup dashboard by slug", func() {
|
||||
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
|
||||
})
|
||||
|
||||
Convey("Should be able to get dashboard with edit rights", func() {
|
||||
So(dash.Meta.CanEdit, ShouldBeTrue)
|
||||
So(dash.Meta.CanSave, ShouldBeTrue)
|
||||
@ -372,9 +576,36 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
dash := GetDashboardShouldReturn200(sc)
|
||||
|
||||
Convey("Should lookup dashboard by uid", func() {
|
||||
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
|
||||
})
|
||||
|
||||
Convey("Should be able to get dashboard with edit rights", func() {
|
||||
So(dash.Meta.CanEdit, ShouldBeTrue)
|
||||
So(dash.Meta.CanSave, ShouldBeTrue)
|
||||
So(dash.Meta.CanAdmin, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
CallDeleteDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
|
||||
Convey("Should lookup dashboard by slug", func() {
|
||||
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, 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) {
|
||||
@ -388,8 +619,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
})
|
||||
|
||||
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
|
||||
CallPostDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
CallPostDashboardShouldReturnSuccess(sc)
|
||||
})
|
||||
})
|
||||
|
||||
@ -405,18 +635,48 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
dash := GetDashboardShouldReturn200(sc)
|
||||
|
||||
Convey("Should lookup dashboard by slug", func() {
|
||||
So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash")
|
||||
})
|
||||
|
||||
Convey("Should not be able to edit or save dashboard", func() {
|
||||
So(dash.Meta.CanEdit, ShouldBeFalse)
|
||||
So(dash.Meta.CanSave, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
dash := GetDashboardShouldReturn200(sc)
|
||||
|
||||
Convey("Should lookup dashboard by uid", func() {
|
||||
So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi")
|
||||
})
|
||||
|
||||
Convey("Should not be able to edit or save dashboard", func() {
|
||||
So(dash.Meta.CanEdit, ShouldBeFalse)
|
||||
So(dash.Meta.CanSave, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
CallDeleteDashboard(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
|
||||
Convey("Should lookup dashboard by slug", func() {
|
||||
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")
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
|
||||
@ -435,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 {
|
||||
@ -479,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
|
||||
@ -496,6 +796,18 @@ func CallPostDashboard(sc *scenarioContext) {
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func CallPostDashboardShouldReturnSuccess(sc *scenarioContext) {
|
||||
CallPostDashboard(sc)
|
||||
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
result := sc.ToJson()
|
||||
So(result.Get("status").MustString(), ShouldEqual, "success")
|
||||
So(result.Get("id").MustInt64(), ShouldBeGreaterThan, 0)
|
||||
So(result.Get("uid").MustString(), ShouldNotBeNil)
|
||||
So(result.Get("slug").MustString(), ShouldNotBeNil)
|
||||
So(result.Get("url").MustString(), ShouldNotBeNil)
|
||||
}
|
||||
|
||||
func postDashboardScenario(desc string, url string, routePattern string, role m.RoleType, cmd m.SaveDashboardCommand, fn scenarioFunc) {
|
||||
Convey(desc+" "+url, func() {
|
||||
defer bus.ClearBusHandlers()
|
||||
@ -518,3 +830,10 @@ func postDashboardScenario(desc string, url string, routePattern string, role m.
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) ToJson() *simplejson.Json {
|
||||
var result *simplejson.Json
|
||||
err := json.NewDecoder(sc.resp.Body).Decode(&result)
|
||||
So(err, ShouldBeNil)
|
||||
return result
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ type DashboardMeta struct {
|
||||
CanAdmin bool `json:"canAdmin"`
|
||||
CanStar bool `json:"canStar"`
|
||||
Slug string `json:"slug"`
|
||||
Url string `json:"url"`
|
||||
Expires time.Time `json:"expires"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
|
46
pkg/middleware/dashboard_redirect.go
Normal file
46
pkg/middleware/dashboard_redirect.go
Normal file
@ -0,0 +1,46 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
func getDashboardUrlBySlug(orgId int64, slug string) (string, error) {
|
||||
query := m.GetDashboardQuery{Slug: slug, OrgId: orgId}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return "", m.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
return m.GetDashboardUrl(query.Result.Uid, query.Result.Slug), nil
|
||||
}
|
||||
|
||||
func RedirectFromLegacyDashboardUrl() macaron.Handler {
|
||||
return func(c *Context) {
|
||||
slug := c.Params("slug")
|
||||
|
||||
if slug != "" {
|
||||
if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil {
|
||||
c.Redirect(url, 301)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func RedirectFromLegacyDashboardSoloUrl() macaron.Handler {
|
||||
return func(c *Context) {
|
||||
slug := c.Params("slug")
|
||||
|
||||
if slug != "" {
|
||||
if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil {
|
||||
url = strings.Replace(url, "/d/", "/d-solo/", 1)
|
||||
c.Redirect(url, 301)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
56
pkg/middleware/dashboard_redirect_test.go
Normal file
56
pkg/middleware/dashboard_redirect_test.go
Normal file
@ -0,0 +1,56 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestMiddlewareDashboardRedirect(t *testing.T) {
|
||||
Convey("Given the dashboard redirect middleware", t, func() {
|
||||
bus.ClearBusHandlers()
|
||||
redirectFromLegacyDashboardUrl := RedirectFromLegacyDashboardUrl()
|
||||
redirectFromLegacyDashboardSoloUrl := RedirectFromLegacyDashboardSoloUrl()
|
||||
|
||||
fakeDash := m.NewDashboard("Child dash")
|
||||
fakeDash.Id = 1
|
||||
fakeDash.FolderId = 1
|
||||
fakeDash.HasAcl = false
|
||||
fakeDash.Uid = util.GenerateShortUid()
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
|
||||
query.Result = fakeDash
|
||||
return nil
|
||||
})
|
||||
|
||||
middlewareScenario("GET dashboard by legacy url", func(sc *scenarioContext) {
|
||||
sc.m.Get("/dashboard/db/:slug", redirectFromLegacyDashboardUrl, sc.defaultHandler)
|
||||
|
||||
sc.fakeReqWithParams("GET", "/dashboard/db/dash", map[string]string{}).exec()
|
||||
|
||||
Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() {
|
||||
So(sc.resp.Code, ShouldEqual, 301)
|
||||
redirectUrl, _ := sc.resp.Result().Location()
|
||||
So(redirectUrl.Path, ShouldEqual, m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug))
|
||||
})
|
||||
})
|
||||
|
||||
middlewareScenario("GET dashboard solo by legacy url", func(sc *scenarioContext) {
|
||||
sc.m.Get("/dashboard-solo/db/:slug", redirectFromLegacyDashboardSoloUrl, sc.defaultHandler)
|
||||
|
||||
sc.fakeReqWithParams("GET", "/dashboard-solo/db/dash", map[string]string{}).exec()
|
||||
|
||||
Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() {
|
||||
So(sc.resp.Code, ShouldEqual, 301)
|
||||
redirectUrl, _ := sc.resp.Result().Location()
|
||||
expectedUrl := m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug)
|
||||
expectedUrl = strings.Replace(expectedUrl, "/d/", "/d-solo/", 1)
|
||||
So(redirectUrl.Path, ShouldEqual, expectedUrl)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
@ -399,6 +399,20 @@ func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
|
||||
return sc
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map[string]string) *scenarioContext {
|
||||
sc.resp = httptest.NewRecorder()
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
q := req.URL.Query()
|
||||
for k, v := range queryParams {
|
||||
q.Add(k, v)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
So(err, ShouldBeNil)
|
||||
sc.req = req
|
||||
|
||||
return sc
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) handler(fn handlerFunc) *scenarioContext {
|
||||
sc.handlerFunc = fn
|
||||
return sc
|
||||
|
@ -2,23 +2,28 @@ package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
// Typed errors
|
||||
var (
|
||||
ErrDashboardNotFound = errors.New("Dashboard not found")
|
||||
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
|
||||
ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists")
|
||||
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
|
||||
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
|
||||
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")
|
||||
ErrDashboardNotFound = errors.New("Dashboard not found")
|
||||
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
|
||||
ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists")
|
||||
ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists")
|
||||
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
|
||||
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
|
||||
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")
|
||||
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
|
||||
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
|
||||
)
|
||||
|
||||
type UpdatePluginDashboardError struct {
|
||||
@ -39,6 +44,7 @@ var (
|
||||
// Dashboard model
|
||||
type Dashboard struct {
|
||||
Id int64
|
||||
Uid string
|
||||
Slug string
|
||||
OrgId int64
|
||||
GnetId int64
|
||||
@ -107,6 +113,10 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard {
|
||||
dash.GnetId = int64(gnetId)
|
||||
}
|
||||
|
||||
if uid, err := dash.Data.Get("uid").String(); err == nil {
|
||||
dash.Uid = uid
|
||||
}
|
||||
|
||||
return dash
|
||||
}
|
||||
|
||||
@ -147,6 +157,40 @@ func SlugifyTitle(title string) string {
|
||||
return slug.Make(strings.ToLower(title))
|
||||
}
|
||||
|
||||
// GetUrl return the html url for a folder if it's folder, otherwise for a dashboard
|
||||
func (dash *Dashboard) GetUrl() string {
|
||||
return GetDashboardFolderUrl(dash.IsFolder, dash.Uid, dash.Slug)
|
||||
}
|
||||
|
||||
// Return the html url for a dashboard
|
||||
func (dash *Dashboard) GenerateUrl() string {
|
||||
return GetDashboardUrl(dash.Uid, dash.Slug)
|
||||
}
|
||||
|
||||
// GetDashboardFolderUrl return the html url for a folder if it's folder, otherwise for a dashboard
|
||||
func GetDashboardFolderUrl(isFolder bool, uid string, slug string) string {
|
||||
if isFolder {
|
||||
return GetFolderUrl(uid, slug)
|
||||
}
|
||||
|
||||
return GetDashboardUrl(uid, slug)
|
||||
}
|
||||
|
||||
// Return the html url for a dashboard
|
||||
func GetDashboardUrl(uid string, slug string) string {
|
||||
return fmt.Sprintf("%s/d/%s/%s", setting.AppSubUrl, uid, slug)
|
||||
}
|
||||
|
||||
// Return the full url for a dashboard
|
||||
func GetFullDashboardUrl(uid string, slug string) string {
|
||||
return fmt.Sprintf("%s%s", setting.AppUrl, GetDashboardUrl(uid, slug))
|
||||
}
|
||||
|
||||
// GetFolderUrl return the html url for a folder
|
||||
func GetFolderUrl(folderUid string, slug string) string {
|
||||
return fmt.Sprintf("%s/dashboards/f/%s/%s", setting.AppSubUrl, folderUid, slug)
|
||||
}
|
||||
|
||||
//
|
||||
// COMMANDS
|
||||
//
|
||||
@ -177,8 +221,9 @@ type DeleteDashboardCommand struct {
|
||||
//
|
||||
|
||||
type GetDashboardQuery struct {
|
||||
Slug string // required if no Id is specified
|
||||
Slug string // required if no Id or Uid is specified
|
||||
Id int64 // optional if slug is set
|
||||
Uid string // optional if slug is set
|
||||
OrgId int64
|
||||
|
||||
Result *Dashboard
|
||||
@ -218,6 +263,13 @@ type GetDashboardSlugByIdQuery struct {
|
||||
Result string
|
||||
}
|
||||
|
||||
type GetDashboardsBySlugQuery struct {
|
||||
OrgId int64
|
||||
Slug string
|
||||
|
||||
Result []*Dashboard
|
||||
}
|
||||
|
||||
type GetFoldersForSignedInUserQuery struct {
|
||||
OrgId int64
|
||||
SignedInUser *SignedInUser
|
||||
@ -235,3 +287,13 @@ type DashboardPermissionForUser struct {
|
||||
Permission PermissionType `json:"permission"`
|
||||
PermissionName string `json:"permissionName"`
|
||||
}
|
||||
|
||||
type DashboardRef struct {
|
||||
Uid string
|
||||
Slug string
|
||||
}
|
||||
|
||||
type GetDashboardUIDByIdQuery struct {
|
||||
Id int64
|
||||
Result *DashboardRef
|
||||
}
|
||||
|
@ -12,17 +12,19 @@ import (
|
||||
)
|
||||
|
||||
type EvalContext struct {
|
||||
Firing bool
|
||||
IsTestRun bool
|
||||
EvalMatches []*EvalMatch
|
||||
Logs []*ResultLogEntry
|
||||
Error error
|
||||
ConditionEvals string
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
Rule *Rule
|
||||
log log.Logger
|
||||
dashboardSlug string
|
||||
Firing bool
|
||||
IsTestRun bool
|
||||
EvalMatches []*EvalMatch
|
||||
Logs []*ResultLogEntry
|
||||
Error error
|
||||
ConditionEvals string
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
Rule *Rule
|
||||
log log.Logger
|
||||
|
||||
dashboardRef *m.DashboardRef
|
||||
|
||||
ImagePublicUrl string
|
||||
ImageOnDiskPath string
|
||||
NoDataFound bool
|
||||
@ -83,29 +85,30 @@ func (c *EvalContext) GetNotificationTitle() string {
|
||||
return "[" + c.GetStateModel().Text + "] " + c.Rule.Name
|
||||
}
|
||||
|
||||
func (c *EvalContext) GetDashboardSlug() (string, error) {
|
||||
if c.dashboardSlug != "" {
|
||||
return c.dashboardSlug, nil
|
||||
func (c *EvalContext) GetDashboardUID() (*m.DashboardRef, error) {
|
||||
if c.dashboardRef != nil {
|
||||
return c.dashboardRef, nil
|
||||
}
|
||||
|
||||
slugQuery := &m.GetDashboardSlugByIdQuery{Id: c.Rule.DashboardId}
|
||||
if err := bus.Dispatch(slugQuery); err != nil {
|
||||
return "", err
|
||||
uidQuery := &m.GetDashboardUIDByIdQuery{Id: c.Rule.DashboardId}
|
||||
if err := bus.Dispatch(uidQuery); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.dashboardSlug = slugQuery.Result
|
||||
return c.dashboardSlug, nil
|
||||
c.dashboardRef = uidQuery.Result
|
||||
return c.dashboardRef, nil
|
||||
}
|
||||
|
||||
const urlFormat = "%s?fullscreen=true&edit=true&tab=alert&panelId=%d&orgId=%d"
|
||||
|
||||
func (c *EvalContext) GetRuleUrl() (string, error) {
|
||||
if c.IsTestRun {
|
||||
return setting.AppUrl, nil
|
||||
}
|
||||
|
||||
if slug, err := c.GetDashboardSlug(); err != nil {
|
||||
if ref, err := c.GetDashboardUID(); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
ruleUrl := fmt.Sprintf("%sdashboard/db/%s?fullscreen&edit&tab=alert&panelId=%d&orgId=%d", setting.AppUrl, slug, c.Rule.PanelId, c.Rule.OrgId)
|
||||
return ruleUrl, nil
|
||||
return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil
|
||||
}
|
||||
}
|
||||
|
@ -87,10 +87,10 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
||||
IsAlertContext: true,
|
||||
}
|
||||
|
||||
if slug, err := context.GetDashboardSlug(); err != nil {
|
||||
if ref, err := context.GetDashboardUID(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
renderOpts.Path = fmt.Sprintf("dashboard-solo/db/%s?&panelId=%d", slug, context.Rule.PanelId)
|
||||
renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?panelId=%d", ref.Uid, ref.Slug, context.Rule.PanelId)
|
||||
}
|
||||
|
||||
if imagePath, err := renderer.RenderToPng(renderOpts); err != nil {
|
||||
|
@ -13,9 +13,10 @@ const (
|
||||
|
||||
type Hit struct {
|
||||
Id int64 `json:"id"`
|
||||
Uid string `json:"uid"`
|
||||
Title string `json:"title"`
|
||||
Uri string `json:"uri"`
|
||||
Slug string `json:"slug"`
|
||||
Url string `json:"url"`
|
||||
Type HitType `json:"type"`
|
||||
Tags []string `json:"tags"`
|
||||
IsStarred bool `json:"isStarred"`
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -18,19 +19,23 @@ func init() {
|
||||
bus.AddHandler("sql", SearchDashboards)
|
||||
bus.AddHandler("sql", GetDashboardTags)
|
||||
bus.AddHandler("sql", GetDashboardSlugById)
|
||||
bus.AddHandler("sql", GetDashboardUIDById)
|
||||
bus.AddHandler("sql", GetDashboardsByPluginId)
|
||||
bus.AddHandler("sql", GetFoldersForSignedInUser)
|
||||
bus.AddHandler("sql", GetDashboardPermissionsForUser)
|
||||
bus.AddHandler("sql", GetDashboardsBySlug)
|
||||
}
|
||||
|
||||
var generateNewUid func() string = util.GenerateShortUid
|
||||
|
||||
func SaveDashboard(cmd *m.SaveDashboardCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
dash := cmd.GetDashboardModel()
|
||||
|
||||
// try get existing dashboard
|
||||
var existing, sameTitle m.Dashboard
|
||||
var existing m.Dashboard
|
||||
|
||||
if dash.Id > 0 {
|
||||
if dash.Id != 0 {
|
||||
dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -52,25 +57,40 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
|
||||
if existing.PluginId != "" && cmd.Overwrite == false {
|
||||
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
|
||||
}
|
||||
}
|
||||
} else if dash.Uid != "" {
|
||||
var sameUid m.Dashboard
|
||||
sameUidExists, err := sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&sameUid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sameTitleExists, err := sess.Where("org_id=? AND slug=?", dash.OrgId, dash.Slug).Get(&sameTitle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sameTitleExists {
|
||||
// another dashboard with same name
|
||||
if dash.Id != sameTitle.Id {
|
||||
if cmd.Overwrite {
|
||||
dash.Id = sameTitle.Id
|
||||
dash.Version = sameTitle.Version
|
||||
} else {
|
||||
return m.ErrDashboardWithSameNameExists
|
||||
if sameUidExists {
|
||||
// another dashboard with same uid
|
||||
if dash.Id != sameUid.Id {
|
||||
if cmd.Overwrite {
|
||||
dash.Id = sameUid.Id
|
||||
dash.Version = sameUid.Version
|
||||
} else {
|
||||
return m.ErrDashboardWithSameUIDExists
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dash.Uid == "" {
|
||||
uid, err := generateNewDashboardUid(sess, dash.OrgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dash.Uid = uid
|
||||
dash.Data.Set("uid", uid)
|
||||
}
|
||||
|
||||
err := guaranteeDashboardNameIsUniqueInFolder(sess, dash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = setHasAcl(sess, dash)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -92,7 +112,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
|
||||
dash.Updated = cmd.UpdatedAt
|
||||
}
|
||||
|
||||
affectedRows, err = sess.MustCols("folder_id", "has_acl").Id(dash.Id).Update(dash)
|
||||
affectedRows, err = sess.MustCols("folder_id", "has_acl").ID(dash.Id).Update(dash)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@ -142,6 +162,40 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
|
||||
})
|
||||
}
|
||||
|
||||
func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) {
|
||||
for i := 0; i < 3; i++ {
|
||||
uid := generateNewUid()
|
||||
|
||||
exists, err := sess.Where("org_id=? AND uid=?", orgId, uid).Get(&m.Dashboard{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return uid, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", m.ErrDashboardFailedGenerateUniqueUid
|
||||
}
|
||||
|
||||
func guaranteeDashboardNameIsUniqueInFolder(sess *DBSession, dash *m.Dashboard) error {
|
||||
var sameNameInFolder m.Dashboard
|
||||
sameNameInFolderExist, err := sess.Where("org_id=? AND title=? AND folder_id = ? AND uid <> ?",
|
||||
dash.OrgId, dash.Title, dash.FolderId, dash.Uid).
|
||||
Get(&sameNameInFolder)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sameNameInFolderExist {
|
||||
return m.ErrDashboardWithSameNameInFolderExists
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setHasAcl(sess *DBSession, dash *m.Dashboard) error {
|
||||
// check if parent has acl
|
||||
if dash.FolderId > 0 {
|
||||
@ -168,7 +222,7 @@ func setHasAcl(sess *DBSession, dash *m.Dashboard) error {
|
||||
}
|
||||
|
||||
func GetDashboard(query *m.GetDashboardQuery) error {
|
||||
dashboard := m.Dashboard{Slug: query.Slug, OrgId: query.OrgId, Id: query.Id}
|
||||
dashboard := m.Dashboard{Slug: query.Slug, OrgId: query.OrgId, Id: query.Id, Uid: query.Uid}
|
||||
has, err := x.Get(&dashboard)
|
||||
|
||||
if err != nil {
|
||||
@ -178,12 +232,14 @@ func GetDashboard(query *m.GetDashboardQuery) error {
|
||||
}
|
||||
|
||||
dashboard.Data.Set("id", dashboard.Id)
|
||||
dashboard.Data.Set("uid", dashboard.Uid)
|
||||
query.Result = &dashboard
|
||||
return nil
|
||||
}
|
||||
|
||||
type DashboardSearchProjection struct {
|
||||
Id int64
|
||||
Uid string
|
||||
Title string
|
||||
Slug string
|
||||
Term string
|
||||
@ -261,15 +317,17 @@ 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,
|
||||
Slug: item.Slug,
|
||||
Url: m.GetDashboardFolderUrl(item.IsFolder, item.Uid, item.Slug),
|
||||
Type: getHitType(item),
|
||||
FolderId: item.FolderId,
|
||||
FolderTitle: item.FolderTitle,
|
||||
FolderSlug: item.FolderSlug,
|
||||
Tags: []string{},
|
||||
}
|
||||
|
||||
query.Result = append(query.Result, hit)
|
||||
hits[item.Id] = hit
|
||||
}
|
||||
@ -488,7 +546,7 @@ func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error {
|
||||
var rawSql = `SELECT slug from dashboard WHERE Id=?`
|
||||
var slug = DashboardSlugDTO{}
|
||||
|
||||
exists, err := x.Sql(rawSql, query.Id).Get(&slug)
|
||||
exists, err := x.SQL(rawSql, query.Id).Get(&slug)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@ -499,3 +557,31 @@ func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error {
|
||||
query.Result = slug.Slug
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetDashboardsBySlug(query *m.GetDashboardsBySlugQuery) error {
|
||||
var dashboards []*m.Dashboard
|
||||
|
||||
if err := x.Where("org_id=? AND slug=?", query.OrgId, query.Slug).Find(&dashboards); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query.Result = dashboards
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetDashboardUIDById(query *m.GetDashboardUIDByIdQuery) error {
|
||||
var rawSql = `SELECT uid, slug from dashboard WHERE Id=?`
|
||||
|
||||
us := &m.DashboardRef{}
|
||||
|
||||
exists, err := x.SQL(rawSql, query.Id).Get(us)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if exists == false {
|
||||
return m.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
query.Result = us
|
||||
return nil
|
||||
}
|
||||
|
349
pkg/services/sqlstore/dashboard_folder_test.go
Normal file
349
pkg/services/sqlstore/dashboard_folder_test.go
Normal file
@ -0,0 +1,349 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
)
|
||||
|
||||
func TestDashboardFolderDataAccess(t *testing.T) {
|
||||
var x *xorm.Engine
|
||||
|
||||
Convey("Testing DB", t, func() {
|
||||
x = InitTestDB(t)
|
||||
|
||||
Convey("Given one dashboard folder with two dashboards and one dashboard in the root folder", func() {
|
||||
folder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp")
|
||||
dashInRoot := insertTestDashboard("test dash 67", 1, 0, false, "prod", "webapp")
|
||||
childDash := insertTestDashboard("test dash 23", 1, folder.Id, false, "prod", "webapp")
|
||||
insertTestDashboard("test dash 45", 1, folder.Id, false, "prod")
|
||||
|
||||
currentUser := createUser("viewer", "Viewer", false)
|
||||
|
||||
Convey("and no acls are set", func() {
|
||||
Convey("should return all dashboards", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].Id, ShouldEqual, folder.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("and acl is set for dashboard folder", func() {
|
||||
var otherUser int64 = 999
|
||||
updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT)
|
||||
|
||||
Convey("should not return folder", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
|
||||
Convey("when the user is given permission", func() {
|
||||
updateTestDashboardWithAcl(folder.Id, currentUser.Id, m.PERMISSION_EDIT)
|
||||
|
||||
Convey("should be able to access folder", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].Id, ShouldEqual, folder.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("when the user is an admin", func() {
|
||||
Convey("should be able to access folder", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{
|
||||
SignedInUser: &m.SignedInUser{
|
||||
UserId: currentUser.Id,
|
||||
OrgId: 1,
|
||||
OrgRole: m.ROLE_ADMIN,
|
||||
},
|
||||
OrgId: 1,
|
||||
DashboardIds: []int64{folder.Id, dashInRoot.Id},
|
||||
}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].Id, ShouldEqual, folder.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("and acl is set for dashboard child and folder has all permissions removed", func() {
|
||||
var otherUser int64 = 999
|
||||
aclId := updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT)
|
||||
removeAcl(aclId)
|
||||
updateTestDashboardWithAcl(childDash.Id, otherUser, m.PERMISSION_EDIT)
|
||||
|
||||
Convey("should not return folder or child", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
|
||||
Convey("when the user is given permission to child", func() {
|
||||
updateTestDashboardWithAcl(childDash.Id, currentUser.Id, m.PERMISSION_EDIT)
|
||||
|
||||
Convey("should be able to search for child dashboard but not folder", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].Id, ShouldEqual, childDash.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("when the user is an admin", func() {
|
||||
Convey("should be able to search for child dash and folder", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{
|
||||
SignedInUser: &m.SignedInUser{
|
||||
UserId: currentUser.Id,
|
||||
OrgId: 1,
|
||||
OrgRole: m.ROLE_ADMIN,
|
||||
},
|
||||
OrgId: 1,
|
||||
DashboardIds: []int64{folder.Id, dashInRoot.Id, childDash.Id},
|
||||
}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 3)
|
||||
So(query.Result[0].Id, ShouldEqual, folder.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, childDash.Id)
|
||||
So(query.Result[2].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given two dashboard folders with one dashboard each and one dashboard in the root folder", func() {
|
||||
folder1 := insertTestDashboard("1 test dash folder", 1, 0, true, "prod")
|
||||
folder2 := insertTestDashboard("2 test dash folder", 1, 0, true, "prod")
|
||||
dashInRoot := insertTestDashboard("test dash 67", 1, 0, false, "prod")
|
||||
childDash1 := insertTestDashboard("child dash 1", 1, folder1.Id, false, "prod")
|
||||
childDash2 := insertTestDashboard("child dash 2", 1, folder2.Id, false, "prod")
|
||||
|
||||
currentUser := createUser("viewer", "Viewer", false)
|
||||
var rootFolderId int64 = 0
|
||||
|
||||
Convey("and one folder is expanded, the other collapsed", func() {
|
||||
Convey("should return dashboards in root and expanded folder", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{FolderIds: []int64{rootFolderId, folder1.Id}, SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 4)
|
||||
So(query.Result[0].Id, ShouldEqual, folder1.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, folder2.Id)
|
||||
So(query.Result[2].Id, ShouldEqual, childDash1.Id)
|
||||
So(query.Result[3].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("and acl is set for one dashboard folder", func() {
|
||||
var otherUser int64 = 999
|
||||
updateTestDashboardWithAcl(folder1.Id, otherUser, m.PERMISSION_EDIT)
|
||||
|
||||
Convey("and a dashboard is moved from folder without acl to the folder with an acl", func() {
|
||||
movedDash := moveDashboard(1, childDash2.Data, folder1.Id)
|
||||
So(movedDash.HasAcl, ShouldBeTrue)
|
||||
|
||||
Convey("should not return folder with acl or its children", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{
|
||||
SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1},
|
||||
OrgId: 1,
|
||||
DashboardIds: []int64{folder1.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
|
||||
}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("and a dashboard is moved from folder with acl to the folder without an acl", func() {
|
||||
movedDash := moveDashboard(1, childDash1.Data, folder2.Id)
|
||||
So(movedDash.HasAcl, ShouldBeFalse)
|
||||
|
||||
Convey("should return folder without acl and its children", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{
|
||||
SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1},
|
||||
OrgId: 1,
|
||||
DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
|
||||
}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 4)
|
||||
So(query.Result[0].Id, ShouldEqual, folder2.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, childDash1.Id)
|
||||
So(query.Result[2].Id, ShouldEqual, childDash2.Id)
|
||||
So(query.Result[3].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("and a dashboard with an acl is moved to the folder without an acl", func() {
|
||||
updateTestDashboardWithAcl(childDash1.Id, otherUser, m.PERMISSION_EDIT)
|
||||
movedDash := moveDashboard(1, childDash1.Data, folder2.Id)
|
||||
So(movedDash.HasAcl, ShouldBeTrue)
|
||||
|
||||
Convey("should return folder without acl but not the dashboard with acl", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{
|
||||
SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1},
|
||||
OrgId: 1,
|
||||
DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
|
||||
}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 3)
|
||||
So(query.Result[0].Id, ShouldEqual, folder2.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, childDash2.Id)
|
||||
So(query.Result[2].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given two dashboard folders", func() {
|
||||
|
||||
folder1 := insertTestDashboard("1 test dash folder", 1, 0, true, "prod")
|
||||
folder2 := insertTestDashboard("2 test dash folder", 1, 0, true, "prod")
|
||||
|
||||
adminUser := createUser("admin", "Admin", true)
|
||||
editorUser := createUser("editor", "Editor", false)
|
||||
viewerUser := createUser("viewer", "Viewer", false)
|
||||
|
||||
Convey("Admin users", func() {
|
||||
Convey("Should have write access to all dashboard folders", func() {
|
||||
query := m.GetFoldersForSignedInUserQuery{
|
||||
OrgId: 1,
|
||||
SignedInUser: &m.SignedInUser{UserId: adminUser.Id, OrgRole: m.ROLE_ADMIN},
|
||||
}
|
||||
|
||||
err := GetFoldersForSignedInUser(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].Id, ShouldEqual, folder1.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, folder2.Id)
|
||||
})
|
||||
|
||||
Convey("should have write access to all folders and dashboards", func() {
|
||||
query := m.GetDashboardPermissionsForUserQuery{
|
||||
DashboardIds: []int64{folder1.Id, folder2.Id},
|
||||
OrgId: 1,
|
||||
UserId: adminUser.Id,
|
||||
OrgRole: m.ROLE_ADMIN,
|
||||
}
|
||||
|
||||
err := GetDashboardPermissionsForUser(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].DashboardId, ShouldEqual, folder1.Id)
|
||||
So(query.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN)
|
||||
So(query.Result[1].DashboardId, ShouldEqual, folder2.Id)
|
||||
So(query.Result[1].Permission, ShouldEqual, m.PERMISSION_ADMIN)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Editor users", func() {
|
||||
query := m.GetFoldersForSignedInUserQuery{
|
||||
OrgId: 1,
|
||||
SignedInUser: &m.SignedInUser{UserId: editorUser.Id, OrgRole: m.ROLE_EDITOR},
|
||||
}
|
||||
|
||||
Convey("Should have write access to all dashboard folders with default ACL", func() {
|
||||
err := GetFoldersForSignedInUser(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].Id, ShouldEqual, folder1.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, folder2.Id)
|
||||
})
|
||||
|
||||
Convey("should have edit access to folders with default ACL", func() {
|
||||
query := m.GetDashboardPermissionsForUserQuery{
|
||||
DashboardIds: []int64{folder1.Id, folder2.Id},
|
||||
OrgId: 1,
|
||||
UserId: editorUser.Id,
|
||||
OrgRole: m.ROLE_EDITOR,
|
||||
}
|
||||
|
||||
err := GetDashboardPermissionsForUser(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].DashboardId, ShouldEqual, folder1.Id)
|
||||
So(query.Result[0].Permission, ShouldEqual, m.PERMISSION_EDIT)
|
||||
So(query.Result[1].DashboardId, ShouldEqual, folder2.Id)
|
||||
So(query.Result[1].Permission, ShouldEqual, m.PERMISSION_EDIT)
|
||||
})
|
||||
|
||||
Convey("Should have write access to one dashboard folder if default role changed to view for one folder", func() {
|
||||
updateTestDashboardWithAcl(folder1.Id, editorUser.Id, m.PERMISSION_VIEW)
|
||||
|
||||
err := GetFoldersForSignedInUser(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Id, ShouldEqual, folder2.Id)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
Convey("Viewer users", func() {
|
||||
query := m.GetFoldersForSignedInUserQuery{
|
||||
OrgId: 1,
|
||||
SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgRole: m.ROLE_VIEWER},
|
||||
}
|
||||
|
||||
Convey("Should have no write access to any dashboard folders with default ACL", func() {
|
||||
err := GetFoldersForSignedInUser(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("should have view access to folders with default ACL", func() {
|
||||
query := m.GetDashboardPermissionsForUserQuery{
|
||||
DashboardIds: []int64{folder1.Id, folder2.Id},
|
||||
OrgId: 1,
|
||||
UserId: viewerUser.Id,
|
||||
OrgRole: m.ROLE_VIEWER,
|
||||
}
|
||||
|
||||
err := GetDashboardPermissionsForUser(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].DashboardId, ShouldEqual, folder1.Id)
|
||||
So(query.Result[0].Permission, ShouldEqual, m.PERMISSION_VIEW)
|
||||
So(query.Result[1].DashboardId, ShouldEqual, folder2.Id)
|
||||
So(query.Result[1].Permission, ShouldEqual, m.PERMISSION_VIEW)
|
||||
})
|
||||
|
||||
Convey("Should be able to get one dashboard folder if default role changed to edit for one folder", func() {
|
||||
updateTestDashboardWithAcl(folder1.Id, viewerUser.Id, m.PERMISSION_EDIT)
|
||||
|
||||
err := GetFoldersForSignedInUser(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Id, ShouldEqual, folder1.Id)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
@ -1,15 +1,16 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDashboardDataAccess(t *testing.T) {
|
||||
@ -30,15 +31,33 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
So(savedDash.Id, ShouldNotEqual, 0)
|
||||
So(savedDash.IsFolder, ShouldBeFalse)
|
||||
So(savedDash.FolderId, ShouldBeGreaterThan, 0)
|
||||
So(len(savedDash.Uid), ShouldBeGreaterThan, 0)
|
||||
|
||||
So(savedFolder.Title, ShouldEqual, "1 test dash folder")
|
||||
So(savedFolder.Slug, ShouldEqual, "1-test-dash-folder")
|
||||
So(savedFolder.Id, ShouldNotEqual, 0)
|
||||
So(savedFolder.IsFolder, ShouldBeTrue)
|
||||
So(savedFolder.FolderId, ShouldEqual, 0)
|
||||
So(len(savedFolder.Uid), ShouldBeGreaterThan, 0)
|
||||
})
|
||||
|
||||
Convey("Should be able to get dashboard", func() {
|
||||
Convey("Should be able to get dashboard by id", func() {
|
||||
query := m.GetDashboardQuery{
|
||||
Id: savedDash.Id,
|
||||
OrgId: 1,
|
||||
}
|
||||
|
||||
err := GetDashboard(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(query.Result.Title, ShouldEqual, "test dash 23")
|
||||
So(query.Result.Slug, ShouldEqual, "test-dash-23")
|
||||
So(query.Result.Id, ShouldEqual, savedDash.Id)
|
||||
So(query.Result.Uid, ShouldEqual, savedDash.Uid)
|
||||
So(query.Result.IsFolder, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("Should be able to get dashboard by slug", func() {
|
||||
query := m.GetDashboardQuery{
|
||||
Slug: "test-dash-23",
|
||||
OrgId: 1,
|
||||
@ -49,6 +68,24 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
|
||||
So(query.Result.Title, ShouldEqual, "test dash 23")
|
||||
So(query.Result.Slug, ShouldEqual, "test-dash-23")
|
||||
So(query.Result.Id, ShouldEqual, savedDash.Id)
|
||||
So(query.Result.Uid, ShouldEqual, savedDash.Uid)
|
||||
So(query.Result.IsFolder, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("Should be able to get dashboard by uid", func() {
|
||||
query := m.GetDashboardQuery{
|
||||
Uid: savedDash.Uid,
|
||||
OrgId: 1,
|
||||
}
|
||||
|
||||
err := GetDashboard(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(query.Result.Title, ShouldEqual, "test dash 23")
|
||||
So(query.Result.Slug, ShouldEqual, "test-dash-23")
|
||||
So(query.Result.Id, ShouldEqual, savedDash.Id)
|
||||
So(query.Result.Uid, ShouldEqual, savedDash.Uid)
|
||||
So(query.Result.IsFolder, ShouldBeFalse)
|
||||
})
|
||||
|
||||
@ -109,6 +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("/dashboards/f/%s/%s", savedFolder.Uid, savedFolder.Slug))
|
||||
})
|
||||
|
||||
Convey("Should be able to search for a dashboard folder's children", func() {
|
||||
@ -124,6 +162,7 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
hit := query.Result[0]
|
||||
So(hit.Id, ShouldEqual, savedDash.Id)
|
||||
So(hit.Url, ShouldEqual, fmt.Sprintf("/d/%s/%s", savedDash.Uid, savedDash.Slug))
|
||||
})
|
||||
|
||||
Convey("Should be able to search for dashboard by dashboard ids", func() {
|
||||
@ -157,18 +196,170 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Should not be able to save dashboard with same name", func() {
|
||||
Convey("Should be able to save dashboards with same name in different folders", func() {
|
||||
firstSaveCmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dash folder and title",
|
||||
"tags": []interface{}{},
|
||||
"uid": "randomHash",
|
||||
}),
|
||||
FolderId: 3,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&firstSaveCmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
secondSaveCmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dash folder and title",
|
||||
"tags": []interface{}{},
|
||||
"uid": "moreRandomHash",
|
||||
}),
|
||||
FolderId: 1,
|
||||
}
|
||||
|
||||
err = SaveDashboard(&secondSaveCmd)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Should not be able to save dashboard with same name in the same folder", func() {
|
||||
firstSaveCmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dash folder and title",
|
||||
"tags": []interface{}{},
|
||||
"uid": "randomHash",
|
||||
}),
|
||||
FolderId: 3,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&firstSaveCmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
secondSaveCmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dash folder and title",
|
||||
"tags": []interface{}{},
|
||||
"uid": "moreRandomHash",
|
||||
}),
|
||||
FolderId: 3,
|
||||
}
|
||||
|
||||
err = SaveDashboard(&secondSaveCmd)
|
||||
So(err, ShouldEqual, m.ErrDashboardWithSameNameInFolderExists)
|
||||
})
|
||||
|
||||
Convey("Should not be able to save dashboard with same uid", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dash 23",
|
||||
"uid": "dsfalkjngailuedt",
|
||||
}),
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
err = SaveDashboard(&cmd)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Should be able to update dashboard with the same title and folder id", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"uid": "randomHash",
|
||||
"title": "folderId",
|
||||
"style": "light",
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
FolderId: 2,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
So(cmd.Result.FolderId, ShouldEqual, 2)
|
||||
|
||||
cmd = m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": cmd.Result.Id,
|
||||
"uid": "randomHash",
|
||||
"title": "folderId",
|
||||
"style": "dark",
|
||||
"version": cmd.Result.Version,
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
FolderId: 2,
|
||||
}
|
||||
|
||||
err = SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Should not be able to update using just uid", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"uid": savedDash.Uid,
|
||||
"title": "folderId",
|
||||
"version": savedDash.Version,
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
FolderId: savedDash.FolderId,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldEqual, m.ErrDashboardWithSameUIDExists)
|
||||
})
|
||||
|
||||
Convey("Should be able to update using just uid with overwrite", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"uid": savedDash.Uid,
|
||||
"title": "folderId",
|
||||
"version": savedDash.Version,
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
FolderId: savedDash.FolderId,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Should retry generation of uid once if it fails.", func() {
|
||||
timesCalled := 0
|
||||
generateNewUid = func() string {
|
||||
timesCalled += 1
|
||||
if timesCalled <= 2 {
|
||||
return savedDash.Uid
|
||||
} else {
|
||||
return util.GenerateShortUid()
|
||||
}
|
||||
}
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"title": "new dash 12334",
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
generateNewUid = util.GenerateShortUid
|
||||
})
|
||||
|
||||
Convey("Should be able to update dashboard and remove folderId", func() {
|
||||
@ -260,336 +451,6 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given one dashboard folder with two dashboards and one dashboard in the root folder", func() {
|
||||
folder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp")
|
||||
dashInRoot := insertTestDashboard("test dash 67", 1, 0, false, "prod", "webapp")
|
||||
childDash := insertTestDashboard("test dash 23", 1, folder.Id, false, "prod", "webapp")
|
||||
insertTestDashboard("test dash 45", 1, folder.Id, false, "prod")
|
||||
|
||||
currentUser := createUser("viewer", "Viewer", false)
|
||||
|
||||
Convey("and no acls are set", func() {
|
||||
Convey("should return all dashboards", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].Id, ShouldEqual, folder.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("and acl is set for dashboard folder", func() {
|
||||
var otherUser int64 = 999
|
||||
updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT)
|
||||
|
||||
Convey("should not return folder", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
|
||||
Convey("when the user is given permission", func() {
|
||||
updateTestDashboardWithAcl(folder.Id, currentUser.Id, m.PERMISSION_EDIT)
|
||||
|
||||
Convey("should be able to access folder", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].Id, ShouldEqual, folder.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("when the user is an admin", func() {
|
||||
Convey("should be able to access folder", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{
|
||||
SignedInUser: &m.SignedInUser{
|
||||
UserId: currentUser.Id,
|
||||
OrgId: 1,
|
||||
OrgRole: m.ROLE_ADMIN,
|
||||
},
|
||||
OrgId: 1,
|
||||
DashboardIds: []int64{folder.Id, dashInRoot.Id},
|
||||
}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].Id, ShouldEqual, folder.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("and acl is set for dashboard child and folder has all permissions removed", func() {
|
||||
var otherUser int64 = 999
|
||||
aclId := updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT)
|
||||
removeAcl(aclId)
|
||||
updateTestDashboardWithAcl(childDash.Id, otherUser, m.PERMISSION_EDIT)
|
||||
|
||||
Convey("should not return folder or child", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
|
||||
Convey("when the user is given permission to child", func() {
|
||||
updateTestDashboardWithAcl(childDash.Id, currentUser.Id, m.PERMISSION_EDIT)
|
||||
|
||||
Convey("should be able to search for child dashboard but not folder", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].Id, ShouldEqual, childDash.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("when the user is an admin", func() {
|
||||
Convey("should be able to search for child dash and folder", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{
|
||||
SignedInUser: &m.SignedInUser{
|
||||
UserId: currentUser.Id,
|
||||
OrgId: 1,
|
||||
OrgRole: m.ROLE_ADMIN,
|
||||
},
|
||||
OrgId: 1,
|
||||
DashboardIds: []int64{folder.Id, dashInRoot.Id, childDash.Id},
|
||||
}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 3)
|
||||
So(query.Result[0].Id, ShouldEqual, folder.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, childDash.Id)
|
||||
So(query.Result[2].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given two dashboard folders with one dashboard each and one dashboard in the root folder", func() {
|
||||
folder1 := insertTestDashboard("1 test dash folder", 1, 0, true, "prod")
|
||||
folder2 := insertTestDashboard("2 test dash folder", 1, 0, true, "prod")
|
||||
dashInRoot := insertTestDashboard("test dash 67", 1, 0, false, "prod")
|
||||
childDash1 := insertTestDashboard("child dash 1", 1, folder1.Id, false, "prod")
|
||||
childDash2 := insertTestDashboard("child dash 2", 1, folder2.Id, false, "prod")
|
||||
|
||||
currentUser := createUser("viewer", "Viewer", false)
|
||||
var rootFolderId int64 = 0
|
||||
|
||||
Convey("and one folder is expanded, the other collapsed", func() {
|
||||
Convey("should return dashboards in root and expanded folder", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{FolderIds: []int64{rootFolderId, folder1.Id}, SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 4)
|
||||
So(query.Result[0].Id, ShouldEqual, folder1.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, folder2.Id)
|
||||
So(query.Result[2].Id, ShouldEqual, childDash1.Id)
|
||||
So(query.Result[3].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("and acl is set for one dashboard folder", func() {
|
||||
var otherUser int64 = 999
|
||||
updateTestDashboardWithAcl(folder1.Id, otherUser, m.PERMISSION_EDIT)
|
||||
|
||||
Convey("and a dashboard is moved from folder without acl to the folder with an acl", func() {
|
||||
movedDash := moveDashboard(1, childDash2.Data, folder1.Id)
|
||||
So(movedDash.HasAcl, ShouldBeTrue)
|
||||
|
||||
Convey("should not return folder with acl or its children", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{
|
||||
SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1},
|
||||
OrgId: 1,
|
||||
DashboardIds: []int64{folder1.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
|
||||
}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("and a dashboard is moved from folder with acl to the folder without an acl", func() {
|
||||
movedDash := moveDashboard(1, childDash1.Data, folder2.Id)
|
||||
So(movedDash.HasAcl, ShouldBeFalse)
|
||||
|
||||
Convey("should return folder without acl and its children", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{
|
||||
SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1},
|
||||
OrgId: 1,
|
||||
DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
|
||||
}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 4)
|
||||
So(query.Result[0].Id, ShouldEqual, folder2.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, childDash1.Id)
|
||||
So(query.Result[2].Id, ShouldEqual, childDash2.Id)
|
||||
So(query.Result[3].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("and a dashboard with an acl is moved to the folder without an acl", func() {
|
||||
updateTestDashboardWithAcl(childDash1.Id, otherUser, m.PERMISSION_EDIT)
|
||||
movedDash := moveDashboard(1, childDash1.Data, folder2.Id)
|
||||
So(movedDash.HasAcl, ShouldBeTrue)
|
||||
|
||||
Convey("should return folder without acl but not the dashboard with acl", func() {
|
||||
query := &search.FindPersistedDashboardsQuery{
|
||||
SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1},
|
||||
OrgId: 1,
|
||||
DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id},
|
||||
}
|
||||
err := SearchDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 3)
|
||||
So(query.Result[0].Id, ShouldEqual, folder2.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, childDash2.Id)
|
||||
So(query.Result[2].Id, ShouldEqual, dashInRoot.Id)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given two dashboard folders", func() {
|
||||
|
||||
folder1 := insertTestDashboard("1 test dash folder", 1, 0, true, "prod")
|
||||
folder2 := insertTestDashboard("2 test dash folder", 1, 0, true, "prod")
|
||||
|
||||
adminUser := createUser("admin", "Admin", true)
|
||||
editorUser := createUser("editor", "Editor", false)
|
||||
viewerUser := createUser("viewer", "Viewer", false)
|
||||
|
||||
Convey("Admin users", func() {
|
||||
Convey("Should have write access to all dashboard folders", func() {
|
||||
query := m.GetFoldersForSignedInUserQuery{
|
||||
OrgId: 1,
|
||||
SignedInUser: &m.SignedInUser{UserId: adminUser.Id, OrgRole: m.ROLE_ADMIN},
|
||||
}
|
||||
|
||||
err := GetFoldersForSignedInUser(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].Id, ShouldEqual, folder1.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, folder2.Id)
|
||||
})
|
||||
|
||||
Convey("should have write access to all folders and dashboards", func() {
|
||||
query := m.GetDashboardPermissionsForUserQuery{
|
||||
DashboardIds: []int64{folder1.Id, folder2.Id},
|
||||
OrgId: 1,
|
||||
UserId: adminUser.Id,
|
||||
OrgRole: m.ROLE_ADMIN,
|
||||
}
|
||||
|
||||
err := GetDashboardPermissionsForUser(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].DashboardId, ShouldEqual, folder1.Id)
|
||||
So(query.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN)
|
||||
So(query.Result[1].DashboardId, ShouldEqual, folder2.Id)
|
||||
So(query.Result[1].Permission, ShouldEqual, m.PERMISSION_ADMIN)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Editor users", func() {
|
||||
query := m.GetFoldersForSignedInUserQuery{
|
||||
OrgId: 1,
|
||||
SignedInUser: &m.SignedInUser{UserId: editorUser.Id, OrgRole: m.ROLE_EDITOR},
|
||||
}
|
||||
|
||||
Convey("Should have write access to all dashboard folders with default ACL", func() {
|
||||
err := GetFoldersForSignedInUser(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].Id, ShouldEqual, folder1.Id)
|
||||
So(query.Result[1].Id, ShouldEqual, folder2.Id)
|
||||
})
|
||||
|
||||
Convey("should have edit access to folders with default ACL", func() {
|
||||
query := m.GetDashboardPermissionsForUserQuery{
|
||||
DashboardIds: []int64{folder1.Id, folder2.Id},
|
||||
OrgId: 1,
|
||||
UserId: editorUser.Id,
|
||||
OrgRole: m.ROLE_EDITOR,
|
||||
}
|
||||
|
||||
err := GetDashboardPermissionsForUser(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].DashboardId, ShouldEqual, folder1.Id)
|
||||
So(query.Result[0].Permission, ShouldEqual, m.PERMISSION_EDIT)
|
||||
So(query.Result[1].DashboardId, ShouldEqual, folder2.Id)
|
||||
So(query.Result[1].Permission, ShouldEqual, m.PERMISSION_EDIT)
|
||||
})
|
||||
|
||||
Convey("Should have write access to one dashboard folder if default role changed to view for one folder", func() {
|
||||
updateTestDashboardWithAcl(folder1.Id, editorUser.Id, m.PERMISSION_VIEW)
|
||||
|
||||
err := GetFoldersForSignedInUser(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Id, ShouldEqual, folder2.Id)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
Convey("Viewer users", func() {
|
||||
query := m.GetFoldersForSignedInUserQuery{
|
||||
OrgId: 1,
|
||||
SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgRole: m.ROLE_VIEWER},
|
||||
}
|
||||
|
||||
Convey("Should have no write access to any dashboard folders with default ACL", func() {
|
||||
err := GetFoldersForSignedInUser(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("should have view access to folders with default ACL", func() {
|
||||
query := m.GetDashboardPermissionsForUserQuery{
|
||||
DashboardIds: []int64{folder1.Id, folder2.Id},
|
||||
OrgId: 1,
|
||||
UserId: viewerUser.Id,
|
||||
OrgRole: m.ROLE_VIEWER,
|
||||
}
|
||||
|
||||
err := GetDashboardPermissionsForUser(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].DashboardId, ShouldEqual, folder1.Id)
|
||||
So(query.Result[0].Permission, ShouldEqual, m.PERMISSION_VIEW)
|
||||
So(query.Result[1].DashboardId, ShouldEqual, folder2.Id)
|
||||
So(query.Result[1].Permission, ShouldEqual, m.PERMISSION_VIEW)
|
||||
})
|
||||
|
||||
Convey("Should be able to get one dashboard folder if default role changed to edit for one folder", func() {
|
||||
updateTestDashboardWithAcl(folder1.Id, viewerUser.Id, m.PERMISSION_EDIT)
|
||||
|
||||
err := GetFoldersForSignedInUser(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Id, ShouldEqual, folder1.Id)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a plugin with imported dashboards", func() {
|
||||
pluginId := "test-app"
|
||||
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
func updateTestDashboard(dashboard *m.Dashboard, data map[string]interface{}) {
|
||||
data["title"] = dashboard.Title
|
||||
data["uid"] = dashboard.Uid
|
||||
|
||||
saveCmd := m.SaveDashboardCommand{
|
||||
OrgId: dashboard.OrgId,
|
||||
@ -44,7 +44,7 @@ func TestGetDashboardVersion(t *testing.T) {
|
||||
|
||||
dashCmd := m.GetDashboardQuery{
|
||||
OrgId: savedDash.OrgId,
|
||||
Slug: savedDash.Slug,
|
||||
Uid: savedDash.Uid,
|
||||
}
|
||||
|
||||
err = GetDashboard(&dashCmd)
|
||||
|
@ -150,4 +150,21 @@ func addDashboardMigration(mg *Migrator) {
|
||||
mg.AddMigration("Add column has_acl in dashboard", NewAddColumnMigration(dashboardV2, &Column{
|
||||
Name: "has_acl", Type: DB_Bool, Nullable: false, Default: "0",
|
||||
}))
|
||||
|
||||
mg.AddMigration("Add column uid in dashboard", NewAddColumnMigration(dashboardV2, &Column{
|
||||
Name: "uid", Type: DB_NVarchar, Length: 40, Nullable: true,
|
||||
}))
|
||||
|
||||
mg.AddMigration("Update uid column values in dashboard", new(RawSqlMigration).
|
||||
Sqlite("UPDATE dashboard SET uid=printf('%09d',id) WHERE uid IS NULL;").
|
||||
Postgres("UPDATE dashboard SET uid=lpad('' || id,9,'0') WHERE uid IS NULL;").
|
||||
Mysql("UPDATE dashboard SET uid=lpad(id,9,'0') WHERE uid IS NULL;"))
|
||||
|
||||
mg.AddMigration("Add unique index dashboard_org_id_uid", NewAddIndexMigration(dashboardV2, &Index{
|
||||
Cols: []string{"org_id", "uid"}, Type: UniqueIndex,
|
||||
}))
|
||||
|
||||
mg.AddMigration("Remove unique index org_id_slug", NewDropIndexMigration(dashboardV2, &Index{
|
||||
Cols: []string{"org_id", "slug"}, Type: UniqueIndex,
|
||||
}))
|
||||
}
|
||||
|
@ -101,6 +101,7 @@ func (sb *SearchBuilder) buildSelect() {
|
||||
sb.sql.WriteString(
|
||||
`SELECT
|
||||
dashboard.id,
|
||||
dashboard.uid,
|
||||
dashboard.title,
|
||||
dashboard.slug,
|
||||
dashboard_tag.term,
|
||||
|
15
pkg/util/shortid_generator.go
Normal file
15
pkg/util/shortid_generator.go
Normal file
@ -0,0 +1,15 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"github.com/teris-io/shortid"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gen, _ := shortid.New(1, shortid.DefaultABC, 1)
|
||||
shortid.SetDefault(gen)
|
||||
}
|
||||
|
||||
// GenerateShortUid generates a short unique identifier.
|
||||
func GenerateShortUid() string {
|
||||
return shortid.MustGenerate()
|
||||
}
|
@ -23,7 +23,7 @@ describe('AlertRuleList', () => {
|
||||
.format(),
|
||||
evalData: {},
|
||||
executionError: '',
|
||||
dashboardUri: 'db/mygool',
|
||||
dashboardUri: 'd/ufkcofof/my-goal',
|
||||
canEdit: true,
|
||||
},
|
||||
])
|
||||
|
@ -137,7 +137,7 @@ export class AlertRuleItem extends React.Component<AlertRuleItemProps, any> {
|
||||
'fa-pause': !rule.isPaused,
|
||||
});
|
||||
|
||||
let ruleUrl = `dashboard/${rule.dashboardUri}?panelId=${rule.panelId}&fullscreen&edit&tab=alert`;
|
||||
let ruleUrl = `${rule.dashboardUri}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`;
|
||||
|
||||
return (
|
||||
<li className="alert-rule-item">
|
||||
|
@ -21,7 +21,7 @@ exports[`AlertRuleList should render 1 rule 1`] = `
|
||||
className="alert-rule-item__name"
|
||||
>
|
||||
<a
|
||||
href="dashboard/db/mygool?panelId=3&fullscreen&edit&tab=alert"
|
||||
href="d/ufkcofof/my-goal?panelId=3&fullscreen=true&edit=true&tab=alert"
|
||||
>
|
||||
<Highlighter
|
||||
highlightClassName="highlight-search-match"
|
||||
@ -92,7 +92,7 @@ exports[`AlertRuleList should render 1 rule 1`] = `
|
||||
</button>
|
||||
<a
|
||||
className="btn btn-small btn-inverse alert-list__btn width-2"
|
||||
href="dashboard/db/mygool?panelId=3&fullscreen&edit&tab=alert"
|
||||
href="d/ufkcofof/my-goal?panelId=3&fullscreen=true&edit=true&tab=alert"
|
||||
title="Edit alert rule"
|
||||
>
|
||||
<i
|
||||
|
@ -19,7 +19,8 @@ export class FolderPermissions extends Component<IContainerProps, any> {
|
||||
|
||||
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 => {
|
||||
view.updatePathAndQuery(`${res.meta.url}/permissions`, {}, {});
|
||||
return nav.initFolderNav(toJS(folder.folder), 'manage-folder-permissions');
|
||||
});
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
@ -20,10 +20,12 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
|
||||
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<IContainerProps, any> {
|
||||
folder
|
||||
.saveFolder(this.dashboard, { overwrite: false })
|
||||
.then(newUrl => {
|
||||
view.updatePathAndQuery(newUrl, '', '');
|
||||
view.updatePathAndQuery(newUrl, {}, {});
|
||||
|
||||
appEvents.emit('dashboard-saved');
|
||||
appEvents.emit('alert-success', ['Folder saved']);
|
||||
|
@ -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: '=',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -225,6 +225,10 @@ export class BackendSrv {
|
||||
return this.get('/api/dashboards/' + type + '/' + slug);
|
||||
}
|
||||
|
||||
getDashboardByUid(uid: string) {
|
||||
return this.get(`/api/dashboards/uid/${uid}`);
|
||||
}
|
||||
|
||||
saveDashboard(dash, options) {
|
||||
options = options || {};
|
||||
|
||||
@ -253,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);
|
||||
})
|
||||
@ -269,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 => {
|
||||
@ -295,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) {
|
||||
|
@ -1,30 +1,18 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import config from 'app/core/config';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { store } from 'app/stores/store';
|
||||
import { reaction } from 'mobx';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
|
||||
// Services that handles angular -> mobx store sync & other react <-> angular sync
|
||||
export class BridgeSrv {
|
||||
private appSubUrl;
|
||||
private fullPageReloadRoutes;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $location, private $timeout, private $window, private $rootScope, private $route) {
|
||||
this.appSubUrl = config.appSubUrl;
|
||||
this.fullPageReloadRoutes = ['/logout'];
|
||||
}
|
||||
|
||||
// Angular's $location does not like <base href...> and absolute urls
|
||||
stripBaseFromUrl(url = '') {
|
||||
const appSubUrl = this.appSubUrl;
|
||||
const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0;
|
||||
const urlWithoutBase =
|
||||
url.length > 0 && url.indexOf(appSubUrl) === 0 ? url.slice(appSubUrl.length - stripExtraChars) : url;
|
||||
|
||||
return urlWithoutBase;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.$rootScope.$on('$routeUpdate', (evt, data) => {
|
||||
let angularUrl = this.$location.url();
|
||||
@ -34,25 +22,25 @@ 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(
|
||||
() => store.view.currentUrl,
|
||||
currentUrl => {
|
||||
let angularUrl = this.$location.url();
|
||||
if (angularUrl !== currentUrl) {
|
||||
this.$location.url(currentUrl);
|
||||
console.log('store updating angular $location.url', currentUrl);
|
||||
const url = locationUtil.stripBaseFromUrl(currentUrl);
|
||||
if (angularUrl !== url) {
|
||||
this.$timeout(() => {
|
||||
this.$location.url(url);
|
||||
});
|
||||
console.log('store updating angular $location.url', url);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
appEvents.on('location-change', payload => {
|
||||
const urlWithoutBase = this.stripBaseFromUrl(payload.href);
|
||||
const urlWithoutBase = locationUtil.stripBaseFromUrl(payload.href);
|
||||
if (this.fullPageReloadRoutes.indexOf(urlWithoutBase) > -1) {
|
||||
this.$window.location.href = payload.href;
|
||||
return;
|
||||
|
@ -41,10 +41,7 @@ export class SearchSrv {
|
||||
.map(orderId => {
|
||||
return _.find(result, { id: orderId });
|
||||
})
|
||||
.filter(hit => hit && !hit.isStarred)
|
||||
.map(hit => {
|
||||
return this.transformToViewModel(hit);
|
||||
});
|
||||
.filter(hit => hit && !hit.isStarred);
|
||||
});
|
||||
}
|
||||
|
||||
@ -81,17 +78,12 @@ export class SearchSrv {
|
||||
score: -2,
|
||||
expanded: this.starredIsOpen,
|
||||
toggle: this.toggleStarred.bind(this),
|
||||
items: result.map(this.transformToViewModel),
|
||||
items: result,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private transformToViewModel(hit) {
|
||||
hit.url = 'dashboard/db/' + hit.slug;
|
||||
return hit;
|
||||
}
|
||||
|
||||
search(options) {
|
||||
let sections: any = {};
|
||||
let promises = [];
|
||||
@ -136,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,
|
||||
};
|
||||
@ -158,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),
|
||||
@ -181,7 +173,7 @@ export class SearchSrv {
|
||||
}
|
||||
|
||||
section.expanded = true;
|
||||
section.items.push(this.transformToViewModel(hit));
|
||||
section.items.push(hit);
|
||||
}
|
||||
}
|
||||
|
||||
@ -198,7 +190,7 @@ export class SearchSrv {
|
||||
};
|
||||
|
||||
return this.backendSrv.search(query).then(results => {
|
||||
section.items = _.map(results, this.transformToViewModel);
|
||||
section.items = results;
|
||||
return Promise.resolve(section);
|
||||
});
|
||||
}
|
||||
|
@ -1,22 +0,0 @@
|
||||
import { BridgeSrv } from 'app/core/services/bridge_srv';
|
||||
|
||||
jest.mock('app/core/config', () => {
|
||||
return {
|
||||
appSubUrl: '/subUrl',
|
||||
};
|
||||
});
|
||||
|
||||
describe('BridgeSrv', () => {
|
||||
let searchSrv;
|
||||
|
||||
beforeEach(() => {
|
||||
searchSrv = new BridgeSrv(null, null, null, null, null);
|
||||
});
|
||||
|
||||
describe('With /subUrl as appSubUrl', () => {
|
||||
it('/subUrl should be stripped', () => {
|
||||
const urlWithoutMaster = searchSrv.stripBaseFromUrl('/subUrl/grafana/');
|
||||
expect(urlWithoutMaster).toBe('/grafana/');
|
||||
});
|
||||
});
|
||||
});
|
16
public/app/core/specs/location_util.jest.ts
Normal file
16
public/app/core/specs/location_util.jest.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
|
||||
jest.mock('app/core/config', () => {
|
||||
return {
|
||||
appSubUrl: '/subUrl',
|
||||
};
|
||||
});
|
||||
|
||||
describe('locationUtil', () => {
|
||||
describe('With /subUrl as appSubUrl', () => {
|
||||
it('/subUrl should be stripped', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/grafana/');
|
||||
expect(urlWithoutMaster).toBe('/grafana/');
|
||||
});
|
||||
});
|
||||
});
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
14
public/app/core/utils/location_util.ts
Normal file
14
public/app/core/utils/location_util.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import config from 'app/core/config';
|
||||
|
||||
const _stripBaseFromUrl = url => {
|
||||
const appSubUrl = config.appSubUrl;
|
||||
const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0;
|
||||
const urlWithoutBase =
|
||||
url.length > 0 && url.indexOf(appSubUrl) === 0 ? url.slice(appSubUrl.length - stripExtraChars) : url;
|
||||
|
||||
return urlWithoutBase;
|
||||
};
|
||||
|
||||
export default {
|
||||
stripBaseFromUrl: _stripBaseFromUrl,
|
||||
};
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -35,18 +35,18 @@ export class DashboardLoaderSrv {
|
||||
};
|
||||
}
|
||||
|
||||
loadDashboard(type, slug) {
|
||||
loadDashboard(type, slug, uid) {
|
||||
var promise;
|
||||
|
||||
if (type === 'script') {
|
||||
promise = this._loadScriptedDashboard(slug);
|
||||
} else if (type === 'snapshot') {
|
||||
promise = this.backendSrv.get('/api/snapshots/' + this.$routeParams.slug).catch(() => {
|
||||
promise = this.backendSrv.get('/api/snapshots/' + slug).catch(() => {
|
||||
return this._dashboardLoadFailed('Snapshot not found', true);
|
||||
});
|
||||
} else {
|
||||
promise = this.backendSrv
|
||||
.getDashboard(this.$routeParams.type, this.$routeParams.slug)
|
||||
.getDashboardByUid(uid)
|
||||
.then(result => {
|
||||
if (result.meta.isFolder) {
|
||||
this.$rootScope.appEvent('alert-error', ['Dashboard not found']);
|
||||
|
@ -12,6 +12,7 @@ import { DashboardMigrator } from './dashboard_migration';
|
||||
|
||||
export class DashboardModel {
|
||||
id: any;
|
||||
uid: any;
|
||||
title: any;
|
||||
autoUpdate: any;
|
||||
description: any;
|
||||
@ -56,6 +57,7 @@ export class DashboardModel {
|
||||
|
||||
this.events = new Emitter();
|
||||
this.id = data.id || null;
|
||||
this.uid = data.uid || null;
|
||||
this.revision = data.revision;
|
||||
this.title = data.title || 'No Title';
|
||||
this.autoUpdate = data.autoUpdate;
|
||||
|
@ -73,9 +73,8 @@ export class DashboardSrv {
|
||||
postSave(clone, data) {
|
||||
this.dash.version = data.version;
|
||||
|
||||
var dashboardUrl = '/dashboard/db/' + data.slug;
|
||||
if (dashboardUrl !== this.$location.path()) {
|
||||
this.$location.url(dashboardUrl);
|
||||
if (data.url !== this.$location.path()) {
|
||||
this.$location.url(data.url);
|
||||
}
|
||||
|
||||
this.$rootScope.appEvent('dashboard-saved', this.dash);
|
||||
|
@ -1,19 +1,24 @@
|
||||
import { FolderPageLoader } from './folder_page_loader';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
|
||||
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 => {
|
||||
const url = locationUtil.stripBaseFromUrl(folder.meta.url);
|
||||
|
||||
if (url !== $location.path()) {
|
||||
$location.path(url).replace();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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}`;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<page-header ng-if="ctrl.navModel" model="ctrl.navModel"></page-header>
|
||||
|
||||
<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>
|
||||
|
@ -186,7 +186,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('/');
|
||||
});
|
||||
|
@ -74,6 +74,7 @@ export class ShareModalCtrl {
|
||||
$scope.shareUrl = linkSrv.addParamsToUrl(baseUrl, params);
|
||||
|
||||
var soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/');
|
||||
soloUrl = soloUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/');
|
||||
delete params.fullscreen;
|
||||
delete params.edit;
|
||||
soloUrl = linkSrv.addParamsToUrl(soloUrl, params);
|
||||
@ -84,6 +85,7 @@ export class ShareModalCtrl {
|
||||
config.appSubUrl + '/dashboard-solo/',
|
||||
config.appSubUrl + '/render/dashboard-solo/'
|
||||
);
|
||||
$scope.imageUrl = $scope.imageUrl.replace(config.appSubUrl + '/d-solo/', config.appSubUrl + '/render/d-solo/');
|
||||
$scope.imageUrl += '&width=1000';
|
||||
$scope.imageUrl += '&height=500';
|
||||
$scope.imageUrl += '&tz=UTC' + encodeURIComponent(moment().format('Z'));
|
||||
|
@ -43,12 +43,23 @@ describe('ShareModalCtrl', function() {
|
||||
});
|
||||
|
||||
it('should generate render url', function() {
|
||||
ctx.$location.$$absUrl = 'http://dashboards.grafana.com/dashboard/db/my-dash';
|
||||
ctx.$location.$$absUrl = 'http://dashboards.grafana.com/d/abcdefghi/my-dash';
|
||||
|
||||
ctx.scope.panel = { id: 22 };
|
||||
|
||||
ctx.scope.init();
|
||||
var base = 'http://dashboards.grafana.com/render/dashboard-solo/db/my-dash';
|
||||
var base = 'http://dashboards.grafana.com/render/d-solo/abcdefghi/my-dash';
|
||||
var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC';
|
||||
expect(ctx.scope.imageUrl).to.contain(base + params);
|
||||
});
|
||||
|
||||
it('should generate render url for scripted dashboard', function() {
|
||||
ctx.$location.$$absUrl = 'http://dashboards.grafana.com/dashboard/script/my-dash.js';
|
||||
|
||||
ctx.scope.panel = { id: 22 };
|
||||
|
||||
ctx.scope.init();
|
||||
var base = 'http://dashboards.grafana.com/render/dashboard-solo/script/my-dash.js';
|
||||
var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC';
|
||||
expect(ctx.scope.imageUrl).to.contain(base + params);
|
||||
});
|
||||
|
@ -1,9 +1,10 @@
|
||||
import angular from 'angular';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
export class SoloPanelCtrl {
|
||||
/** @ngInject */
|
||||
constructor($scope, $routeParams, $location, dashboardLoaderSrv, contextSrv) {
|
||||
constructor($scope, $routeParams, $location, dashboardLoaderSrv, contextSrv, backendSrv) {
|
||||
var panelId;
|
||||
|
||||
$scope.init = function() {
|
||||
@ -15,7 +16,18 @@ export class SoloPanelCtrl {
|
||||
|
||||
$scope.onAppEvent('dashboard-initialized', $scope.initPanelScope);
|
||||
|
||||
dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug).then(function(result) {
|
||||
// if no uid, redirect to new route based on slug
|
||||
if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) {
|
||||
backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => {
|
||||
if (res) {
|
||||
const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/'));
|
||||
$location.path(url).replace();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(function(result) {
|
||||
result.meta.soloMode = true;
|
||||
$scope.initDashboard(result, $scope);
|
||||
});
|
||||
|
@ -4,7 +4,7 @@
|
||||
{{group.header}}
|
||||
</h6>
|
||||
<div class="dashlist-item" ng-repeat="dash in group.list">
|
||||
<a class="dashlist-link dashlist-link-{{dash.type}}" href="dashboard/{{dash.uri}}">
|
||||
<a class="dashlist-link dashlist-link-{{dash.type}}" href="{{dash.url}}">
|
||||
<span class="dashlist-title">
|
||||
{{dash.title}}
|
||||
</span>
|
||||
|
@ -1,11 +1,12 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
|
||||
export class LoadDashboardCtrl {
|
||||
/** @ngInject */
|
||||
constructor($scope, $routeParams, dashboardLoaderSrv, backendSrv, $location) {
|
||||
constructor($scope, $routeParams, dashboardLoaderSrv, backendSrv, $location, $browser) {
|
||||
$scope.appEvent('dashboard-fetch-start');
|
||||
|
||||
if (!$routeParams.slug) {
|
||||
if (!$routeParams.uid && !$routeParams.slug) {
|
||||
backendSrv.get('/api/dashboards/home').then(function(homeDash) {
|
||||
if (homeDash.redirectUri) {
|
||||
$location.path('dashboard/' + homeDash.redirectUri);
|
||||
@ -18,7 +19,24 @@ export class LoadDashboardCtrl {
|
||||
return;
|
||||
}
|
||||
|
||||
dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug).then(function(result) {
|
||||
// if no uid, redirect to new route based on slug
|
||||
if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) {
|
||||
backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => {
|
||||
if (res) {
|
||||
const url = locationUtil.stripBaseFromUrl(res.meta.url);
|
||||
$location.path(url).replace();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(function(result) {
|
||||
const url = locationUtil.stripBaseFromUrl(result.meta.url);
|
||||
|
||||
if (url !== $location.path()) {
|
||||
$location.path(url).replace();
|
||||
}
|
||||
|
||||
if ($routeParams.keepRows) {
|
||||
result.meta.keepRows = true;
|
||||
}
|
||||
|
@ -16,12 +16,24 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
reloadOnSearch: false,
|
||||
pageClass: 'page-dashboard',
|
||||
})
|
||||
.when('/d/:uid/:slug', {
|
||||
templateUrl: 'public/app/partials/dashboard.html',
|
||||
controller: 'LoadDashboardCtrl',
|
||||
reloadOnSearch: false,
|
||||
pageClass: 'page-dashboard',
|
||||
})
|
||||
.when('/dashboard/:type/:slug', {
|
||||
templateUrl: 'public/app/partials/dashboard.html',
|
||||
controller: 'LoadDashboardCtrl',
|
||||
reloadOnSearch: false,
|
||||
pageClass: 'page-dashboard',
|
||||
})
|
||||
.when('/d-solo/:uid/:slug', {
|
||||
templateUrl: 'public/app/features/panel/partials/soloPanel.html',
|
||||
controller: 'SoloPanelCtrl',
|
||||
reloadOnSearch: false,
|
||||
pageClass: 'page-dashboard',
|
||||
})
|
||||
.when('/dashboard-solo/:type/:slug', {
|
||||
templateUrl: 'public/app/features/panel/partials/soloPanel.html',
|
||||
controller: 'SoloPanelCtrl',
|
||||
@ -69,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: '<react-container />',
|
||||
resolve: {
|
||||
component: () => FolderPermissions,
|
||||
},
|
||||
})
|
||||
.when('/dashboards/folder/:folderId/:slug/settings', {
|
||||
.when('/dashboards/f/:uid/:slug/settings', {
|
||||
template: '<react-container />',
|
||||
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',
|
||||
|
@ -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);
|
||||
}),
|
||||
}));
|
||||
|
@ -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', () => {
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
export const backendSrv = {
|
||||
get: jest.fn(),
|
||||
getDashboard: jest.fn(),
|
||||
getDashboardByUid: jest.fn(),
|
||||
post: jest.fn(),
|
||||
};
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
||||
}
|
||||
],
|
||||
|
||||
"uid": "1MHHlVjzz",
|
||||
"title": "Nginx Connections",
|
||||
"revision": 25,
|
||||
"schemaVersion": 11,
|
||||
|
@ -16,5 +16,6 @@
|
||||
],
|
||||
"schemaVersion": 11,
|
||||
"title": "Nginx Connections",
|
||||
"uid": "1MHHlVjzz",
|
||||
"version": 0
|
||||
}
|
||||
|
18
vendor/github.com/teris-io/shortid/LICENSE
generated
vendored
Normal file
18
vendor/github.com/teris-io/shortid/LICENSE
generated
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
software and associated documentation files (the "Software"), to deal in the Software
|
||||
without restriction, including without limitation the rights to use, copy, modify,
|
||||
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies
|
||||
or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
362
vendor/github.com/teris-io/shortid/shortid.go
generated
vendored
Normal file
362
vendor/github.com/teris-io/shortid/shortid.go
generated
vendored
Normal file
@ -0,0 +1,362 @@
|
||||
// Copyright (c) 2016-2017. Oleg Sklyar & teris.io. All rights reserved.
|
||||
// See the LICENSE file in the project root for licensing information.
|
||||
|
||||
// Original algorithm:
|
||||
// Copyright (c) 2015 Dylan Greene, contributors: https://github.com/dylang/shortid.
|
||||
// MIT-license as found in the LICENSE file.
|
||||
|
||||
// Seed computation: based on The Central Randomizer 1.3
|
||||
// Copyright (c) 1997 Paul Houle (houle@msc.cornell.edu)
|
||||
|
||||
// Package shortid enables the generation of short, unique, non-sequential and by default URL friendly
|
||||
// Ids. The package is heavily inspired by the node.js https://github.com/dylang/shortid library.
|
||||
//
|
||||
// Id Length
|
||||
//
|
||||
// The standard Id length is 9 symbols when generated at a rate of 1 Id per millisecond,
|
||||
// occasionally it reaches 11 (at the rate of a few thousand Ids per millisecond) and very-very
|
||||
// rarely it can go beyond that during continuous generation at full throttle on high-performant
|
||||
// hardware. A test generating 500k Ids at full throttle on conventional hardware generated the
|
||||
// following Ids at the head and the tail (length > 9 is expected for this test):
|
||||
//
|
||||
// -NDveu-9Q
|
||||
// iNove6iQ9J
|
||||
// NVDve6-9Q
|
||||
// VVDvc6i99J
|
||||
// NVovc6-QQy
|
||||
// VVoveui9QC
|
||||
// ...
|
||||
// tFmGc6iQQs
|
||||
// KpTvcui99k
|
||||
// KFTGcuiQ9p
|
||||
// KFmGeu-Q9O
|
||||
// tFTvcu-QQt
|
||||
// tpTveu-99u
|
||||
//
|
||||
// Life span
|
||||
//
|
||||
// The package guarantees the generation of unique Ids with zero collisions for 34 years
|
||||
// (1/1/2016-1/1/2050) using the same worker Id within a single (although concurrent) application if
|
||||
// application restarts take longer than 1 millisecond. The package supports up to 32 works, all
|
||||
// providing unique sequences.
|
||||
//
|
||||
// Implementation details
|
||||
//
|
||||
// Although heavily inspired by the node.js shortid library this is
|
||||
// not a simple Go port. In addition it
|
||||
//
|
||||
// - is safe to concurrency;
|
||||
// - does not require any yearly version/epoch resets;
|
||||
// - provides stable Id size over a long period at the rate of 1ms;
|
||||
// - guarantees no collisions (due to guaranteed fixed size of Ids between milliseconds and because
|
||||
// multiple requests within the same ms lead to longer Ids with the prefix unique to the ms);
|
||||
// - supports 32 over 16 workers.
|
||||
//
|
||||
// The algorithm uses less randomness than the original node.js implementation, which permits to
|
||||
// extend the life span as well as reduce and guarantee the length. In general terms, each Id
|
||||
// has the following 3 pieces of information encoded: the millisecond (first 8 symbols), the worker
|
||||
// Id (9th symbol), running concurrent counter within the same millisecond, only if required, over
|
||||
// all remaining symbols. The element of randomness per symbol is 1/2 for the worker and the
|
||||
// millisecond and 0 for the counter. Here 0 means no randomness, i.e. every value is encoded using
|
||||
// a 64-base alphabet; 1/2 means one of two matching symbols of the supplied alphabet, 1/4 one of
|
||||
// four matching symbols. The original algorithm of the node.js module uses 1/4 throughout.
|
||||
//
|
||||
// All methods accepting the parameters that govern the randomness are exported and can be used
|
||||
// to directly implement an algorithm with e.g. more randomness, but with longer Ids and shorter
|
||||
// life spans.
|
||||
package shortid
|
||||
|
||||
import (
|
||||
randc "crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
randm "math/rand"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Version defined the library version.
|
||||
const Version = 1.1
|
||||
|
||||
// DefaultABC is the default URL-friendly alphabet.
|
||||
const DefaultABC = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-"
|
||||
|
||||
// Abc represents a shuffled alphabet used to generate the Ids and provides methods to
|
||||
// encode data.
|
||||
type Abc struct {
|
||||
alphabet []rune
|
||||
}
|
||||
|
||||
// Shortid type represents a short Id generator working with a given alphabet.
|
||||
type Shortid struct {
|
||||
abc Abc
|
||||
worker uint
|
||||
epoch time.Time // ids can be generated for 34 years since this date
|
||||
ms uint // ms since epoch for the last id
|
||||
count uint // request count within the same ms
|
||||
mx sync.Mutex // locks access to ms and count
|
||||
}
|
||||
|
||||
var shortid *Shortid
|
||||
|
||||
func init() {
|
||||
shortid = MustNew(0, DefaultABC, 1)
|
||||
}
|
||||
|
||||
// GetDefault retrieves the default short Id generator initialised with the default alphabet,
|
||||
// worker=0 and seed=1. The default can be overwritten using SetDefault.
|
||||
func GetDefault() *Shortid {
|
||||
return (*Shortid)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&shortid))))
|
||||
}
|
||||
|
||||
// SetDefault overwrites the default generator.
|
||||
func SetDefault(sid *Shortid) {
|
||||
target := (*unsafe.Pointer)(unsafe.Pointer(&shortid))
|
||||
source := unsafe.Pointer(sid)
|
||||
atomic.SwapPointer(target, source)
|
||||
}
|
||||
|
||||
// Generate generates an Id using the default generator.
|
||||
func Generate() (string, error) {
|
||||
return shortid.Generate()
|
||||
}
|
||||
|
||||
// MustGenerate acts just like Generate, but panics instead of returning errors.
|
||||
func MustGenerate() string {
|
||||
id, err := Generate()
|
||||
if err == nil {
|
||||
return id
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// New constructs an instance of the short Id generator for the given worker number [0,31], alphabet
|
||||
// (64 unique symbols) and seed value (to shuffle the alphabet). The worker number should be
|
||||
// different for multiple or distributed processes generating Ids into the same data space. The
|
||||
// seed, on contrary, should be identical.
|
||||
func New(worker uint8, alphabet string, seed uint64) (*Shortid, error) {
|
||||
if worker > 31 {
|
||||
return nil, errors.New("expected worker in the range [0,31]")
|
||||
}
|
||||
abc, err := NewAbc(alphabet, seed)
|
||||
if err == nil {
|
||||
sid := &Shortid{
|
||||
abc: abc,
|
||||
worker: uint(worker),
|
||||
epoch: time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||
ms: 0,
|
||||
count: 0,
|
||||
}
|
||||
return sid, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// MustNew acts just like New, but panics instead of returning errors.
|
||||
func MustNew(worker uint8, alphabet string, seed uint64) *Shortid {
|
||||
sid, err := New(worker, alphabet, seed)
|
||||
if err == nil {
|
||||
return sid
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Generate generates a new short Id.
|
||||
func (sid *Shortid) Generate() (string, error) {
|
||||
return sid.GenerateInternal(nil, sid.epoch)
|
||||
}
|
||||
|
||||
// MustGenerate acts just like Generate, but panics instead of returning errors.
|
||||
func (sid *Shortid) MustGenerate() string {
|
||||
id, err := sid.Generate()
|
||||
if err == nil {
|
||||
return id
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// GenerateInternal should only be used for testing purposes.
|
||||
func (sid *Shortid) GenerateInternal(tm *time.Time, epoch time.Time) (string, error) {
|
||||
ms, count := sid.getMsAndCounter(tm, epoch)
|
||||
idrunes := make([]rune, 9)
|
||||
if tmp, err := sid.abc.Encode(ms, 8, 5); err == nil {
|
||||
copy(idrunes, tmp) // first 8 symbols
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
if tmp, err := sid.abc.Encode(sid.worker, 1, 5); err == nil {
|
||||
idrunes[8] = tmp[0]
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
if count > 0 {
|
||||
if countrunes, err := sid.abc.Encode(count, 0, 6); err == nil {
|
||||
// only extend if really need it
|
||||
idrunes = append(idrunes, countrunes...)
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return string(idrunes), nil
|
||||
}
|
||||
|
||||
func (sid *Shortid) getMsAndCounter(tm *time.Time, epoch time.Time) (uint, uint) {
|
||||
sid.mx.Lock()
|
||||
defer sid.mx.Unlock()
|
||||
var ms uint
|
||||
if tm != nil {
|
||||
ms = uint(tm.Sub(epoch).Nanoseconds() / 1000000)
|
||||
} else {
|
||||
ms = uint(time.Now().Sub(epoch).Nanoseconds() / 1000000)
|
||||
}
|
||||
if ms == sid.ms {
|
||||
sid.count++
|
||||
} else {
|
||||
sid.count = 0
|
||||
sid.ms = ms
|
||||
}
|
||||
return sid.ms, sid.count
|
||||
}
|
||||
|
||||
// String returns a string representation of the short Id generator.
|
||||
func (sid *Shortid) String() string {
|
||||
return fmt.Sprintf("Shortid(worker=%v, epoch=%v, abc=%v)", sid.worker, sid.epoch, sid.abc)
|
||||
}
|
||||
|
||||
// Abc returns the instance of alphabet used for representing the Ids.
|
||||
func (sid *Shortid) Abc() Abc {
|
||||
return sid.abc
|
||||
}
|
||||
|
||||
// Epoch returns the value of epoch used as the beginning of millisecond counting (normally
|
||||
// 2016-01-01 00:00:00 local time)
|
||||
func (sid *Shortid) Epoch() time.Time {
|
||||
return sid.epoch
|
||||
}
|
||||
|
||||
// Worker returns the value of worker for this short Id generator.
|
||||
func (sid *Shortid) Worker() uint {
|
||||
return sid.worker
|
||||
}
|
||||
|
||||
// NewAbc constructs a new instance of shuffled alphabet to be used for Id representation.
|
||||
func NewAbc(alphabet string, seed uint64) (Abc, error) {
|
||||
runes := []rune(alphabet)
|
||||
if len(runes) != len(DefaultABC) {
|
||||
return Abc{}, fmt.Errorf("alphabet must contain %v unique characters", len(DefaultABC))
|
||||
}
|
||||
if nonUnique(runes) {
|
||||
return Abc{}, errors.New("alphabet must contain unique characters only")
|
||||
}
|
||||
abc := Abc{alphabet: nil}
|
||||
abc.shuffle(alphabet, seed)
|
||||
return abc, nil
|
||||
}
|
||||
|
||||
// MustNewAbc acts just like NewAbc, but panics instead of returning errors.
|
||||
func MustNewAbc(alphabet string, seed uint64) Abc {
|
||||
res, err := NewAbc(alphabet, seed)
|
||||
if err == nil {
|
||||
return res
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
func nonUnique(runes []rune) bool {
|
||||
found := make(map[rune]struct{})
|
||||
for _, r := range runes {
|
||||
if _, seen := found[r]; !seen {
|
||||
found[r] = struct{}{}
|
||||
}
|
||||
}
|
||||
return len(found) < len(runes)
|
||||
}
|
||||
|
||||
func (abc *Abc) shuffle(alphabet string, seed uint64) {
|
||||
source := []rune(alphabet)
|
||||
for len(source) > 1 {
|
||||
seed = (seed*9301 + 49297) % 233280
|
||||
i := int(seed * uint64(len(source)) / 233280)
|
||||
|
||||
abc.alphabet = append(abc.alphabet, source[i])
|
||||
source = append(source[:i], source[i+1:]...)
|
||||
}
|
||||
abc.alphabet = append(abc.alphabet, source[0])
|
||||
}
|
||||
|
||||
// Encode encodes a given value into a slice of runes of length nsymbols. In case nsymbols==0, the
|
||||
// length of the result is automatically computed from data. Even if fewer symbols is required to
|
||||
// encode the data than nsymbols, all positions are used encoding 0 where required to guarantee
|
||||
// uniqueness in case further data is added to the sequence. The value of digits [4,6] represents
|
||||
// represents n in 2^n, which defines how much randomness flows into the algorithm: 4 -- every value
|
||||
// can be represented by 4 symbols in the alphabet (permitting at most 16 values), 5 -- every value
|
||||
// can be represented by 2 symbols in the alphabet (permitting at most 32 values), 6 -- every value
|
||||
// is represented by exactly 1 symbol with no randomness (permitting 64 values).
|
||||
func (abc *Abc) Encode(val, nsymbols, digits uint) ([]rune, error) {
|
||||
if digits < 4 || 6 < digits {
|
||||
return nil, fmt.Errorf("allowed digits range [4,6], found %v", digits)
|
||||
}
|
||||
|
||||
var computedSize uint = 1
|
||||
if val >= 1 {
|
||||
computedSize = uint(math.Log2(float64(val)))/digits + 1
|
||||
}
|
||||
if nsymbols == 0 {
|
||||
nsymbols = computedSize
|
||||
} else if nsymbols < computedSize {
|
||||
return nil, fmt.Errorf("cannot accommodate data, need %v digits, got %v", computedSize, nsymbols)
|
||||
}
|
||||
|
||||
mask := 1<<digits - 1
|
||||
|
||||
random := make([]int, int(nsymbols))
|
||||
// no random component if digits == 6
|
||||
if digits < 6 {
|
||||
copy(random, maskedRandomInts(len(random), 0x3f-mask))
|
||||
}
|
||||
|
||||
res := make([]rune, int(nsymbols))
|
||||
for i := range res {
|
||||
shift := digits * uint(i)
|
||||
index := (int(val>>shift) & mask) | random[i]
|
||||
res[i] = abc.alphabet[index]
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// MustEncode acts just like Encode, but panics instead of returning errors.
|
||||
func (abc *Abc) MustEncode(val, size, digits uint) []rune {
|
||||
res, err := abc.Encode(val, size, digits)
|
||||
if err == nil {
|
||||
return res
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
func maskedRandomInts(size, mask int) []int {
|
||||
ints := make([]int, size)
|
||||
bytes := make([]byte, size)
|
||||
if _, err := randc.Read(bytes); err == nil {
|
||||
for i, b := range bytes {
|
||||
ints[i] = int(b) & mask
|
||||
}
|
||||
} else {
|
||||
for i := range ints {
|
||||
ints[i] = randm.Intn(0xff) & mask
|
||||
}
|
||||
}
|
||||
return ints
|
||||
}
|
||||
|
||||
// String returns a string representation of the Abc instance.
|
||||
func (abc Abc) String() string {
|
||||
return fmt.Sprintf("Abc{alphabet='%v')", abc.Alphabet())
|
||||
}
|
||||
|
||||
// Alphabet returns the alphabet used as an immutable string.
|
||||
func (abc Abc) Alphabet() string {
|
||||
return string(abc.alphabet)
|
||||
}
|
Loading…
Reference in New Issue
Block a user