Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v8.0.x] Library Panels: Add name endpoint & unique name validation (#33987) #34285

Merged
merged 1 commit into from May 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 12 additions & 1 deletion pkg/services/libraryelements/api.go
Expand Up @@ -19,6 +19,7 @@ func (l *LibraryElementService) registerAPIEndpoints() {
entities.Get("/", middleware.ReqSignedIn, routing.Wrap(l.getAllHandler))
entities.Get("/:uid", middleware.ReqSignedIn, routing.Wrap(l.getHandler))
entities.Get("/:uid/connections/", middleware.ReqSignedIn, routing.Wrap(l.getConnectionsHandler))
entities.Get("/name/:name", middleware.ReqSignedIn, routing.Wrap(l.getByNameHandler))
entities.Patch("/:uid", middleware.ReqSignedIn, binding.Bind(patchLibraryElementCommand{}), routing.Wrap(l.patchHandler))
})
}
Expand All @@ -45,7 +46,7 @@ func (l *LibraryElementService) deleteHandler(c *models.ReqContext) response.Res

// getHandler handles GET /api/library-elements/:uid.
func (l *LibraryElementService) getHandler(c *models.ReqContext) response.Response {
element, err := l.getLibraryElement(c, c.Params(":uid"))
element, err := l.getLibraryElementByUid(c)
if err != nil {
return toLibraryElementError(err, "Failed to get library element")
}
Expand Down Expand Up @@ -93,6 +94,16 @@ func (l *LibraryElementService) getConnectionsHandler(c *models.ReqContext) resp
return response.JSON(200, util.DynMap{"result": connections})
}

// getByNameHandler handles GET /api/library-elements/name/:name/.
func (l *LibraryElementService) getByNameHandler(c *models.ReqContext) response.Response {
elements, err := l.getLibraryElementsByName(c)
if err != nil {
return toLibraryElementError(err, "Failed to get library element")
}

return response.JSON(200, util.DynMap{"result": elements})
}

func toLibraryElementError(err error, message string) response.Response {
if errors.Is(err, errLibraryElementAlreadyExists) {
return response.Error(400, errLibraryElementAlreadyExists.Error(), err)
Expand Down
95 changes: 55 additions & 40 deletions pkg/services/libraryelements/database.go
Expand Up @@ -187,24 +187,23 @@ func (l *LibraryElementService) deleteLibraryElement(c *models.ReqContext, uid s
})
}

// getLibraryElement gets a Library Element.
func (l *LibraryElementService) getLibraryElement(c *models.ReqContext, uid string) (LibraryElementDTO, error) {
var libraryElement LibraryElementWithMeta
// getLibraryElement gets a Library Element where param == value
func (l *LibraryElementService) getLibraryElements(c *models.ReqContext, params []Pair) ([]LibraryElementDTO, error) {
libraryElements := make([]LibraryElementWithMeta, 0)
err := l.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
libraryElements := make([]LibraryElementWithMeta, 0)
builder := sqlstore.SQLBuilder{}
builder.Write(selectLibraryElementDTOWithMeta)
builder.Write(", 'General' as folder_name ")
builder.Write(", '' as folder_uid ")
builder.Write(fromLibraryElementDTOWithMeta)
builder.Write(` WHERE le.uid=? AND le.org_id=? AND le.folder_id=0`, uid, c.SignedInUser.OrgId)
writeParamSelectorSQL(&builder, append(params, Pair{"folder_id", 0})...)
builder.Write(" UNION ")
builder.Write(selectLibraryElementDTOWithMeta)
builder.Write(", dashboard.title as folder_name ")
builder.Write(", dashboard.uid as folder_uid ")
builder.Write(fromLibraryElementDTOWithMeta)
builder.Write(" INNER JOIN dashboard AS dashboard on le.folder_id = dashboard.id AND le.folder_id <> 0")
builder.Write(` WHERE le.uid=? AND le.org_id=?`, uid, c.SignedInUser.OrgId)
writeParamSelectorSQL(&builder, params...)
if c.SignedInUser.OrgRole != models.ROLE_ADMIN {
builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW)
}
Expand All @@ -215,49 +214,65 @@ func (l *LibraryElementService) getLibraryElement(c *models.ReqContext, uid stri
if len(libraryElements) == 0 {
return errLibraryElementNotFound
}
if len(libraryElements) > 1 {
return fmt.Errorf("found %d elements, while expecting at most one", len(libraryElements))
}

libraryElement = libraryElements[0]

return nil
})
if err != nil {
return LibraryElementDTO{}, err
return []LibraryElementDTO{}, err
}

dto := LibraryElementDTO{
ID: libraryElement.ID,
OrgID: libraryElement.OrgID,
FolderID: libraryElement.FolderID,
UID: libraryElement.UID,
Name: libraryElement.Name,
Kind: libraryElement.Kind,
Type: libraryElement.Type,
Description: libraryElement.Description,
Model: libraryElement.Model,
Version: libraryElement.Version,
Meta: LibraryElementDTOMeta{
FolderName: libraryElement.FolderName,
FolderUID: libraryElement.FolderUID,
ConnectedDashboards: libraryElement.ConnectedDashboards,
Created: libraryElement.Created,
Updated: libraryElement.Updated,
CreatedBy: LibraryElementDTOMetaUser{
ID: libraryElement.CreatedBy,
Name: libraryElement.CreatedByName,
AvatarURL: dtos.GetGravatarUrl(libraryElement.CreatedByEmail),
},
UpdatedBy: LibraryElementDTOMetaUser{
ID: libraryElement.UpdatedBy,
Name: libraryElement.UpdatedByName,
AvatarURL: dtos.GetGravatarUrl(libraryElement.UpdatedByEmail),
leDtos := make([]LibraryElementDTO, len(libraryElements))
for i, libraryElement := range libraryElements {
leDtos[i] = LibraryElementDTO{
ID: libraryElement.ID,
OrgID: libraryElement.OrgID,
FolderID: libraryElement.FolderID,
UID: libraryElement.UID,
Name: libraryElement.Name,
Kind: libraryElement.Kind,
Type: libraryElement.Type,
Description: libraryElement.Description,
Model: libraryElement.Model,
Version: libraryElement.Version,
Meta: LibraryElementDTOMeta{
FolderName: libraryElement.FolderName,
FolderUID: libraryElement.FolderUID,
ConnectedDashboards: libraryElement.ConnectedDashboards,
Created: libraryElement.Created,
Updated: libraryElement.Updated,
CreatedBy: LibraryElementDTOMetaUser{
ID: libraryElement.CreatedBy,
Name: libraryElement.CreatedByName,
AvatarURL: dtos.GetGravatarUrl(libraryElement.CreatedByEmail),
},
UpdatedBy: LibraryElementDTOMetaUser{
ID: libraryElement.UpdatedBy,
Name: libraryElement.UpdatedByName,
AvatarURL: dtos.GetGravatarUrl(libraryElement.UpdatedByEmail),
},
},
},
}
}

return dto, nil
return leDtos, nil
}

// getLibraryElementByUid gets a Library Element by uid.
func (l *LibraryElementService) getLibraryElementByUid(c *models.ReqContext) (LibraryElementDTO, error) {
libraryElements, err := l.getLibraryElements(c, []Pair{{key: "org_id", value: c.SignedInUser.OrgId}, {key: "uid", value: c.Params(":uid")}})
if err != nil {
return LibraryElementDTO{}, err
}
if len(libraryElements) > 1 {
return LibraryElementDTO{}, fmt.Errorf("found %d elements, while expecting at most one", len(libraryElements))
}

return libraryElements[0], nil
}

// getLibraryElementByName gets a Library Element by name.
func (l *LibraryElementService) getLibraryElementsByName(c *models.ReqContext) ([]LibraryElementDTO, error) {
return l.getLibraryElements(c, []Pair{{"org_id", c.SignedInUser.OrgId}, {"name", c.Params(":name")}})
}

// getAllLibraryElements gets all Library Elements.
Expand Down
190 changes: 115 additions & 75 deletions pkg/services/libraryelements/libraryelements_get_test.go
Expand Up @@ -14,54 +14,74 @@ import (
func TestGetLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to get a library panel that does not exist, it should fail",
func(t *testing.T, sc scenarioContext) {
// by uid
sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown"})
resp := sc.service.getHandler(sc.reqContext)
require.Equal(t, 404, resp.Status())

// by name
sc.reqContext.ReplaceAllParams(map[string]string{":name": "unknown"})
resp = sc.service.getByNameHandler(sc.reqContext)
require.Equal(t, 404, resp.Status())
})

scenarioWithPanel(t, "When an admin tries to get a library panel that exists, it should succeed and return correct result",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.getHandler(sc.reqContext)
var result = validateAndUnMarshalResponse(t, resp)
var expected = libraryElementResult{
Result: libraryElement{
ID: 1,
OrgID: 1,
FolderID: 1,
UID: result.Result.UID,
Name: "Text - Library Panel",
Kind: int64(Panel),
Type: "text",
Description: "A description",
Model: map[string]interface{}{
"datasource": "${DS_GDEV-TESTDATA}",
"description": "A description",
"id": float64(1),
"title": "Text - Library Panel",
"type": "text",
},
Version: 1,
Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid,
ConnectedDashboards: 0,
Created: result.Result.Meta.Created,
Updated: result.Result.Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{
ID: 1,
Name: userInDbName,
AvatarURL: userInDbAvatar,
var expected = func(res libraryElementResult) libraryElementResult {
return libraryElementResult{
Result: libraryElement{
ID: 1,
OrgID: 1,
FolderID: 1,
UID: res.Result.UID,
Name: "Text - Library Panel",
Kind: int64(Panel),
Type: "text",
Description: "A description",
Model: map[string]interface{}{
"datasource": "${DS_GDEV-TESTDATA}",
"description": "A description",
"id": float64(1),
"title": "Text - Library Panel",
"type": "text",
},
UpdatedBy: LibraryElementDTOMetaUser{
ID: 1,
Name: userInDbName,
AvatarURL: userInDbAvatar,
Version: 1,
Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid,
ConnectedDashboards: 0,
Created: res.Result.Meta.Created,
Updated: res.Result.Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{
ID: 1,
Name: userInDbName,
AvatarURL: userInDbAvatar,
},
UpdatedBy: LibraryElementDTOMetaUser{
ID: 1,
Name: userInDbName,
AvatarURL: userInDbAvatar,
},
},
},
},
}
}
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {

// by uid
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.getHandler(sc.reqContext)
var result = validateAndUnMarshalResponse(t, resp)

if diff := cmp.Diff(expected(result), result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}

// by name
sc.reqContext.ReplaceAllParams(map[string]string{":name": sc.initialResult.Result.Name})
resp = sc.service.getByNameHandler(sc.reqContext)
arrayResult := validateAndUnMarshalArrayResponse(t, resp)

if diff := cmp.Diff(libraryElementArrayResult{Result: []libraryElement{expected(result).Result}}, arrayResult, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
Expand Down Expand Up @@ -102,57 +122,77 @@ func TestGetLibraryElement(t *testing.T) {
err := sc.service.ConnectElementsToDashboard(sc.reqContext, []string{sc.initialResult.Result.UID}, dashInDB.Id)
require.NoError(t, err)

sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.getHandler(sc.reqContext)
var result = validateAndUnMarshalResponse(t, resp)
var expected = libraryElementResult{
Result: libraryElement{
ID: 1,
OrgID: 1,
FolderID: 1,
UID: result.Result.UID,
Name: "Text - Library Panel",
Kind: int64(Panel),
Type: "text",
Description: "A description",
Model: map[string]interface{}{
"datasource": "${DS_GDEV-TESTDATA}",
"description": "A description",
"id": float64(1),
"title": "Text - Library Panel",
"type": "text",
},
Version: 1,
Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid,
ConnectedDashboards: 1,
Created: result.Result.Meta.Created,
Updated: result.Result.Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{
ID: 1,
Name: userInDbName,
AvatarURL: userInDbAvatar,
expected := func(res libraryElementResult) libraryElementResult {
return libraryElementResult{
Result: libraryElement{
ID: 1,
OrgID: 1,
FolderID: 1,
UID: res.Result.UID,
Name: "Text - Library Panel",
Kind: int64(Panel),
Type: "text",
Description: "A description",
Model: map[string]interface{}{
"datasource": "${DS_GDEV-TESTDATA}",
"description": "A description",
"id": float64(1),
"title": "Text - Library Panel",
"type": "text",
},
UpdatedBy: LibraryElementDTOMetaUser{
ID: 1,
Name: userInDbName,
AvatarURL: userInDbAvatar,
Version: 1,
Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid,
ConnectedDashboards: 1,
Created: res.Result.Meta.Created,
Updated: res.Result.Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{
ID: 1,
Name: userInDbName,
AvatarURL: userInDbAvatar,
},
UpdatedBy: LibraryElementDTOMetaUser{
ID: 1,
Name: userInDbName,
AvatarURL: userInDbAvatar,
},
},
},
},
}
}

// by uid
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.getHandler(sc.reqContext)
result := validateAndUnMarshalResponse(t, resp)

if diff := cmp.Diff(expected(result), result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {

// by name
sc.reqContext.ReplaceAllParams(map[string]string{":name": sc.initialResult.Result.Name})
resp = sc.service.getByNameHandler(sc.reqContext)
arrayResult := validateAndUnMarshalArrayResponse(t, resp)
if diff := cmp.Diff(libraryElementArrayResult{Result: []libraryElement{expected(result).Result}}, arrayResult, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})

scenarioWithPanel(t, "When an admin tries to get a library panel that exists in an other org, it should fail",
func(t *testing.T, sc scenarioContext) {
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
sc.reqContext.SignedInUser.OrgId = 2
sc.reqContext.SignedInUser.OrgRole = models.ROLE_ADMIN

// by uid
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.getHandler(sc.reqContext)
require.Equal(t, 404, resp.Status())

// by name
sc.reqContext.ReplaceAllParams(map[string]string{":name": sc.initialResult.Result.Name})
resp = sc.service.getByNameHandler(sc.reqContext)
require.Equal(t, 404, resp.Status())
})
}