Library Panels: Add name endpoint & unique name validation to AddLibraryPanelModal (#33987)
This commit is contained in:
@@ -19,6 +19,7 @@ func (l *LibraryElementService) registerAPIEndpoints() {
|
|||||||
entities.Get("/", middleware.ReqSignedIn, routing.Wrap(l.getAllHandler))
|
entities.Get("/", middleware.ReqSignedIn, routing.Wrap(l.getAllHandler))
|
||||||
entities.Get("/:uid", middleware.ReqSignedIn, routing.Wrap(l.getHandler))
|
entities.Get("/:uid", middleware.ReqSignedIn, routing.Wrap(l.getHandler))
|
||||||
entities.Get("/:uid/connections/", middleware.ReqSignedIn, routing.Wrap(l.getConnectionsHandler))
|
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))
|
entities.Patch("/:uid", middleware.ReqSignedIn, binding.Bind(patchLibraryElementCommand{}), routing.Wrap(l.patchHandler))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -45,7 +46,7 @@ func (l *LibraryElementService) deleteHandler(c *models.ReqContext) response.Res
|
|||||||
|
|
||||||
// getHandler handles GET /api/library-elements/:uid.
|
// getHandler handles GET /api/library-elements/:uid.
|
||||||
func (l *LibraryElementService) getHandler(c *models.ReqContext) response.Response {
|
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 {
|
if err != nil {
|
||||||
return toLibraryElementError(err, "Failed to get library element")
|
return toLibraryElementError(err, "Failed to get library element")
|
||||||
}
|
}
|
||||||
@@ -93,6 +94,16 @@ func (l *LibraryElementService) getConnectionsHandler(c *models.ReqContext) resp
|
|||||||
return response.JSON(200, util.DynMap{"result": connections})
|
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 {
|
func toLibraryElementError(err error, message string) response.Response {
|
||||||
if errors.Is(err, errLibraryElementAlreadyExists) {
|
if errors.Is(err, errLibraryElementAlreadyExists) {
|
||||||
return response.Error(400, errLibraryElementAlreadyExists.Error(), err)
|
return response.Error(400, errLibraryElementAlreadyExists.Error(), err)
|
||||||
|
|||||||
@@ -187,24 +187,23 @@ func (l *LibraryElementService) deleteLibraryElement(c *models.ReqContext, uid s
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// getLibraryElement gets a Library Element.
|
// getLibraryElement gets a Library Element where param == value
|
||||||
func (l *LibraryElementService) getLibraryElement(c *models.ReqContext, uid string) (LibraryElementDTO, error) {
|
func (l *LibraryElementService) getLibraryElements(c *models.ReqContext, params []Pair) ([]LibraryElementDTO, error) {
|
||||||
var libraryElement LibraryElementWithMeta
|
libraryElements := make([]LibraryElementWithMeta, 0)
|
||||||
err := l.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
err := l.SQLStore.WithDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
|
||||||
libraryElements := make([]LibraryElementWithMeta, 0)
|
|
||||||
builder := sqlstore.SQLBuilder{}
|
builder := sqlstore.SQLBuilder{}
|
||||||
builder.Write(selectLibraryElementDTOWithMeta)
|
builder.Write(selectLibraryElementDTOWithMeta)
|
||||||
builder.Write(", 'General' as folder_name ")
|
builder.Write(", 'General' as folder_name ")
|
||||||
builder.Write(", '' as folder_uid ")
|
builder.Write(", '' as folder_uid ")
|
||||||
builder.Write(fromLibraryElementDTOWithMeta)
|
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(" UNION ")
|
||||||
builder.Write(selectLibraryElementDTOWithMeta)
|
builder.Write(selectLibraryElementDTOWithMeta)
|
||||||
builder.Write(", dashboard.title as folder_name ")
|
builder.Write(", dashboard.title as folder_name ")
|
||||||
builder.Write(", dashboard.uid as folder_uid ")
|
builder.Write(", dashboard.uid as folder_uid ")
|
||||||
builder.Write(fromLibraryElementDTOWithMeta)
|
builder.Write(fromLibraryElementDTOWithMeta)
|
||||||
builder.Write(" INNER JOIN dashboard AS dashboard on le.folder_id = dashboard.id AND le.folder_id <> 0")
|
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 {
|
if c.SignedInUser.OrgRole != models.ROLE_ADMIN {
|
||||||
builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW)
|
builder.WriteDashboardPermissionFilter(c.SignedInUser, models.PERMISSION_VIEW)
|
||||||
}
|
}
|
||||||
@@ -215,49 +214,65 @@ func (l *LibraryElementService) getLibraryElement(c *models.ReqContext, uid stri
|
|||||||
if len(libraryElements) == 0 {
|
if len(libraryElements) == 0 {
|
||||||
return errLibraryElementNotFound
|
return errLibraryElementNotFound
|
||||||
}
|
}
|
||||||
if len(libraryElements) > 1 {
|
|
||||||
return fmt.Errorf("found %d elements, while expecting at most one", len(libraryElements))
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryElement = libraryElements[0]
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return []LibraryElementDTO{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 {
|
if err != nil {
|
||||||
return LibraryElementDTO{}, err
|
return LibraryElementDTO{}, err
|
||||||
}
|
}
|
||||||
|
if len(libraryElements) > 1 {
|
||||||
dto := LibraryElementDTO{
|
return LibraryElementDTO{}, fmt.Errorf("found %d elements, while expecting at most one", len(libraryElements))
|
||||||
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 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.
|
// getAllLibraryElements gets all Library Elements.
|
||||||
|
|||||||
@@ -14,54 +14,74 @@ import (
|
|||||||
func TestGetLibraryElement(t *testing.T) {
|
func TestGetLibraryElement(t *testing.T) {
|
||||||
scenarioWithPanel(t, "When an admin tries to get a library panel that does not exist, it should fail",
|
scenarioWithPanel(t, "When an admin tries to get a library panel that does not exist, it should fail",
|
||||||
func(t *testing.T, sc scenarioContext) {
|
func(t *testing.T, sc scenarioContext) {
|
||||||
|
// by uid
|
||||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown"})
|
sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown"})
|
||||||
resp := sc.service.getHandler(sc.reqContext)
|
resp := sc.service.getHandler(sc.reqContext)
|
||||||
require.Equal(t, 404, resp.Status())
|
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",
|
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) {
|
func(t *testing.T, sc scenarioContext) {
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// by uid
|
||||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||||
resp := sc.service.getHandler(sc.reqContext)
|
resp := sc.service.getHandler(sc.reqContext)
|
||||||
var result = validateAndUnMarshalResponse(t, resp)
|
var result = validateAndUnMarshalResponse(t, resp)
|
||||||
var expected = libraryElementResult{
|
|
||||||
Result: libraryElement{
|
if diff := cmp.Diff(expected(result), result, getCompareOptions()...); diff != "" {
|
||||||
ID: 1,
|
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||||
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,
|
|
||||||
},
|
|
||||||
UpdatedBy: LibraryElementDTOMetaUser{
|
|
||||||
ID: 1,
|
|
||||||
Name: userInDbName,
|
|
||||||
AvatarURL: userInDbAvatar,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
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)
|
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -102,57 +122,77 @@ func TestGetLibraryElement(t *testing.T) {
|
|||||||
err := sc.service.ConnectElementsToDashboard(sc.reqContext, []string{sc.initialResult.Result.UID}, dashInDB.Id)
|
err := sc.service.ConnectElementsToDashboard(sc.reqContext, []string{sc.initialResult.Result.UID}, dashInDB.Id)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
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})
|
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||||
resp := sc.service.getHandler(sc.reqContext)
|
resp := sc.service.getHandler(sc.reqContext)
|
||||||
var result = validateAndUnMarshalResponse(t, resp)
|
result := validateAndUnMarshalResponse(t, resp)
|
||||||
var expected = libraryElementResult{
|
|
||||||
Result: libraryElement{
|
if diff := cmp.Diff(expected(result), result, getCompareOptions()...); diff != "" {
|
||||||
ID: 1,
|
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
|
||||||
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,
|
|
||||||
},
|
|
||||||
UpdatedBy: LibraryElementDTOMetaUser{
|
|
||||||
ID: 1,
|
|
||||||
Name: userInDbName,
|
|
||||||
AvatarURL: userInDbAvatar,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
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)
|
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",
|
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) {
|
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.OrgId = 2
|
||||||
sc.reqContext.SignedInUser.OrgRole = models.ROLE_ADMIN
|
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)
|
resp := sc.service.getHandler(sc.reqContext)
|
||||||
require.Equal(t, 404, resp.Status())
|
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())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,10 @@ type libraryElementResult struct {
|
|||||||
Result libraryElement `json:"result"`
|
Result libraryElement `json:"result"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type libraryElementArrayResult struct {
|
||||||
|
Result []libraryElement `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
type libraryElementsSearch struct {
|
type libraryElementsSearch struct {
|
||||||
Result libraryElementsSearchResult `json:"result"`
|
Result libraryElementsSearchResult `json:"result"`
|
||||||
}
|
}
|
||||||
@@ -248,6 +252,17 @@ func validateAndUnMarshalResponse(t *testing.T, resp response.Response) libraryE
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateAndUnMarshalArrayResponse(t *testing.T, resp response.Response) libraryElementArrayResult {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
require.Equal(t, 200, resp.Status())
|
||||||
|
var result = libraryElementArrayResult{}
|
||||||
|
err := json.Unmarshal(resp.Body(), &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
|
func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,28 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Pair struct {
|
||||||
|
key string
|
||||||
|
value interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectLibraryElementByParam(params []Pair) (string, []interface{}) {
|
||||||
|
conditions := make([]string, 0, len(params))
|
||||||
|
values := make([]interface{}, 0, len(params))
|
||||||
|
for _, p := range params {
|
||||||
|
conditions = append(conditions, "le."+p.key+"=?")
|
||||||
|
values = append(values, p.value)
|
||||||
|
}
|
||||||
|
return ` WHERE ` + strings.Join(conditions, " AND "), values
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeParamSelectorSQL(builder *sqlstore.SQLBuilder, params ...Pair) {
|
||||||
|
if len(params) > 0 {
|
||||||
|
conditionString, paramValues := selectLibraryElementByParam(params)
|
||||||
|
builder.Write(conditionString, paramValues...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func writePerPageSQL(query searchLibraryElementsQuery, sqlStore *sqlstore.SQLStore, builder *sqlstore.SQLBuilder) {
|
func writePerPageSQL(query searchLibraryElementsQuery, sqlStore *sqlstore.SQLStore, builder *sqlstore.SQLBuilder) {
|
||||||
if query.perPage != 0 {
|
if query.perPage != 0 {
|
||||||
offset := query.perPage * (query.page - 1)
|
offset := query.perPage * (query.page - 1)
|
||||||
|
|||||||
+37
-8
@@ -1,8 +1,11 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { Button, Field, Input, Modal } from '@grafana/ui';
|
import { Button, Field, Input, Modal } from '@grafana/ui';
|
||||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||||
import { PanelModel } from '../../../dashboard/state';
|
import { PanelModel } from '../../../dashboard/state';
|
||||||
import { usePanelSave } from '../../utils/usePanelSave';
|
import { usePanelSave } from '../../utils/usePanelSave';
|
||||||
|
import { useAsync, useDebounce } from 'react-use';
|
||||||
|
import { getLibraryPanelByName } from '../../state/api';
|
||||||
|
|
||||||
interface AddLibraryPanelContentsProps {
|
interface AddLibraryPanelContentsProps {
|
||||||
onDismiss: () => void;
|
onDismiss: () => void;
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
@@ -12,11 +15,42 @@ interface AddLibraryPanelContentsProps {
|
|||||||
export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: AddLibraryPanelContentsProps) => {
|
export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: AddLibraryPanelContentsProps) => {
|
||||||
const [folderId, setFolderId] = useState(initialFolderId);
|
const [folderId, setFolderId] = useState(initialFolderId);
|
||||||
const [panelTitle, setPanelTitle] = useState(panel.title);
|
const [panelTitle, setPanelTitle] = useState(panel.title);
|
||||||
|
const [debouncedPanelTitle, setDebouncedPanelTitle] = useState(panel.title);
|
||||||
|
const [waiting, setWaiting] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => setWaiting(true), [panelTitle]);
|
||||||
|
useDebounce(() => setDebouncedPanelTitle(panelTitle), 350, [panelTitle]);
|
||||||
|
|
||||||
const { saveLibraryPanel } = usePanelSave();
|
const { saveLibraryPanel } = usePanelSave();
|
||||||
|
const onCreate = useCallback(() => {
|
||||||
|
panel.title = panelTitle;
|
||||||
|
saveLibraryPanel(panel, folderId!).then((res) => {
|
||||||
|
if (!(res instanceof Error)) {
|
||||||
|
onDismiss();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [panel, panelTitle, folderId, onDismiss, saveLibraryPanel]);
|
||||||
|
const isValidTitle = useAsync(async () => {
|
||||||
|
try {
|
||||||
|
return !(await getLibraryPanelByName(panelTitle)).some((lp) => lp.folderId === folderId);
|
||||||
|
} catch (err) {
|
||||||
|
err.isHandled = true;
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
setWaiting(false);
|
||||||
|
}
|
||||||
|
}, [debouncedPanelTitle, folderId]);
|
||||||
|
|
||||||
|
const invalidInput =
|
||||||
|
!isValidTitle?.value && isValidTitle.value !== undefined && panelTitle === debouncedPanelTitle && !waiting;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Field label="Library panel name">
|
<Field
|
||||||
|
label="Library panel name"
|
||||||
|
invalid={invalidInput}
|
||||||
|
error={invalidInput ? 'Library panel with this name already exists' : ''}
|
||||||
|
>
|
||||||
<Input name="name" value={panelTitle} onChange={(e) => setPanelTitle(e.currentTarget.value)} />
|
<Input name="name" value={panelTitle} onChange={(e) => setPanelTitle(e.currentTarget.value)} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Save in folder" description="Library panel permissions are derived from the folder permissions">
|
<Field label="Save in folder" description="Library panel permissions are derived from the folder permissions">
|
||||||
@@ -27,12 +61,7 @@ export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: A
|
|||||||
<Button variant="secondary" onClick={onDismiss} fill="outline">
|
<Button variant="secondary" onClick={onDismiss} fill="outline">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button onClick={onCreate} disabled={invalidInput}>
|
||||||
onClick={() => {
|
|
||||||
panel.title = panelTitle;
|
|
||||||
saveLibraryPanel(panel, folderId!).then(() => onDismiss());
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Create library panel
|
Create library panel
|
||||||
</Button>
|
</Button>
|
||||||
</Modal.ButtonRow>
|
</Modal.ButtonRow>
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ export async function getLibraryPanel(uid: string): Promise<LibraryElementDTO> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getLibraryPanelByName(name: string): Promise<LibraryElementDTO[]> {
|
||||||
|
const { result } = await getBackendSrv().get<{ result: LibraryElementDTO[] }>(`/api/library-elements/name/${name}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export async function addLibraryPanel(
|
export async function addLibraryPanel(
|
||||||
panelSaveModel: PanelModelWithLibraryPanel,
|
panelSaveModel: PanelModelWithLibraryPanel,
|
||||||
folderId: number
|
folderId: number
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ import { notifyApp } from 'app/core/actions';
|
|||||||
export const usePanelSave = () => {
|
export const usePanelSave = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [state, saveLibraryPanel] = useAsyncFn(async (panel: PanelModel, folderId: number) => {
|
const [state, saveLibraryPanel] = useAsyncFn(async (panel: PanelModel, folderId: number) => {
|
||||||
return await saveAndRefreshLibraryPanel(panel, folderId);
|
try {
|
||||||
|
return await saveAndRefreshLibraryPanel(panel, folderId);
|
||||||
|
} catch (err) {
|
||||||
|
err.isHandled = true;
|
||||||
|
throw new Error(err.data.message);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user