mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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 4c46e8a6c4.
* 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 <arve.knudsen@gmail.com>
* Chore: fixes import after commited suggestion
Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
This commit is contained in:
@@ -17,6 +17,8 @@ var (
|
|||||||
|
|
||||||
type Response interface {
|
type Response interface {
|
||||||
WriteTo(ctx *models.ReqContext)
|
WriteTo(ctx *models.ReqContext)
|
||||||
|
// Status gets the response's status.
|
||||||
|
Status() int
|
||||||
}
|
}
|
||||||
|
|
||||||
type NormalResponse struct {
|
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) {
|
func (r *NormalResponse) WriteTo(ctx *models.ReqContext) {
|
||||||
if r.err != nil {
|
if r.err != nil {
|
||||||
ctx.Logger.Error(r.errMessage, "error", r.err, "remote_addr", ctx.RemoteAddr())
|
ctx.Logger.Error(r.errMessage, "error", r.err, "remote_addr", ctx.RemoteAddr())
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ func RegisterOverride(fn OverrideServiceFunc) {
|
|||||||
overrides = append(overrides, fn)
|
overrides = append(overrides, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ClearOverrides() {
|
||||||
|
overrides = nil
|
||||||
|
}
|
||||||
|
|
||||||
func getServicesWithOverrides() []*Descriptor {
|
func getServicesWithOverrides() []*Descriptor {
|
||||||
slice := []*Descriptor{}
|
slice := []*Descriptor{}
|
||||||
for _, s := range services {
|
for _, s := range services {
|
||||||
|
|||||||
36
pkg/services/librarypanels/api.go
Normal file
36
pkg/services/librarypanels/api.go
Normal file
@@ -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})
|
||||||
|
}
|
||||||
41
pkg/services/librarypanels/database.go
Normal file
41
pkg/services/librarypanels/database.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
package librarypanels
|
package librarypanels
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/api/routing"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/registry"
|
"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/services/sqlstore/migrator"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LibraryPanelService is the service for the Panel Library feature.
|
// LibraryPanelService is the service for the Panel Library feature.
|
||||||
type LibraryPanelService struct {
|
type LibraryPanelService struct {
|
||||||
Cfg *setting.Cfg `inject:""`
|
Cfg *setting.Cfg `inject:""`
|
||||||
log log.Logger
|
SQLStore *sqlstore.SQLStore `inject:""`
|
||||||
|
RouteRegister routing.RouteRegister `inject:""`
|
||||||
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -18,25 +22,27 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the LibraryPanel service
|
// Init initializes the LibraryPanel service
|
||||||
func (pl *LibraryPanelService) Init() error {
|
func (lps *LibraryPanelService) Init() error {
|
||||||
pl.log = log.New("library_panel")
|
lps.log = log.New("librarypanels")
|
||||||
|
|
||||||
|
lps.registerAPIEndpoints()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsEnabled returns true if the Panel Library feature is enabled for this instance.
|
// IsEnabled returns true if the Panel Library feature is enabled for this instance.
|
||||||
func (pl *LibraryPanelService) IsEnabled() bool {
|
func (lps *LibraryPanelService) IsEnabled() bool {
|
||||||
if pl.Cfg == nil {
|
if lps.Cfg == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return pl.Cfg.IsPanelLibraryEnabled()
|
return lps.Cfg.IsPanelLibraryEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddMigration defines database migrations.
|
// AddMigration defines database migrations.
|
||||||
// If Panel Library is not enabled does nothing.
|
// If Panel Library is not enabled does nothing.
|
||||||
func (pl *LibraryPanelService) AddMigration(mg *migrator.Migrator) {
|
func (lps *LibraryPanelService) AddMigration(mg *migrator.Migrator) {
|
||||||
if !pl.IsEnabled() {
|
if !lps.IsEnabled() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +53,7 @@ func (pl *LibraryPanelService) AddMigration(mg *migrator.Migrator) {
|
|||||||
{Name: "org_id", Type: migrator.DB_BigInt, Nullable: false},
|
{Name: "org_id", Type: migrator.DB_BigInt, Nullable: false},
|
||||||
{Name: "folder_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: "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", Type: migrator.DB_DateTime, Nullable: false},
|
||||||
{Name: "created_by", Type: migrator.DB_BigInt, Nullable: false},
|
{Name: "created_by", Type: migrator.DB_BigInt, Nullable: false},
|
||||||
{Name: "updated", Type: migrator.DB_DateTime, Nullable: false},
|
{Name: "updated", Type: migrator.DB_DateTime, Nullable: false},
|
||||||
|
|||||||
100
pkg/services/librarypanels/librarypanels_test.go
Normal file
100
pkg/services/librarypanels/librarypanels_test.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
36
pkg/services/librarypanels/models.go
Normal file
36
pkg/services/librarypanels/models.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user