From 941ba1d2f78a9b8a4ad9e2af9b94a3779a53e96d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 9 Dec 2020 13:10:18 +0100 Subject: [PATCH] PanelLibrary: Adds api and db to create Library/Shared/Reusable Panel (#29642) * PanelLibrary: Adds panellib table * Refactor: removes drop table migration * Refactor: fixes spelling mistake * Refactor: changes after PR comments * Refactor: some more renames * PanelLibrary: Adds api and db to create Library/Shared/Reusable Panel * Refactor: reverts SqlStore change and uses RegisterOverride instead * Refactor: fixes lint error * Refactor: fixes imports * Refactor: reverts unintentional changes * Refactor: Adds repository * Revert "Refactor: Adds repository" This reverts commit 4c46e8a6c420abe407d431a015cfd11ebee4082c. * Refactor: changes after PR comments * Refactor: Simplfies further * Chore: fixes linting * Chore: Changes after PR comments * Update pkg/services/librarypanels/api.go Co-authored-by: Arve Knudsen * Chore: fixes import after commited suggestion Co-authored-by: Arve Knudsen --- pkg/api/common.go | 7 ++ pkg/registry/registry.go | 4 + pkg/services/librarypanels/api.go | 36 +++++++ pkg/services/librarypanels/database.go | 41 +++++++ pkg/services/librarypanels/librarypanels.go | 26 +++-- .../librarypanels/librarypanels_test.go | 100 ++++++++++++++++++ pkg/services/librarypanels/models.go | 36 +++++++ 7 files changed, 240 insertions(+), 10 deletions(-) create mode 100644 pkg/services/librarypanels/api.go create mode 100644 pkg/services/librarypanels/database.go create mode 100644 pkg/services/librarypanels/librarypanels_test.go create mode 100644 pkg/services/librarypanels/models.go diff --git a/pkg/api/common.go b/pkg/api/common.go index 83063d8098c..7c88bfd45f8 100644 --- a/pkg/api/common.go +++ b/pkg/api/common.go @@ -17,6 +17,8 @@ var ( type Response interface { WriteTo(ctx *models.ReqContext) + // Status gets the response's status. + Status() int } type NormalResponse struct { @@ -41,6 +43,11 @@ func Wrap(action interface{}) macaron.Handler { } } +// Status gets the response's status. +func (r *NormalResponse) Status() int { + return r.status +} + func (r *NormalResponse) WriteTo(ctx *models.ReqContext) { if r.err != nil { ctx.Logger.Error(r.errMessage, "error", r.err, "remote_addr", ctx.RemoteAddr()) diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index dd60fa56f6e..7b6b22beabc 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -65,6 +65,10 @@ func RegisterOverride(fn OverrideServiceFunc) { overrides = append(overrides, fn) } +func ClearOverrides() { + overrides = nil +} + func getServicesWithOverrides() []*Descriptor { slice := []*Descriptor{} for _, s := range services { diff --git a/pkg/services/librarypanels/api.go b/pkg/services/librarypanels/api.go new file mode 100644 index 00000000000..3452fe18079 --- /dev/null +++ b/pkg/services/librarypanels/api.go @@ -0,0 +1,36 @@ +package librarypanels + +import ( + "errors" + + "github.com/go-macaron/binding" + "github.com/grafana/grafana/pkg/api" + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/util" +) + +func (lps *LibraryPanelService) registerAPIEndpoints() { + if !lps.IsEnabled() { + return + } + + lps.RouteRegister.Group("/api/library-panels", func(libraryPanels routing.RouteRegister) { + libraryPanels.Post("/", middleware.ReqSignedIn, binding.Bind(addLibraryPanelCommand{}), api.Wrap(lps.createHandler)) + }) +} + +// createHandler handles POST /api/library-panels. +func (lps *LibraryPanelService) createHandler(c *models.ReqContext, cmd addLibraryPanelCommand) api.Response { + panel, err := lps.createLibraryPanel(c, cmd) + + if err != nil { + if errors.Is(err, errLibraryPanelAlreadyAdded) { + return api.Error(400, errLibraryPanelAlreadyAdded.Error(), err) + } + return api.Error(500, "Failed to create library panel", err) + } + + return api.JSON(200, util.DynMap{"panel": panel}) +} diff --git a/pkg/services/librarypanels/database.go b/pkg/services/librarypanels/database.go new file mode 100644 index 00000000000..659ed86c886 --- /dev/null +++ b/pkg/services/librarypanels/database.go @@ -0,0 +1,41 @@ +package librarypanels + +import ( + "context" + "time" + + "github.com/grafana/grafana/pkg/models" + + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +// createLibraryPanel function adds a LibraryPanel +func (lps *LibraryPanelService) createLibraryPanel(c *models.ReqContext, cmd addLibraryPanelCommand) (LibraryPanel, error) { + libraryPanel := LibraryPanel{ + OrgID: c.SignedInUser.OrgId, + FolderID: cmd.FolderID, + Title: cmd.Title, + Model: cmd.Model, + + Created: time.Now(), + Updated: time.Now(), + + CreatedBy: c.SignedInUser.UserId, + UpdatedBy: c.SignedInUser.UserId, + } + + err := lps.SQLStore.WithTransactionalDbSession(context.Background(), func(session *sqlstore.DBSession) error { + if res, err := session.Query("SELECT 1 from library_panel WHERE org_id=? and folder_id=? and title=?", c.SignedInUser.OrgId, cmd.FolderID, cmd.Title); err != nil { + return err + } else if len(res) == 1 { + return errLibraryPanelAlreadyAdded + } + + if _, err := session.Insert(&libraryPanel); err != nil { + return err + } + return nil + }) + + return libraryPanel, err +} diff --git a/pkg/services/librarypanels/librarypanels.go b/pkg/services/librarypanels/librarypanels.go index d48132444e1..3c6821d0017 100644 --- a/pkg/services/librarypanels/librarypanels.go +++ b/pkg/services/librarypanels/librarypanels.go @@ -1,16 +1,20 @@ package librarypanels import ( + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/setting" ) // LibraryPanelService is the service for the Panel Library feature. type LibraryPanelService struct { - Cfg *setting.Cfg `inject:""` - log log.Logger + Cfg *setting.Cfg `inject:""` + SQLStore *sqlstore.SQLStore `inject:""` + RouteRegister routing.RouteRegister `inject:""` + log log.Logger } func init() { @@ -18,25 +22,27 @@ func init() { } // Init initializes the LibraryPanel service -func (pl *LibraryPanelService) Init() error { - pl.log = log.New("library_panel") +func (lps *LibraryPanelService) Init() error { + lps.log = log.New("librarypanels") + + lps.registerAPIEndpoints() return nil } // IsEnabled returns true if the Panel Library feature is enabled for this instance. -func (pl *LibraryPanelService) IsEnabled() bool { - if pl.Cfg == nil { +func (lps *LibraryPanelService) IsEnabled() bool { + if lps.Cfg == nil { return false } - return pl.Cfg.IsPanelLibraryEnabled() + return lps.Cfg.IsPanelLibraryEnabled() } // AddMigration defines database migrations. // If Panel Library is not enabled does nothing. -func (pl *LibraryPanelService) AddMigration(mg *migrator.Migrator) { - if !pl.IsEnabled() { +func (lps *LibraryPanelService) AddMigration(mg *migrator.Migrator) { + if !lps.IsEnabled() { return } @@ -47,7 +53,7 @@ func (pl *LibraryPanelService) AddMigration(mg *migrator.Migrator) { {Name: "org_id", Type: migrator.DB_BigInt, Nullable: false}, {Name: "folder_id", Type: migrator.DB_BigInt, Nullable: false}, {Name: "title", Type: migrator.DB_NVarchar, Length: 255, Nullable: false}, - {Name: "data", Type: migrator.DB_Text, Nullable: false}, + {Name: "model", Type: migrator.DB_Text, Nullable: false}, {Name: "created", Type: migrator.DB_DateTime, Nullable: false}, {Name: "created_by", Type: migrator.DB_BigInt, Nullable: false}, {Name: "updated", Type: migrator.DB_DateTime, Nullable: false}, diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go new file mode 100644 index 00000000000..177f03cfe42 --- /dev/null +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -0,0 +1,100 @@ +package librarypanels + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" +) + +func TestCreateLibraryPanel(t *testing.T) { + t.Run("should fail if library panel already exists", func(t *testing.T) { + lps, context := setupTestEnv(t, models.ROLE_EDITOR) + command := addLibraryPanelCommand{ + FolderID: 1, + Title: "Text - Library Panel", + Model: []byte(` + { + "datasource": "${DS_GDEV-TESTDATA}", + "id": 1, + "title": "Text - Library Panel", + "type": "text" + } +`), + } + + response := lps.createHandler(&context, command) + require.Equal(t, 200, response.Status()) + + response = lps.createHandler(&context, command) + require.Equal(t, 400, response.Status()) + + t.Cleanup(registry.ClearOverrides) + }) +} + +func setupMigrations(cfg *setting.Cfg) LibraryPanelService { + lps := LibraryPanelService{ + SQLStore: nil, + Cfg: cfg, + } + + overrideServiceFunc := func(d registry.Descriptor) (*registry.Descriptor, bool) { + descriptor := registry.Descriptor{ + Name: "LibraryPanelService", + Instance: &lps, + InitPriority: 0, + } + + return &descriptor, true + } + + registry.RegisterOverride(overrideServiceFunc) + + return lps +} + +func setupTestEnv(t *testing.T, orgRole models.RoleType) (LibraryPanelService, models.ReqContext) { + cfg := setting.NewCfg() + cfg.FeatureToggles = map[string]bool{"panelLibrary": true} + + service := setupMigrations(cfg) + + sqlStore := sqlstore.InitTestDB(t) + service.SQLStore = sqlStore + + user := models.SignedInUser{ + UserId: 1, + OrgId: 1, + OrgName: "", + OrgRole: orgRole, + Login: "", + Name: "", + Email: "", + ApiKeyId: 0, + OrgCount: 0, + IsGrafanaAdmin: false, + IsAnonymous: false, + HelpFlags1: 0, + LastSeenAt: time.Now(), + Teams: nil, + } + + context := models.ReqContext{ + Context: nil, + SignedInUser: &user, + UserToken: nil, + IsSignedIn: false, + IsRenderCall: false, + AllowAnonymous: false, + SkipCache: false, + Logger: nil, + } + + return service, context +} diff --git a/pkg/services/librarypanels/models.go b/pkg/services/librarypanels/models.go new file mode 100644 index 00000000000..3bbc69cc778 --- /dev/null +++ b/pkg/services/librarypanels/models.go @@ -0,0 +1,36 @@ +package librarypanels + +import ( + "encoding/json" + "fmt" + "time" +) + +// LibraryPanel is the model for library panel definitions. +type LibraryPanel struct { + ID int64 `xorm:"pk autoincr 'id'"` + OrgID int64 `xorm:"org_id"` + FolderID int64 `xorm:"folder_id"` + Title string + Model json.RawMessage + + Created time.Time + Updated time.Time + + CreatedBy int64 + UpdatedBy int64 +} + +var ( + // errLibraryPanelAlreadyAdded is an error when you add a library panel that already exists. + errLibraryPanelAlreadyAdded = fmt.Errorf("library panel with that title already exists") +) + +// Commands + +// addLibraryPanelCommand is the command for adding a LibraryPanel +type addLibraryPanelCommand struct { + FolderID int64 `json:"folderId"` + Title string `json:"title"` + Model json.RawMessage `json:"model"` +}