LibraryPanels: Support CRUD via apiserver (#113035)
This commit is contained in:
@@ -87,7 +87,7 @@ func (l *LibraryElementService) createHandler(c *contextmodel.ReqContext) respon
|
||||
}
|
||||
}
|
||||
|
||||
element, err := l.createLibraryElement(c.Req.Context(), c.SignedInUser, cmd)
|
||||
element, err := l.CreateElement(c.Req.Context(), c.SignedInUser, cmd)
|
||||
if err != nil {
|
||||
return l.toLibraryElementError(err, "Failed to create library element")
|
||||
}
|
||||
@@ -124,7 +124,7 @@ func (l *LibraryElementService) createHandler(c *contextmodel.ReqContext) respon
|
||||
// 404: notFoundError
|
||||
// 500: internalServerError
|
||||
func (l *LibraryElementService) deleteHandler(c *contextmodel.ReqContext) response.Response {
|
||||
id, err := l.deleteLibraryElement(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"])
|
||||
id, err := l.DeleteLibraryElement(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"])
|
||||
if err != nil {
|
||||
return l.toLibraryElementError(err, "Failed to delete library element")
|
||||
}
|
||||
@@ -252,7 +252,7 @@ func (l *LibraryElementService) patchHandler(c *contextmodel.ReqContext) respons
|
||||
}
|
||||
}
|
||||
|
||||
element, err := l.patchLibraryElement(c.Req.Context(), c.SignedInUser, cmd, web.Params(c.Req)[":uid"])
|
||||
element, err := l.PatchLibraryElement(c.Req.Context(), c.SignedInUser, cmd, web.Params(c.Req)[":uid"])
|
||||
if err != nil {
|
||||
return l.toLibraryElementError(err, "Failed to update library element")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package libraryelements
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements/model"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func ToCreateLibraryElementCommand(raw runtime.Object) (*model.CreateLibraryElementCommand, error) {
|
||||
obj, err := utils.MetaAccessor(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
folder := obj.GetFolder()
|
||||
cmd := &model.CreateLibraryElementCommand{
|
||||
UID: obj.GetName(),
|
||||
FolderUID: &folder,
|
||||
Kind: 1, // the only kind... LibraryPanel
|
||||
Name: obj.FindTitle("library panel"),
|
||||
}
|
||||
if cmd.UID == "" {
|
||||
if obj.GetGenerateName() == "" {
|
||||
return nil, fmt.Errorf("expecting either name or generateName property")
|
||||
}
|
||||
cmd.UID = obj.GetGenerateName() + util.GenerateShortUID()
|
||||
}
|
||||
cmd.Model, err = toRawMessage(raw)
|
||||
return cmd, err
|
||||
}
|
||||
|
||||
func ToPatchLibraryElementCommand(raw runtime.Object) (*model.PatchLibraryElementCommand, error) {
|
||||
obj, err := utils.MetaAccessor(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
folder := obj.GetFolder()
|
||||
cmd := &model.PatchLibraryElementCommand{
|
||||
UID: obj.GetName(),
|
||||
FolderUID: &folder,
|
||||
Kind: 1, // the only kind... LibraryPanel
|
||||
Name: obj.FindTitle("library panel"),
|
||||
}
|
||||
cmd.Model, err = toRawMessage(raw)
|
||||
return cmd, err
|
||||
}
|
||||
|
||||
func toRawMessage(raw runtime.Object) (json.RawMessage, error) {
|
||||
switch obj := raw.(type) {
|
||||
case *v0alpha1.LibraryPanel:
|
||||
return json.Marshal(obj.Spec)
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported library panel type: %T", raw)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package libraryelements
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements/model"
|
||||
)
|
||||
|
||||
func TestConversionsCommands(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
input runtime.Object
|
||||
expectedCreate *model.CreateLibraryElementCommand
|
||||
expectedPatch *model.PatchLibraryElementCommand
|
||||
}{
|
||||
{
|
||||
name: "basic conversion",
|
||||
input: &v0alpha1.LibraryPanel{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "uid",
|
||||
Annotations: map[string]string{
|
||||
utils.AnnoKeyFolder: "aaa",
|
||||
},
|
||||
},
|
||||
Spec: v0alpha1.LibraryPanelSpec{
|
||||
Type: "timeseries",
|
||||
PluginVersion: "1.2.3",
|
||||
Title: "title",
|
||||
Description: "descr",
|
||||
Options: common.Unstructured{
|
||||
Object: map[string]any{
|
||||
"hello": "options",
|
||||
},
|
||||
},
|
||||
FieldConfig: common.Unstructured{
|
||||
Object: map[string]any{
|
||||
"hello": "fieldConfig",
|
||||
},
|
||||
},
|
||||
PanelTitle: "panel title",
|
||||
GridPos: v0alpha1.GridPos{
|
||||
W: 1, H: 2, X: 3, Y: 4,
|
||||
},
|
||||
Transparent: true,
|
||||
Links: []common.Unstructured{{
|
||||
Object: map[string]any{
|
||||
"link1": "hello",
|
||||
},
|
||||
}},
|
||||
Datasource: &data.DataSourceRef{
|
||||
UID: "uid",
|
||||
Type: "ttt",
|
||||
APIVersion: "v0alpha1",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedCreate: &model.CreateLibraryElementCommand{
|
||||
FolderUID: ptr.To("aaa"),
|
||||
UID: "uid",
|
||||
Name: "title",
|
||||
Kind: 1,
|
||||
Model: json.RawMessage(`{"type":"timeseries","pluginVersion":"1.2.3","title":"title","panelTitle":"panel title","description":"descr","options":{"hello":"options"},"fieldConfig":{"hello":"fieldConfig"},"datasource":{"type":"ttt","uid":"uid","apiVersion":"v0alpha1"},"gridPos":{"w":1,"h":2,"x":3,"y":4},"transparent":true,"links":[{"link1":"hello"}]}`),
|
||||
},
|
||||
expectedPatch: &model.PatchLibraryElementCommand{
|
||||
FolderUID: ptr.To("aaa"),
|
||||
UID: "uid",
|
||||
Name: "title",
|
||||
Kind: 1,
|
||||
Version: 0,
|
||||
Model: json.RawMessage(`{"type":"timeseries","pluginVersion":"1.2.3","title":"title","panelTitle":"panel title","description":"descr","options":{"hello":"options"},"fieldConfig":{"hello":"fieldConfig"},"datasource":{"type":"ttt","uid":"uid","apiVersion":"v0alpha1"},"gridPos":{"w":1,"h":2,"x":3,"y":4},"transparent":true,"links":[{"link1":"hello"}]}`),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
create, err := ToCreateLibraryElementCommand(tt.input)
|
||||
require.NoError(t, err)
|
||||
if diff := cmp.Diff(tt.expectedCreate, create); diff != "" {
|
||||
require.FailNowf(t, "Create mismatch (-want +got):%s", diff)
|
||||
}
|
||||
|
||||
patch, err := ToPatchLibraryElementCommand(tt.input)
|
||||
require.NoError(t, err)
|
||||
if diff := cmp.Diff(tt.expectedPatch, patch); diff != "" {
|
||||
require.FailNowf(t, "Path mismatch (-want +got):%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ func (l *LibraryElementService) GetLibraryElement(c context.Context, signedInUse
|
||||
}
|
||||
|
||||
// createLibraryElement adds a library element.
|
||||
func (l *LibraryElementService) createLibraryElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) {
|
||||
func (l *LibraryElementService) CreateElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) {
|
||||
if err := l.requireSupportedElementKind(cmd.Kind); err != nil {
|
||||
return model.LibraryElementDTO{}, err
|
||||
}
|
||||
@@ -169,7 +169,7 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn
|
||||
err = l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
|
||||
allowed, err := l.AccessControl.Evaluate(c, signedInUser, ac.EvalPermission(ActionLibraryPanelsCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID)))
|
||||
if !allowed {
|
||||
return fmt.Errorf("insufficient permissions for creating library panel in folder with UID %s", folderUID)
|
||||
return fmt.Errorf("insufficient permissions for creating library panel in folder with UID: '%s'", folderUID)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -216,7 +216,7 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn
|
||||
}
|
||||
|
||||
// deleteLibraryElement deletes a library element.
|
||||
func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedInUser identity.Requester, uid string) (int64, error) {
|
||||
func (l *LibraryElementService) DeleteLibraryElement(c context.Context, signedInUser identity.Requester, uid string) (int64, error) {
|
||||
var elementID int64
|
||||
err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
|
||||
element, err := l.GetLibraryElement(c, signedInUser, session, uid)
|
||||
@@ -578,7 +578,7 @@ func (l *LibraryElementService) handleFolderIDPatches(ctx context.Context, eleme
|
||||
}
|
||||
|
||||
// patchLibraryElement updates a Library Element.
|
||||
func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInUser identity.Requester, cmd model.PatchLibraryElementCommand, uid string) (model.LibraryElementDTO, error) {
|
||||
func (l *LibraryElementService) PatchLibraryElement(c context.Context, signedInUser identity.Requester, cmd model.PatchLibraryElementCommand, uid string) (model.LibraryElementDTO, error) {
|
||||
var dto model.LibraryElementDTO
|
||||
if err := l.requireSupportedElementKind(cmd.Kind); err != nil {
|
||||
return model.LibraryElementDTO{}, err
|
||||
|
||||
@@ -17,6 +17,16 @@ type LibraryElementService struct {
|
||||
idCounter int64
|
||||
}
|
||||
|
||||
// DeleteLibraryElement implements libraryelements.Service.
|
||||
func (l *LibraryElementService) DeleteLibraryElement(c context.Context, signedInUser identity.Requester, uid string) (int64, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// PatchLibraryElement implements libraryelements.Service.
|
||||
func (l *LibraryElementService) PatchLibraryElement(c context.Context, signedInUser identity.Requester, cmd model.PatchLibraryElementCommand, uid string) (model.LibraryElementDTO, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
var _ libraryelements.Service = (*LibraryElementService)(nil)
|
||||
|
||||
func (l *LibraryElementService) CreateElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) {
|
||||
@@ -68,6 +78,11 @@ func (l *LibraryElementService) CreateElement(c context.Context, signedInUser id
|
||||
return dto, nil
|
||||
}
|
||||
|
||||
// PatchElement implements libraryelements.Service.
|
||||
func (l *LibraryElementService) PatchElement(c context.Context, signedInUser identity.Requester, cmd model.PatchLibraryElementCommand, uid string) (model.LibraryElementDTO, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (l *LibraryElementService) GetElement(c context.Context, signedInUser identity.Requester, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) {
|
||||
l.mx.RLock()
|
||||
defer l.mx.RUnlock()
|
||||
|
||||
@@ -2,13 +2,13 @@ package libraryelements
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestUnstructuredToLegacyLibraryPanelDTO(t *testing.T) {
|
||||
@@ -155,26 +154,22 @@ func TestUnstructuredToLegacyLibraryPanelDTO(t *testing.T) {
|
||||
require.Equal(t, testUser.Login, result.Meta.UpdatedBy.Name)
|
||||
require.Equal(t, dtos.GetGravatarUrl(cfg, testUser.Email), result.Meta.UpdatedBy.AvatarUrl)
|
||||
|
||||
var modelMap map[string]interface{}
|
||||
err = json.Unmarshal(result.Model, &modelMap)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "testdata", modelMap["datasource"].(map[string]interface{})["type"])
|
||||
require.Equal(t, "test-datasource", modelMap["datasource"].(map[string]interface{})["uid"])
|
||||
require.Equal(t, "Test description", modelMap["description"])
|
||||
require.Equal(t, float64(123), modelMap["id"])
|
||||
require.Equal(t, "text", modelMap["type"])
|
||||
require.Equal(t, "Test Panel Title", modelMap["title"])
|
||||
require.Equal(t, "Test content", modelMap["options"].(map[string]interface{})["content"])
|
||||
require.Equal(t, true, modelMap["transparent"])
|
||||
require.Equal(t, "Test Library Panel", modelMap["libraryPanel"].(map[string]interface{})["name"])
|
||||
require.Equal(t, "test-panel-uid", modelMap["libraryPanel"].(map[string]interface{})["uid"])
|
||||
links := modelMap["links"].([]interface{})
|
||||
require.Len(t, links, 1)
|
||||
require.Equal(t, "Test Link", links[0].(map[string]interface{})["title"])
|
||||
|
||||
targets := modelMap["targets"].([]interface{})
|
||||
require.Len(t, targets, 1)
|
||||
require.Equal(t, "A", targets[0].(map[string]interface{})["refId"])
|
||||
// fmt.Printf("%s\n", result.Model)
|
||||
require.JSONEq(t, `{
|
||||
"datasource": { "type": "testdata", "uid": "test-datasource" },
|
||||
"description": "Test description",
|
||||
"fieldConfig": { "defaults": { "color": { "mode": "palette-classic" } } },
|
||||
"gridPos": { "w": 12, "h": 8, "x": 0, "y": 0 },
|
||||
"id": 123,
|
||||
"libraryPanel": { "name": "Test Library Panel", "uid": "test-panel-uid" },
|
||||
"links": [{ "title": "Test Link", "url": "https://example.com" }],
|
||||
"options": { "content": "Test content" },
|
||||
"pluginVersion": "1.0.0",
|
||||
"targets": [{ "refId": "A", "expr": "test_query" }],
|
||||
"title": "Test Panel Title",
|
||||
"transparent": true,
|
||||
"type": "text"
|
||||
}`, string(result.Model))
|
||||
|
||||
dashboardsSvc.AssertExpectations(t)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.Rout
|
||||
// Service is a service for operating on library elements.
|
||||
type Service interface {
|
||||
CreateElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error)
|
||||
PatchLibraryElement(c context.Context, signedInUser identity.Requester, cmd model.PatchLibraryElementCommand, uid string) (model.LibraryElementDTO, error)
|
||||
DeleteLibraryElement(c context.Context, signedInUser identity.Requester, uid string) (int64, error)
|
||||
GetElement(c context.Context, signedInUser identity.Requester, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error)
|
||||
GetElementsForDashboard(c context.Context, dashboardID int64) (map[string]model.LibraryElementDTO, error)
|
||||
ConnectElementsToDashboard(c context.Context, signedInUser identity.Requester, elementUIDs []string, dashboardID int64) error
|
||||
@@ -63,11 +65,6 @@ type LibraryElementService struct {
|
||||
|
||||
var _ Service = (*LibraryElementService)(nil)
|
||||
|
||||
// CreateElement creates a Library Element.
|
||||
func (l *LibraryElementService) CreateElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) {
|
||||
return l.createLibraryElement(c, signedInUser, cmd)
|
||||
}
|
||||
|
||||
// GetElement gets an element from a UID.
|
||||
func (l *LibraryElementService) GetElement(c context.Context, signedInUser identity.Requester, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) {
|
||||
return l.getLibraryElementByUid(c, signedInUser, cmd)
|
||||
|
||||
@@ -103,7 +103,7 @@ func TestIntegration_GetLibraryElement(t *testing.T) {
|
||||
newFolder := createFolder(t, sc, "NewFolder", sc.folderSvc)
|
||||
sc.reqContext.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersRead] = []string{dashboards.ScopeFoldersAll}
|
||||
sc.reqContext.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersDelete] = []string{dashboards.ScopeFoldersAll}
|
||||
result, err := sc.service.createLibraryElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, model.CreateLibraryElementCommand{
|
||||
result, err := sc.service.CreateElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, model.CreateLibraryElementCommand{
|
||||
FolderID: newFolder.ID, // nolint:staticcheck
|
||||
FolderUID: &newFolder.UID,
|
||||
Name: "Testing Library Panel With Deleted Folder",
|
||||
|
||||
@@ -150,7 +150,7 @@ func TestIntegration_GetLibraryPanelConnections(t *testing.T) {
|
||||
newFolder := createFolder(t, sc, "NewFolder", sc.folderSvc)
|
||||
sc.reqContext.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersRead] = []string{dashboards.ScopeFoldersAll}
|
||||
sc.reqContext.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersDelete] = []string{dashboards.ScopeFoldersAll}
|
||||
_, err = sc.service.createLibraryElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, model.CreateLibraryElementCommand{
|
||||
_, err = sc.service.CreateElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, model.CreateLibraryElementCommand{
|
||||
FolderID: newFolder.ID, // nolint:staticcheck
|
||||
FolderUID: &newFolder.UID,
|
||||
Name: "Testing Library Panel With Deleted Folder",
|
||||
|
||||
Reference in New Issue
Block a user