diff --git a/pkg/services/librarypanels/api.go b/pkg/services/librarypanels/api.go new file mode 100644 index 00000000000..832777aed84 --- /dev/null +++ b/pkg/services/librarypanels/api.go @@ -0,0 +1,32 @@ +package librarypanels + +import ( + "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.addLibraryPanelEndpoint)) + }) +} + +// addLibraryPanelEndpoint handles POST /api/library-panels. +func (lps *LibraryPanelService) addLibraryPanelEndpoint(c *models.ReqContext, cmd addLibraryPanelCommand) api.Response { + cmd.OrgId = c.SignedInUser.OrgId + cmd.SignedInUser = c.SignedInUser + + if err := lps.addLibraryPanel(&cmd); err != nil { + return api.Error(500, "Failed to create library panel", err) + } + + return api.JSON(200, util.DynMap{"id": cmd.Result.Id}) +} diff --git a/pkg/services/librarypanels/database.go b/pkg/services/librarypanels/database.go new file mode 100644 index 00000000000..0fd1dbe14d4 --- /dev/null +++ b/pkg/services/librarypanels/database.go @@ -0,0 +1,42 @@ +package librarypanels + +import ( + "context" + "time" + + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +// AddLibraryPanel function adds a LibraryPanel +func (lps *LibraryPanelService) addLibraryPanel(cmd *addLibraryPanelCommand) error { + return lps.SQLStore.WithTransactionalDbSession(context.Background(), func(session *sqlstore.DBSession) error { + libraryPanel := &LibraryPanel{ + OrgId: cmd.OrgId, + FolderId: cmd.FolderId, + Title: cmd.Title, + Model: cmd.Model, + + Created: time.Now(), + Updated: time.Now(), + + CreatedBy: cmd.SignedInUser.UserId, + UpdatedBy: cmd.SignedInUser.UserId, + } + + if res, err := session.Query("SELECT 1 from library_panel WHERE org_id=? and folder_id=? and title=?", cmd.OrgId, cmd.FolderId, cmd.Title); err != nil { + return err + } else if len(res) == 1 { + return errLibraryPanelAlreadyAdded + } + + // TODO + // check if user has rights + + if _, err := session.Insert(libraryPanel); err != nil { + return err + } + + cmd.Result = libraryPanel + return nil + }) +} diff --git a/pkg/services/librarypanels/database_test.go b/pkg/services/librarypanels/database_test.go new file mode 100644 index 00000000000..8e3ecedce55 --- /dev/null +++ b/pkg/services/librarypanels/database_test.go @@ -0,0 +1,63 @@ +package librarypanels + +import ( + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestAddLibraryPanel(t *testing.T) { + t.Run("should fail if library panel already exists", func(t *testing.T) { + lps, user := setupTestEnv(t, models.ROLE_EDITOR) + command := addLibraryPanelCommand{ + OrgId: 1, + FolderId: 1, + SignedInUser: &user, + Title: "Text - Library Panel", + Model: []byte(` + { + "datasource": "${DS_GDEV-TESTDATA}", + "id": 1, + "title": "Text - Library Panel", + "type": "text" + } +`), + } + + noErr := lps.addLibraryPanel(&command) + require.NoError(t, noErr) + + err := lps.addLibraryPanel(&command) + require.Error(t, err) + }) +} + +func setupTestEnv(t *testing.T, orgRole models.RoleType) (LibraryPanelService, models.SignedInUser) { + cfg := setting.Cfg{} + cfg.FeatureToggles = map[string]bool{"panelLibrary": true} + sqlStore := sqlstore.InitTestDBWithCfg(t, &cfg) + 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, + } + lps := LibraryPanelService{ + SQLStore: sqlStore, + Cfg: &cfg, + } + return lps, user +} diff --git a/pkg/services/librarypanels/librarypanels.go b/pkg/services/librarypanels/librarypanels.go index d48132444e1..7e4bd0cc666 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,29 @@ 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") + + if lps.IsEnabled() { + 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 { - return false +func (lps *LibraryPanelService) IsEnabled() bool { + if lps.Cfg == nil { + return true // used for test purposes only } - 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 +55,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/models.go b/pkg/services/librarypanels/models.go new file mode 100644 index 00000000000..041dabc4257 --- /dev/null +++ b/pkg/services/librarypanels/models.go @@ -0,0 +1,44 @@ +package librarypanels + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/grafana/grafana/pkg/models" +) + +// LibraryPanel is the model for library panel definitions. +type LibraryPanel struct { + Id int64 + OrgId int64 + FolderId int64 + 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") + // errLibraryPanelNotFound is an error for an unknown library panel definition. + errLibraryPanelNotFound = fmt.Errorf("could not find library panel definition") +) + +// Commands + +// addLibraryPanelCommand is the command for adding a LibraryPanel +type addLibraryPanelCommand struct { + OrgId int64 `json:"-"` + FolderId int64 `json:"folderId"` + Title string `json:"title"` + SignedInUser *models.SignedInUser `json:"-"` + Model json.RawMessage `json:"model"` + + Result *LibraryPanel +} diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index ad67867284d..0731b2f2623 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -346,6 +346,15 @@ var testSQLStore *SQLStore // InitTestDB initializes the test DB. func InitTestDB(t ITestDB) *SQLStore { + return initTestDB(t, nil) +} + +// InitTestDBWithCfg initializes the test DB with Cfg. +func InitTestDBWithCfg(t ITestDB, cfg *setting.Cfg) *SQLStore { + return initTestDB(t, nil) +} + +func initTestDB(t ITestDB, cfg *setting.Cfg) *SQLStore { t.Helper() if testSQLStore == nil { testSQLStore = &SQLStore{} @@ -362,7 +371,14 @@ func InitTestDB(t ITestDB) *SQLStore { } // set test db config - testSQLStore.Cfg = setting.NewCfg() + if cfg == nil { + testSQLStore.Cfg = setting.NewCfg() + } + + if cfg != nil { + testSQLStore.Cfg = cfg + } + sec, err := testSQLStore.Cfg.Raw.NewSection("database") if err != nil { t.Fatalf("Failed to create section: %s", err) diff --git a/public/app/features/variables/editor/getVariableQueryEditor.tsx b/public/app/features/variables/editor/getVariableQueryEditor.tsx index 5824530e5d9..22c4c1441ea 100644 --- a/public/app/features/variables/editor/getVariableQueryEditor.tsx +++ b/public/app/features/variables/editor/getVariableQueryEditor.tsx @@ -20,6 +20,11 @@ export async function getVariableQueryEditor< datasource: DataSourceApi, importDataSourcePluginFunc = importDataSourcePlugin ): Promise { + if (hasLegacyVariableSupport(datasource)) { + const dsPlugin = await importDataSourcePluginFunc(datasource.meta!); + return dsPlugin.components.VariableQueryEditor ?? LegacyVariableQueryEditor; + } + if (hasCustomVariableSupport(datasource)) { return datasource.variables.editor; } @@ -38,11 +43,6 @@ export async function getVariableQueryEditor< return StandardVariableQueryEditor; } - if (hasLegacyVariableSupport(datasource)) { - const dsPlugin = await importDataSourcePluginFunc(datasource.meta!); - return dsPlugin.components.VariableQueryEditor ?? LegacyVariableQueryEditor; - } - return null; } diff --git a/public/test/matchers/toEmitValues.test.ts b/public/test/matchers/toEmitValues.test.ts index 5106353f686..c2b5fc73751 100644 --- a/public/test/matchers/toEmitValues.test.ts +++ b/public/test/matchers/toEmitValues.test.ts @@ -137,3 +137,12 @@ describe('toEmitValues matcher', () => { }); }); }); + +describe.only('demo testing observables', () => { + it('toEmitValues demo', async () => { + const observable = of(1, 'a', 'I'); + }); + it('toEmitValuesWith demo', async () => { + const observable = of(1, 'a', 'I'); + }); +});