LibraryPanels: removes feature toggle (#33839)

* WIP: intial structure

* Refactor: adds create library element endpoint

* Feature: adds delete library element

* wip

* Refactor: adds get api

* Refactor: adds get all api

* Refactor: adds patch api

* Refactor: changes to library_element_connection

* Refactor: add get connections api

* wip: in the middle of refactor

* wip

* Refactor: consolidating both api:s

* Refactor: points front end to library elements api

* Tests: Fixes broken test

* LibraryPanels: removes feature toggle

* Fix: fixes delete library elements in folder and adds tests

* Tests: fixes snapshot

* Refactor: adds service interfaces so they can be easily mocked

* Refactor: changes order of tabs in manage folder

* Refactor: fixes so link does not cover whole card

* Refactor: fixes index string name

* Update pkg/services/libraryelements/libraryelements.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/services/libraryelements/libraryelements_permissions_test.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/services/libraryelements/database.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Chore: changes after PR comments

* Update libraryelements.go

* Update libraryelements.go

* Chore: updates after PR comments

* Chore: trying to fix build error

* Refactor: fixed stupid mistake

* Update libraryelements.go

* Chore: tries to fix build errors

* Refactor: trying to fix MySQL key length

* Update libraryelements.go

* Update pkg/services/libraryelements/libraryelements.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/services/librarypanels/librarypanels.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Refactor: changes after PR comments

* Refactor: changes after PR comments

* Tests: fixes tests

* Refactor: renames connections to connectedDashboards

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
This commit is contained in:
Hugo Häggmark
2021-05-12 08:48:17 +02:00
committed by GitHub
parent f61328af2d
commit 69d9f427e1
25 changed files with 477 additions and 441 deletions

View File

@@ -46,7 +46,6 @@ export interface FeatureToggles {
ngalert: boolean; ngalert: boolean;
trimDefaults: boolean; trimDefaults: boolean;
panelLibrary: boolean;
accesscontrol: boolean; accesscontrol: boolean;
/** /**

View File

@@ -55,7 +55,6 @@ export class GrafanaBootConfig implements GrafanaConfig {
featureToggles: FeatureToggles = { featureToggles: FeatureToggles = {
meta: false, meta: false,
ngalert: false, ngalert: false,
panelLibrary: false,
reportVariables: false, reportVariables: false,
accesscontrol: false, accesscontrol: false,
trimDefaults: false, trimDefaults: false,

View File

@@ -170,13 +170,12 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
// make sure db version is in sync with json model version // make sure db version is in sync with json model version
dash.Data.Set("version", dash.Version) dash.Data.Set("version", dash.Version)
if hs.Cfg.IsPanelLibraryEnabled() { // load library panels JSON for this dashboard
// load library panels JSON for this dashboard err = hs.LibraryPanelService.LoadLibraryPanelsForDashboard(c, dash)
err = hs.LibraryPanelService.LoadLibraryPanelsForDashboard(c, dash) if err != nil {
if err != nil { return response.Error(500, "Error while loading library panels", err)
return response.Error(500, "Error while loading library panels", err)
}
} }
var trimedJson simplejson.Json var trimedJson simplejson.Json
if trimDefaults && !hs.LoadSchemaService.IsDisabled() { if trimDefaults && !hs.LoadSchemaService.IsDisabled() {
trimedJson, err = hs.LoadSchemaService.DashboardTrimDefaults(*dash.Data) trimedJson, err = hs.LoadSchemaService.DashboardTrimDefaults(*dash.Data)
@@ -249,16 +248,14 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response {
return dashboardGuardianResponse(err) return dashboardGuardianResponse(err)
} }
if hs.Cfg.IsPanelLibraryEnabled() { // disconnect all library elements for this dashboard
// disconnect all library elements for this dashboard err := hs.LibraryElementService.DisconnectElementsFromDashboard(c, dash.Id)
err := hs.LibraryElementService.DisconnectElementsFromDashboard(c, dash.Id) if err != nil {
if err != nil { hs.log.Error("Failed to disconnect library elements", "dashboard", dash.Id, "user", c.SignedInUser.UserId, "error", err)
hs.log.Error("Failed to disconnect library elements", "dashboard", dash.Id, "user", c.SignedInUser.UserId, "error", err)
}
} }
svc := dashboards.NewService(hs.SQLStore) svc := dashboards.NewService(hs.SQLStore)
err := svc.DeleteDashboard(dash.Id, c.OrgId) err = svc.DeleteDashboard(dash.Id, c.OrgId)
if err != nil { if err != nil {
var dashboardErr models.DashboardErr var dashboardErr models.DashboardErr
if ok := errors.As(err, &dashboardErr); ok { if ok := errors.As(err, &dashboardErr); ok {
@@ -318,12 +315,10 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext, cmd models.SaveDashboa
allowUiUpdate = hs.ProvisioningService.GetAllowUIUpdatesFromConfig(provisioningData.Name) allowUiUpdate = hs.ProvisioningService.GetAllowUIUpdatesFromConfig(provisioningData.Name)
} }
if hs.Cfg.IsPanelLibraryEnabled() { // clean up all unnecessary library panels JSON properties so we store a minimum JSON
// clean up all unnecessary library panels JSON properties so we store a minimum JSON err = hs.LibraryPanelService.CleanLibraryPanelsForDashboard(dash)
err = hs.LibraryPanelService.CleanLibraryPanelsForDashboard(dash) if err != nil {
if err != nil { return response.Error(500, "Error while cleaning library panels", err)
return response.Error(500, "Error while cleaning library panels", err)
}
} }
dashItem := &dashboards.SaveDashboardDTO{ dashItem := &dashboards.SaveDashboardDTO{
@@ -373,12 +368,10 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext, cmd models.SaveDashboa
} }
} }
if hs.Cfg.IsPanelLibraryEnabled() { // connect library panels for this dashboard after the dashboard is stored and has an ID
// connect library panels for this dashboard after the dashboard is stored and has an ID err = hs.LibraryPanelService.ConnectLibraryPanelsForDashboard(c, dashboard)
err = hs.LibraryPanelService.ConnectLibraryPanelsForDashboard(c, dashboard) if err != nil {
if err != nil { return response.Error(500, "Error while connecting library panels", err)
return response.Error(500, "Error while connecting library panels", err)
}
} }
c.TimeRequest(metrics.MApiDashboardSave) c.TimeRequest(metrics.MApiDashboardSave)

View File

@@ -16,6 +16,7 @@ import (
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/quota"
@@ -172,7 +173,11 @@ func TestDashboardAPIEndpoint(t *testing.T) {
"/api/dashboards/db/:slug", role, func(sc *scenarioContext) { "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
state := setUp() state := setUp()
callDeleteDashboardBySlug(sc, &HTTPServer{Cfg: setting.NewCfg()}) callDeleteDashboardBySlug(sc, &HTTPServer{
Cfg: setting.NewCfg(),
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
})
assert.Equal(t, 403, sc.resp.Code) assert.Equal(t, 403, sc.resp.Code)
assert.Equal(t, "child-dash", state.dashQueries[0].Slug) assert.Equal(t, "child-dash", state.dashQueries[0].Slug)
@@ -182,7 +187,11 @@ func TestDashboardAPIEndpoint(t *testing.T) {
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
state := setUp() state := setUp()
callDeleteDashboardByUID(sc, &HTTPServer{Cfg: setting.NewCfg()}) callDeleteDashboardBySlug(sc, &HTTPServer{
Cfg: setting.NewCfg(),
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
})
assert.Equal(t, 403, sc.resp.Code) assert.Equal(t, 403, sc.resp.Code)
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
@@ -237,7 +246,11 @@ func TestDashboardAPIEndpoint(t *testing.T) {
"/api/dashboards/db/:slug", role, func(sc *scenarioContext) { "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
state := setUp() state := setUp()
callDeleteDashboardBySlug(sc, &HTTPServer{Cfg: setting.NewCfg()}) callDeleteDashboardBySlug(sc, &HTTPServer{
Cfg: setting.NewCfg(),
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
})
assert.Equal(t, 200, sc.resp.Code) assert.Equal(t, 200, sc.resp.Code)
assert.Equal(t, "child-dash", state.dashQueries[0].Slug) assert.Equal(t, "child-dash", state.dashQueries[0].Slug)
}) })
@@ -246,7 +259,11 @@ func TestDashboardAPIEndpoint(t *testing.T) {
"/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
state := setUp() state := setUp()
callDeleteDashboardByUID(sc, &HTTPServer{Cfg: setting.NewCfg()}) callDeleteDashboardBySlug(sc, &HTTPServer{
Cfg: setting.NewCfg(),
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
})
assert.Equal(t, 200, sc.resp.Code) assert.Equal(t, 200, sc.resp.Code)
assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid)
}) })
@@ -271,8 +288,10 @@ func TestDashboardAPIEndpoint(t *testing.T) {
t.Run("Given a dashboard with a parent folder which has an ACL", func(t *testing.T) { t.Run("Given a dashboard with a parent folder which has an ACL", func(t *testing.T) {
hs := &HTTPServer{ hs := &HTTPServer{
Cfg: setting.NewCfg(), Cfg: setting.NewCfg(),
Live: newTestLive(t), Live: newTestLive(t),
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
} }
setUp := func() *testState { setUp := func() *testState {
@@ -1031,7 +1050,11 @@ func TestDashboardAPIEndpoint(t *testing.T) {
"/api/dashboards/db/:slug", models.ROLE_EDITOR, func(sc *scenarioContext) { "/api/dashboards/db/:slug", models.ROLE_EDITOR, func(sc *scenarioContext) {
setUp() setUp()
callDeleteDashboardBySlug(sc, &HTTPServer{Cfg: setting.NewCfg()}) callDeleteDashboardBySlug(sc, &HTTPServer{
Cfg: setting.NewCfg(),
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
})
assert.Equal(t, 400, sc.resp.Code) assert.Equal(t, 400, sc.resp.Code)
result := sc.ToJSON() result := sc.ToJSON()
@@ -1041,7 +1064,11 @@ func TestDashboardAPIEndpoint(t *testing.T) {
loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/abcdefghi", "/api/dashboards/db/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) { loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/abcdefghi", "/api/dashboards/db/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) {
setUp() setUp()
callDeleteDashboardByUID(sc, &HTTPServer{Cfg: setting.NewCfg()}) callDeleteDashboardBySlug(sc, &HTTPServer{
Cfg: setting.NewCfg(),
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
})
assert.Equal(t, 400, sc.resp.Code) assert.Equal(t, 400, sc.resp.Code)
result := sc.ToJSON() result := sc.ToJSON()
@@ -1073,8 +1100,10 @@ func TestDashboardAPIEndpoint(t *testing.T) {
} }
hs := &HTTPServer{ hs := &HTTPServer{
Cfg: setting.NewCfg(), Cfg: setting.NewCfg(),
ProvisioningService: mock, ProvisioningService: mock,
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
} }
callGetDashboard(sc, hs) callGetDashboard(sc, hs)
@@ -1095,10 +1124,16 @@ func getDashboardShouldReturn200WithConfig(sc *scenarioContext, provisioningServ
provisioningService = provisioning.NewProvisioningServiceMock() provisioningService = provisioning.NewProvisioningServiceMock()
} }
libraryPanelsService := mockLibraryPanelService{}
libraryElementsService := mockLibraryElementService{}
hs := &HTTPServer{ hs := &HTTPServer{
Cfg: setting.NewCfg(), Cfg: setting.NewCfg(),
ProvisioningService: provisioningService, LibraryPanelService: &libraryPanelsService,
LibraryElementService: &libraryElementsService,
ProvisioningService: provisioningService,
} }
callGetDashboard(sc, hs) callGetDashboard(sc, hs)
require.Equal(sc.t, 200, sc.resp.Code) require.Equal(sc.t, 200, sc.resp.Code)
@@ -1185,7 +1220,9 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
QuotaService: &quota.QuotaService{ QuotaService: &quota.QuotaService{
Cfg: cfg, Cfg: cfg,
}, },
PluginManager: &fakePluginManager{}, PluginManager: &fakePluginManager{},
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
} }
sc := setupScenarioContext(t, url) sc := setupScenarioContext(t, url)
@@ -1242,11 +1279,13 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout
cfg := setting.NewCfg() cfg := setting.NewCfg()
hs := HTTPServer{ hs := HTTPServer{
Cfg: cfg, Cfg: cfg,
Bus: bus.GetBus(), Bus: bus.GetBus(),
ProvisioningService: provisioning.NewProvisioningServiceMock(), ProvisioningService: provisioning.NewProvisioningServiceMock(),
Live: newTestLive(t), Live: newTestLive(t),
QuotaService: &quota.QuotaService{Cfg: cfg}, QuotaService: &quota.QuotaService{Cfg: cfg},
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
} }
sc := setupScenarioContext(t, url) sc := setupScenarioContext(t, url)
@@ -1293,3 +1332,45 @@ func (s mockDashboardProvisioningService) GetProvisionedDashboardDataByDashboard
*models.DashboardProvisioning, error) { *models.DashboardProvisioning, error) {
return nil, nil return nil, nil
} }
type mockLibraryPanelService struct {
}
func (m *mockLibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error {
return nil
}
func (m *mockLibraryPanelService) CleanLibraryPanelsForDashboard(dash *models.Dashboard) error {
return nil
}
func (m *mockLibraryPanelService) ConnectLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error {
return nil
}
type mockLibraryElementService struct {
}
func (l *mockLibraryElementService) CreateElement(c *models.ReqContext, cmd libraryelements.CreateLibraryElementCommand) (libraryelements.LibraryElementDTO, error) {
return libraryelements.LibraryElementDTO{}, nil
}
// GetElementsForDashboard gets all connected elements for a specific dashboard.
func (l *mockLibraryElementService) GetElementsForDashboard(c *models.ReqContext, dashboardID int64) (map[string]libraryelements.LibraryElementDTO, error) {
return map[string]libraryelements.LibraryElementDTO{}, nil
}
// ConnectElementsToDashboard connects elements to a specific dashboard.
func (l *mockLibraryElementService) ConnectElementsToDashboard(c *models.ReqContext, elementUIDs []string, dashboardID int64) error {
return nil
}
// DisconnectElementsFromDashboard disconnects elements from a specific dashboard.
func (l *mockLibraryElementService) DisconnectElementsFromDashboard(c *models.ReqContext, dashboardID int64) error {
return nil
}
// DeleteLibraryElementsInFolder deletes all elements for a specific folder.
func (l *mockLibraryElementService) DeleteLibraryElementsInFolder(c *models.ReqContext, folderUID string) error {
return nil
}

View File

@@ -4,13 +4,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
@@ -88,14 +87,12 @@ func (hs *HTTPServer) UpdateFolder(c *models.ReqContext, cmd models.UpdateFolder
func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { // temporarily adding this function to HTTPServer, will be removed from HTTPServer when librarypanels featuretoggle is removed func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { // temporarily adding this function to HTTPServer, will be removed from HTTPServer when librarypanels featuretoggle is removed
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore)
if hs.Cfg.IsPanelLibraryEnabled() { err := hs.LibraryElementService.DeleteLibraryElementsInFolder(c, c.Params(":uid"))
err := hs.LibraryElementService.DeleteLibraryElementsInFolder(c, c.Params(":uid")) if err != nil {
if err != nil { if errors.Is(err, libraryelements.ErrFolderHasConnectedLibraryElements) {
if errors.Is(err, libraryelements.ErrFolderHasConnectedLibraryElements) { return response.Error(403, "Folder could not be deleted because it contains library elements in use", err)
return response.Error(403, "Folder could not be deleted because it contains library elements in use", err)
}
return ToFolderErrorResponse(err)
} }
return ToFolderErrorResponse(err)
} }
f, err := s.DeleteFolder(c.Params(":uid")) f, err := s.DeleteFolder(c.Params(":uid"))

View File

@@ -13,6 +13,9 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/librarypanels"
"github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/api/routing"
httpstatic "github.com/grafana/grafana/pkg/api/static" httpstatic "github.com/grafana/grafana/pkg/api/static"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
@@ -34,8 +37,6 @@ import (
"github.com/grafana/grafana/pkg/services/datasourceproxy" "github.com/grafana/grafana/pkg/services/datasourceproxy"
"github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/librarypanels"
"github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/live/pushhttp" "github.com/grafana/grafana/pkg/services/live/pushhttp"
"github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login"
@@ -97,13 +98,13 @@ type HTTPServer struct {
LivePushGateway *pushhttp.Gateway `inject:""` LivePushGateway *pushhttp.Gateway `inject:""`
ContextHandler *contexthandler.ContextHandler `inject:""` ContextHandler *contexthandler.ContextHandler `inject:""`
SQLStore *sqlstore.SQLStore `inject:""` SQLStore *sqlstore.SQLStore `inject:""`
LibraryPanelService *librarypanels.LibraryPanelService `inject:""`
LibraryElementService *libraryelements.LibraryElementService `inject:""`
DataService *tsdb.Service `inject:""` DataService *tsdb.Service `inject:""`
PluginDashboardService *plugindashboards.Service `inject:""` PluginDashboardService *plugindashboards.Service `inject:""`
AlertEngine *alerting.AlertEngine `inject:""` AlertEngine *alerting.AlertEngine `inject:""`
LoadSchemaService *schemaloader.SchemaLoaderService `inject:""` LoadSchemaService *schemaloader.SchemaLoaderService `inject:""`
Alertmanager *notifier.Alertmanager `inject:""` Alertmanager *notifier.Alertmanager `inject:""`
LibraryPanelService librarypanels.Service `inject:""`
LibraryElementService libraryelements.Service `inject:""`
Listener net.Listener Listener net.Listener
} }

View File

@@ -168,14 +168,12 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
Icon: "camera", Icon: "camera",
}) })
if hs.Cfg.IsPanelLibraryEnabled() { dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{ Text: "Library panels",
Text: "Library panels", Id: "library-panels",
Id: "library-panels", Url: hs.Cfg.AppSubURL + "/library-panels",
Url: hs.Cfg.AppSubURL + "/library-panels", Icon: "library-panel",
Icon: "library-panel", })
})
}
} }
navTree = append(navTree, &dtos.NavLink{ navTree = append(navTree, &dtos.NavLink{

View File

@@ -13,10 +13,6 @@ import (
) )
func (l *LibraryElementService) registerAPIEndpoints() { func (l *LibraryElementService) registerAPIEndpoints() {
if !l.IsEnabled() {
return
}
l.RouteRegister.Group("/api/library-elements", func(entities routing.RouteRegister) { l.RouteRegister.Group("/api/library-elements", func(entities routing.RouteRegister) {
entities.Post("/", middleware.ReqSignedIn, binding.Bind(CreateLibraryElementCommand{}), routing.Wrap(l.createHandler)) entities.Post("/", middleware.ReqSignedIn, binding.Bind(CreateLibraryElementCommand{}), routing.Wrap(l.createHandler))
entities.Delete("/:uid", middleware.ReqSignedIn, routing.Wrap(l.deleteHandler)) entities.Delete("/:uid", middleware.ReqSignedIn, routing.Wrap(l.deleteHandler))

View File

@@ -22,7 +22,7 @@ SELECT DISTINCT
, u1.email AS created_by_email , u1.email AS created_by_email
, u2.login AS updated_by_name , u2.login AS updated_by_name
, u2.email AS updated_by_email , u2.email AS updated_by_email
, (SELECT COUNT(connection_id) FROM ` + connectionTableName + ` WHERE library_element_id = le.id AND connection_kind=1) AS connections` , (SELECT COUNT(connection_id) FROM ` + connectionTableName + ` WHERE element_id = le.id AND kind=1) AS connected_dashboards`
fromLibraryElementDTOWithMeta = ` fromLibraryElementDTOWithMeta = `
FROM library_element AS le FROM library_element AS le
LEFT JOIN user AS u1 ON le.created_by = u1.id LEFT JOIN user AS u1 ON le.created_by = u1.id
@@ -134,9 +134,9 @@ func (l *LibraryElementService) createLibraryElement(c *models.ReqContext, cmd C
Model: element.Model, Model: element.Model,
Version: element.Version, Version: element.Version,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
Connections: 0, ConnectedDashboards: 0,
Created: element.Created, Created: element.Created,
Updated: element.Updated, Updated: element.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: element.CreatedBy, ID: element.CreatedBy,
Name: c.SignedInUser.Login, Name: c.SignedInUser.Login,
@@ -166,7 +166,7 @@ func (l *LibraryElementService) deleteLibraryElement(c *models.ReqContext, uid s
var connectionIDs []struct { var connectionIDs []struct {
ConnectionID int64 `xorm:"connection_id"` ConnectionID int64 `xorm:"connection_id"`
} }
sql := "SELECT connection_id FROM library_element_connection WHERE library_element_id=?" sql := "SELECT connection_id FROM library_element_connection WHERE element_id=?"
if err := session.SQL(sql, element.ID).Find(&connectionIDs); err != nil { if err := session.SQL(sql, element.ID).Find(&connectionIDs); err != nil {
return err return err
} else if len(connectionIDs) > 0 { } else if len(connectionIDs) > 0 {
@@ -239,11 +239,11 @@ func (l *LibraryElementService) getLibraryElement(c *models.ReqContext, uid stri
Model: libraryElement.Model, Model: libraryElement.Model,
Version: libraryElement.Version, Version: libraryElement.Version,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: libraryElement.FolderName, FolderName: libraryElement.FolderName,
FolderUID: libraryElement.FolderUID, FolderUID: libraryElement.FolderUID,
Connections: libraryElement.Connections, ConnectedDashboards: libraryElement.ConnectedDashboards,
Created: libraryElement.Created, Created: libraryElement.Created,
Updated: libraryElement.Updated, Updated: libraryElement.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: libraryElement.CreatedBy, ID: libraryElement.CreatedBy,
Name: libraryElement.CreatedByName, Name: libraryElement.CreatedByName,
@@ -332,11 +332,11 @@ func (l *LibraryElementService) getAllLibraryElements(c *models.ReqContext, quer
Model: element.Model, Model: element.Model,
Version: element.Version, Version: element.Version,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: element.FolderName, FolderName: element.FolderName,
FolderUID: element.FolderUID, FolderUID: element.FolderUID,
Connections: element.Connections, ConnectedDashboards: element.ConnectedDashboards,
Created: element.Created, Created: element.Created,
Updated: element.Updated, Updated: element.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: element.CreatedBy, ID: element.CreatedBy,
Name: element.CreatedByName, Name: element.CreatedByName,
@@ -467,9 +467,9 @@ func (l *LibraryElementService) patchLibraryElement(c *models.ReqContext, cmd pa
Model: libraryElement.Model, Model: libraryElement.Model,
Version: libraryElement.Version, Version: libraryElement.Version,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
Connections: elementInDB.Connections, ConnectedDashboards: elementInDB.ConnectedDashboards,
Created: libraryElement.Created, Created: libraryElement.Created,
Updated: libraryElement.Updated, Updated: libraryElement.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: elementInDB.CreatedBy, ID: elementInDB.CreatedBy,
Name: elementInDB.CreatedByName, Name: elementInDB.CreatedByName,
@@ -503,7 +503,7 @@ func (l *LibraryElementService) getConnections(c *models.ReqContext, uid string)
builder.Write(" FROM " + connectionTableName + " AS lec") builder.Write(" FROM " + connectionTableName + " AS lec")
builder.Write(" LEFT JOIN user AS u1 ON lec.created_by = u1.id") builder.Write(" LEFT JOIN user AS u1 ON lec.created_by = u1.id")
builder.Write(" INNER JOIN dashboard AS dashboard on lec.connection_id = dashboard.id") builder.Write(" INNER JOIN dashboard AS dashboard on lec.connection_id = dashboard.id")
builder.Write(` WHERE lec.library_element_id=?`, element.ID) builder.Write(` WHERE lec.element_id=?`, element.ID)
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)
} }
@@ -514,8 +514,8 @@ func (l *LibraryElementService) getConnections(c *models.ReqContext, uid string)
for _, connection := range libraryElementConnections { for _, connection := range libraryElementConnections {
connections = append(connections, LibraryElementConnectionDTO{ connections = append(connections, LibraryElementConnectionDTO{
ID: connection.ID, ID: connection.ID,
Kind: connection.ConnectionKind, Kind: connection.Kind,
ElementID: connection.LibraryElementID, ElementID: connection.ElementID,
ConnectionID: connection.ConnectionID, ConnectionID: connection.ConnectionID,
Created: connection.Created, Created: connection.Created,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
@@ -542,7 +542,7 @@ func (l *LibraryElementService) getElementsForDashboardID(c *models.ReqContext,
", coalesce(dashboard.uid, '') AS folder_uid" + ", coalesce(dashboard.uid, '') AS folder_uid" +
fromLibraryElementDTOWithMeta + fromLibraryElementDTOWithMeta +
" LEFT JOIN dashboard AS dashboard ON dashboard.id = le.folder_id" + " LEFT JOIN dashboard AS dashboard ON dashboard.id = le.folder_id" +
" INNER JOIN " + connectionTableName + " AS lce ON lce.library_element_id = le.id AND lce.connection_kind=1 AND lce.connection_id=?" " INNER JOIN " + connectionTableName + " AS lce ON lce.element_id = le.id AND lce.kind=1 AND lce.connection_id=?"
sess := session.SQL(sql, dashboardID) sess := session.SQL(sql, dashboardID)
err := sess.Find(&libraryElements) err := sess.Find(&libraryElements)
if err != nil { if err != nil {
@@ -562,11 +562,11 @@ func (l *LibraryElementService) getElementsForDashboardID(c *models.ReqContext,
Model: element.Model, Model: element.Model,
Version: element.Version, Version: element.Version,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: element.FolderName, FolderName: element.FolderName,
FolderUID: element.FolderUID, FolderUID: element.FolderUID,
Connections: element.Connections, ConnectedDashboards: element.ConnectedDashboards,
Created: element.Created, Created: element.Created,
Updated: element.Updated, Updated: element.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: element.CreatedBy, ID: element.CreatedBy,
Name: element.CreatedByName, Name: element.CreatedByName,
@@ -590,7 +590,7 @@ func (l *LibraryElementService) getElementsForDashboardID(c *models.ReqContext,
// connectElementsToDashboardID adds connections for all elements Library Elements in a Dashboard. // connectElementsToDashboardID adds connections for all elements Library Elements in a Dashboard.
func (l *LibraryElementService) connectElementsToDashboardID(c *models.ReqContext, elementUIDs []string, dashboardID int64) error { func (l *LibraryElementService) connectElementsToDashboardID(c *models.ReqContext, elementUIDs []string, dashboardID int64) error {
err := l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { err := l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
_, err := session.Exec("DELETE FROM "+connectionTableName+" WHERE connection_kind=1 AND connection_id=?", dashboardID) _, err := session.Exec("DELETE FROM "+connectionTableName+" WHERE kind=1 AND connection_id=?", dashboardID)
if err != nil { if err != nil {
return err return err
} }
@@ -604,11 +604,11 @@ func (l *LibraryElementService) connectElementsToDashboardID(c *models.ReqContex
} }
connection := libraryElementConnection{ connection := libraryElementConnection{
LibraryElementID: element.ID, ElementID: element.ID,
ConnectionKind: 1, Kind: 1,
ConnectionID: dashboardID, ConnectionID: dashboardID,
Created: time.Now(), Created: time.Now(),
CreatedBy: c.SignedInUser.UserId, CreatedBy: c.SignedInUser.UserId,
} }
if _, err := session.Insert(&connection); err != nil { if _, err := session.Insert(&connection); err != nil {
if l.SQLStore.Dialect.IsUniqueConstraintViolation(err) { if l.SQLStore.Dialect.IsUniqueConstraintViolation(err) {
@@ -626,7 +626,7 @@ func (l *LibraryElementService) connectElementsToDashboardID(c *models.ReqContex
// disconnectElementsFromDashboardID deletes connections for all Library Elements in a Dashboard. // disconnectElementsFromDashboardID deletes connections for all Library Elements in a Dashboard.
func (l *LibraryElementService) disconnectElementsFromDashboardID(c *models.ReqContext, dashboardID int64) error { func (l *LibraryElementService) disconnectElementsFromDashboardID(c *models.ReqContext, dashboardID int64) error {
return l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { return l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
_, err := session.Exec("DELETE FROM "+connectionTableName+" WHERE connection_kind=1 AND connection_id=?", dashboardID) _, err := session.Exec("DELETE FROM "+connectionTableName+" WHERE kind=1 AND connection_id=?", dashboardID)
if err != nil { if err != nil {
return err return err
} }
@@ -656,7 +656,7 @@ func (l *LibraryElementService) deleteLibraryElementsInFolderUID(c *models.ReqCo
ConnectionID int64 `xorm:"connection_id"` ConnectionID int64 `xorm:"connection_id"`
} }
sql := "SELECT lec.connection_id FROM library_element AS le" sql := "SELECT lec.connection_id FROM library_element AS le"
sql += " INNER JOIN " + connectionTableName + " AS lec on le.id = lec.library_element_id" sql += " INNER JOIN " + connectionTableName + " AS lec on le.id = lec.element_id"
sql += " WHERE le.folder_id=? AND le.org_id=?" sql += " WHERE le.folder_id=? AND le.org_id=?"
err = session.SQL(sql, folderID, c.SignedInUser.OrgId).Find(&connectionIDs) err = session.SQL(sql, folderID, c.SignedInUser.OrgId).Find(&connectionIDs)
if err != nil { if err != nil {
@@ -674,7 +674,7 @@ func (l *LibraryElementService) deleteLibraryElementsInFolderUID(c *models.ReqCo
return err return err
} }
for _, elementID := range elementIDs { for _, elementID := range elementIDs {
_, err := session.Exec("DELETE FROM "+connectionTableName+" WHERE library_element_id=?", elementID.ID) _, err := session.Exec("DELETE FROM "+connectionTableName+" WHERE element_id=?", elementID.ID)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -10,6 +10,15 @@ import (
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
// Service is a service for operating on library elements.
type Service interface {
CreateElement(c *models.ReqContext, cmd CreateLibraryElementCommand) (LibraryElementDTO, error)
GetElementsForDashboard(c *models.ReqContext, dashboardID int64) (map[string]LibraryElementDTO, error)
ConnectElementsToDashboard(c *models.ReqContext, elementUIDs []string, dashboardID int64) error
DisconnectElementsFromDashboard(c *models.ReqContext, dashboardID int64) error
DeleteLibraryElementsInFolder(c *models.ReqContext, folderUID string) error
}
// LibraryElementService is the service for the Library Element feature. // LibraryElementService is the service for the Library Element feature.
type LibraryElementService struct { type LibraryElementService struct {
Cfg *setting.Cfg `inject:""` Cfg *setting.Cfg `inject:""`
@@ -33,66 +42,34 @@ func (l *LibraryElementService) Init() error {
return nil return nil
} }
// IsEnabled returns true if the Panel Library feature is enabled for this instance.
func (l *LibraryElementService) IsEnabled() bool {
if l.Cfg == nil {
return false
}
return l.Cfg.IsPanelLibraryEnabled()
}
// CreateElement creates a Library Element. // CreateElement creates a Library Element.
func (l *LibraryElementService) CreateElement(c *models.ReqContext, cmd CreateLibraryElementCommand) (LibraryElementDTO, error) { func (l *LibraryElementService) CreateElement(c *models.ReqContext, cmd CreateLibraryElementCommand) (LibraryElementDTO, error) {
if !l.IsEnabled() {
return LibraryElementDTO{}, nil
}
return l.createLibraryElement(c, cmd) return l.createLibraryElement(c, cmd)
} }
// GetElementsForDashboard gets all connected elements for a specific dashboard. // GetElementsForDashboard gets all connected elements for a specific dashboard.
func (l *LibraryElementService) GetElementsForDashboard(c *models.ReqContext, dashboardID int64) (map[string]LibraryElementDTO, error) { func (l *LibraryElementService) GetElementsForDashboard(c *models.ReqContext, dashboardID int64) (map[string]LibraryElementDTO, error) {
if !l.IsEnabled() {
return map[string]LibraryElementDTO{}, nil
}
return l.getElementsForDashboardID(c, dashboardID) return l.getElementsForDashboardID(c, dashboardID)
} }
// ConnectElementsToDashboard connects elements to a specific dashboard. // ConnectElementsToDashboard connects elements to a specific dashboard.
func (l *LibraryElementService) ConnectElementsToDashboard(c *models.ReqContext, elementUIDs []string, dashboardID int64) error { func (l *LibraryElementService) ConnectElementsToDashboard(c *models.ReqContext, elementUIDs []string, dashboardID int64) error {
if !l.IsEnabled() {
return nil
}
return l.connectElementsToDashboardID(c, elementUIDs, dashboardID) return l.connectElementsToDashboardID(c, elementUIDs, dashboardID)
} }
// DisconnectElementsFromDashboard disconnects elements from a specific dashboard. // DisconnectElementsFromDashboard disconnects elements from a specific dashboard.
func (l *LibraryElementService) DisconnectElementsFromDashboard(c *models.ReqContext, dashboardID int64) error { func (l *LibraryElementService) DisconnectElementsFromDashboard(c *models.ReqContext, dashboardID int64) error {
if !l.IsEnabled() {
return nil
}
return l.disconnectElementsFromDashboardID(c, dashboardID) return l.disconnectElementsFromDashboardID(c, dashboardID)
} }
// DeleteLibraryElementsInFolder deletes all elements for a specific folder. // DeleteLibraryElementsInFolder deletes all elements for a specific folder.
func (l *LibraryElementService) DeleteLibraryElementsInFolder(c *models.ReqContext, folderUID string) error { func (l *LibraryElementService) DeleteLibraryElementsInFolder(c *models.ReqContext, folderUID string) error {
if !l.IsEnabled() {
return nil
}
return l.deleteLibraryElementsInFolderUID(c, folderUID) return l.deleteLibraryElementsInFolderUID(c, folderUID)
} }
// 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 (l *LibraryElementService) AddMigration(mg *migrator.Migrator) { func (l *LibraryElementService) AddMigration(mg *migrator.Migrator) {
if !l.IsEnabled() {
return
}
libraryElementsV1 := migrator.Table{ libraryElementsV1 := migrator.Table{
Name: "library_element", Name: "library_element",
Columns: []*migrator.Column{ Columns: []*migrator.Column{
@@ -100,7 +77,7 @@ func (l *LibraryElementService) 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: "uid", Type: migrator.DB_NVarchar, Length: 40, Nullable: false}, {Name: "uid", Type: migrator.DB_NVarchar, Length: 40, Nullable: false},
{Name: "name", Type: migrator.DB_NVarchar, Length: 255, Nullable: false}, {Name: "name", Type: migrator.DB_NVarchar, Length: 150, Nullable: false},
{Name: "kind", Type: migrator.DB_BigInt, Nullable: false}, {Name: "kind", Type: migrator.DB_BigInt, Nullable: false},
{Name: "type", Type: migrator.DB_NVarchar, Length: 40, Nullable: false}, {Name: "type", Type: migrator.DB_NVarchar, Length: 40, Nullable: false},
{Name: "description", Type: migrator.DB_NVarchar, Length: 255, Nullable: false}, {Name: "description", Type: migrator.DB_NVarchar, Length: 255, Nullable: false},
@@ -117,23 +94,23 @@ func (l *LibraryElementService) AddMigration(mg *migrator.Migrator) {
} }
mg.AddMigration("create library_element table v1", migrator.NewAddTableMigration(libraryElementsV1)) mg.AddMigration("create library_element table v1", migrator.NewAddTableMigration(libraryElementsV1))
mg.AddMigration("add index library_element", migrator.NewAddIndexMigration(libraryElementsV1, libraryElementsV1.Indices[0])) mg.AddMigration("add index library_element org_id-folder_id-name-kind", migrator.NewAddIndexMigration(libraryElementsV1, libraryElementsV1.Indices[0]))
libraryElementConnectionV1 := migrator.Table{ libraryElementConnectionV1 := migrator.Table{
Name: connectionTableName, Name: connectionTableName,
Columns: []*migrator.Column{ Columns: []*migrator.Column{
{Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, {Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "library_element_id", Type: migrator.DB_BigInt, Nullable: false}, {Name: "element_id", Type: migrator.DB_BigInt, Nullable: false},
{Name: "connection_kind", Type: migrator.DB_BigInt, Nullable: false}, {Name: "kind", Type: migrator.DB_BigInt, Nullable: false},
{Name: "connection_id", Type: migrator.DB_BigInt, Nullable: false}, {Name: "connection_id", Type: migrator.DB_BigInt, 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},
}, },
Indices: []*migrator.Index{ Indices: []*migrator.Index{
{Cols: []string{"library_element_id", "connection_kind", "connection_id"}, Type: migrator.UniqueIndex}, {Cols: []string{"element_id", "kind", "connection_id"}, Type: migrator.UniqueIndex},
}, },
} }
mg.AddMigration("create "+connectionTableName+" table v1", migrator.NewAddTableMigration(libraryElementConnectionV1)) mg.AddMigration("create "+connectionTableName+" table v1", migrator.NewAddTableMigration(libraryElementConnectionV1))
mg.AddMigration("add index "+connectionTableName, migrator.NewAddIndexMigration(libraryElementConnectionV1, libraryElementConnectionV1.Indices[0])) mg.AddMigration("add index "+connectionTableName+" element_id-kind-connection_id", migrator.NewAddIndexMigration(libraryElementConnectionV1, libraryElementConnectionV1.Indices[0]))
} }

View File

@@ -36,9 +36,9 @@ func TestCreateLibraryElement(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
Connections: 0, ConnectedDashboards: 0,
Created: sc.initialResult.Result.Meta.Created, Created: sc.initialResult.Result.Meta.Created,
Updated: sc.initialResult.Result.Meta.Updated, Updated: sc.initialResult.Result.Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: "signed_in_user", Name: "signed_in_user",
@@ -81,9 +81,9 @@ func TestCreateLibraryElement(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Meta.Created, Created: result.Result.Meta.Created,
Updated: result.Result.Meta.Updated, Updated: result.Result.Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: "signed_in_user", Name: "signed_in_user",

View File

@@ -74,11 +74,11 @@ func TestGetAllLibraryElements(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder", FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid, FolderUID: sc.folder.Uid,
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Elements[0].Meta.Created, Created: result.Result.Elements[0].Meta.Created,
Updated: result.Result.Elements[0].Meta.Updated, Updated: result.Result.Elements[0].Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,
@@ -138,11 +138,11 @@ func TestGetAllLibraryElements(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder", FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid, FolderUID: sc.folder.Uid,
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Elements[0].Meta.Created, Created: result.Result.Elements[0].Meta.Created,
Updated: result.Result.Elements[0].Meta.Updated, Updated: result.Result.Elements[0].Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,
@@ -199,11 +199,11 @@ func TestGetAllLibraryElements(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder", FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid, FolderUID: sc.folder.Uid,
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Elements[0].Meta.Created, Created: result.Result.Elements[0].Meta.Created,
Updated: result.Result.Elements[0].Meta.Updated, Updated: result.Result.Elements[0].Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,
@@ -234,11 +234,11 @@ func TestGetAllLibraryElements(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder", FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid, FolderUID: sc.folder.Uid,
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Elements[1].Meta.Created, Created: result.Result.Elements[1].Meta.Created,
Updated: result.Result.Elements[1].Meta.Updated, Updated: result.Result.Elements[1].Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,
@@ -298,11 +298,11 @@ func TestGetAllLibraryElements(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder", FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid, FolderUID: sc.folder.Uid,
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Elements[0].Meta.Created, Created: result.Result.Elements[0].Meta.Created,
Updated: result.Result.Elements[0].Meta.Updated, Updated: result.Result.Elements[0].Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,
@@ -333,11 +333,11 @@ func TestGetAllLibraryElements(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder", FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid, FolderUID: sc.folder.Uid,
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Elements[1].Meta.Created, Created: result.Result.Elements[1].Meta.Created,
Updated: result.Result.Elements[1].Meta.Updated, Updated: result.Result.Elements[1].Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,
@@ -417,11 +417,11 @@ func TestGetAllLibraryElements(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder", FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid, FolderUID: sc.folder.Uid,
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Elements[0].Meta.Created, Created: result.Result.Elements[0].Meta.Created,
Updated: result.Result.Elements[0].Meta.Updated, Updated: result.Result.Elements[0].Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,
@@ -452,11 +452,11 @@ func TestGetAllLibraryElements(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder", FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid, FolderUID: sc.folder.Uid,
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Elements[1].Meta.Created, Created: result.Result.Elements[1].Meta.Created,
Updated: result.Result.Elements[1].Meta.Updated, Updated: result.Result.Elements[1].Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,
@@ -554,11 +554,11 @@ func TestGetAllLibraryElements(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: "NewFolder", FolderName: "NewFolder",
FolderUID: newFolder.Uid, FolderUID: newFolder.Uid,
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Elements[0].Meta.Created, Created: result.Result.Elements[0].Meta.Created,
Updated: result.Result.Elements[0].Meta.Updated, Updated: result.Result.Elements[0].Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,
@@ -649,11 +649,11 @@ func TestGetAllLibraryElements(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder", FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid, FolderUID: sc.folder.Uid,
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Elements[0].Meta.Created, Created: result.Result.Elements[0].Meta.Created,
Updated: result.Result.Elements[0].Meta.Updated, Updated: result.Result.Elements[0].Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,
@@ -684,11 +684,11 @@ func TestGetAllLibraryElements(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder", FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid, FolderUID: sc.folder.Uid,
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Elements[1].Meta.Created, Created: result.Result.Elements[1].Meta.Created,
Updated: result.Result.Elements[1].Meta.Updated, Updated: result.Result.Elements[1].Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,
@@ -748,11 +748,11 @@ func TestGetAllLibraryElements(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder", FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid, FolderUID: sc.folder.Uid,
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Elements[0].Meta.Created, Created: result.Result.Elements[0].Meta.Created,
Updated: result.Result.Elements[0].Meta.Updated, Updated: result.Result.Elements[0].Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,
@@ -812,11 +812,11 @@ func TestGetAllLibraryElements(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder", FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid, FolderUID: sc.folder.Uid,
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Elements[0].Meta.Created, Created: result.Result.Elements[0].Meta.Created,
Updated: result.Result.Elements[0].Meta.Updated, Updated: result.Result.Elements[0].Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,
@@ -877,11 +877,11 @@ func TestGetAllLibraryElements(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder", FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid, FolderUID: sc.folder.Uid,
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Elements[0].Meta.Created, Created: result.Result.Elements[0].Meta.Created,
Updated: result.Result.Elements[0].Meta.Updated, Updated: result.Result.Elements[0].Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,
@@ -951,11 +951,11 @@ func TestGetAllLibraryElements(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder", FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid, FolderUID: sc.folder.Uid,
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Elements[0].Meta.Created, Created: result.Result.Elements[0].Meta.Created,
Updated: result.Result.Elements[0].Meta.Updated, Updated: result.Result.Elements[0].Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,
@@ -1023,11 +1023,11 @@ func TestGetAllLibraryElements(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder", FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid, FolderUID: sc.folder.Uid,
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Elements[0].Meta.Created, Created: result.Result.Elements[0].Meta.Created,
Updated: result.Result.Elements[0].Meta.Updated, Updated: result.Result.Elements[0].Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,
@@ -1058,11 +1058,11 @@ func TestGetAllLibraryElements(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder", FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid, FolderUID: sc.folder.Uid,
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Elements[1].Meta.Created, Created: result.Result.Elements[1].Meta.Created,
Updated: result.Result.Elements[1].Meta.Updated, Updated: result.Result.Elements[1].Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,
@@ -1124,11 +1124,11 @@ func TestGetAllLibraryElements(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder", FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid, FolderUID: sc.folder.Uid,
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Elements[0].Meta.Created, Created: result.Result.Elements[0].Meta.Created,
Updated: result.Result.Elements[0].Meta.Updated, Updated: result.Result.Elements[0].Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,

View File

@@ -3,6 +3,8 @@ package libraryelements
import ( import (
"testing" "testing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -41,11 +43,92 @@ func TestGetLibraryElement(t *testing.T) {
}, },
Version: 1, Version: 1,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
FolderName: "ScenarioFolder", FolderName: "ScenarioFolder",
FolderUID: sc.folder.Uid, FolderUID: sc.folder.Uid,
Connections: 0, ConnectedDashboards: 0,
Created: result.Result.Meta.Created, Created: result.Result.Meta.Created,
Updated: result.Result.Meta.Updated, 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 != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
scenarioWithPanel(t, "When an admin tries to get a connected library panel, it should succeed and return correct connected dashboards",
func(t *testing.T, sc scenarioContext) {
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
"name": sc.initialResult.Result.Name,
},
},
},
}
dash := models.Dashboard{
Title: "Testing getHandler",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id)
err := sc.service.ConnectElementsToDashboard(sc.reqContext, []string{sc.initialResult.Result.UID}, dashInDB.Id)
require.NoError(t, err)
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.getHandler(sc.reqContext)
var result = validateAndUnMarshalResponse(t, resp)
var expected = libraryElementResult{
Result: libraryElement{
ID: 1,
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{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,

View File

@@ -57,9 +57,9 @@ func TestPatchLibraryElement(t *testing.T) {
}, },
Version: 2, Version: 2,
Meta: LibraryElementDTOMeta{ Meta: LibraryElementDTOMeta{
Connections: 0, ConnectedDashboards: 0,
Created: sc.initialResult.Result.Meta.Created, Created: sc.initialResult.Result.Meta.Created,
Updated: result.Result.Meta.Updated, Updated: result.Result.Meta.Updated,
CreatedBy: LibraryElementDTOMetaUser{ CreatedBy: LibraryElementDTOMetaUser{
ID: 1, ID: 1,
Name: userInDbName, Name: userInDbName,

View File

@@ -121,27 +121,6 @@ type libraryElementsSearchResult struct {
PerPage int `json:"perPage"` PerPage int `json:"perPage"`
} }
func overrideLibraryElementServiceInRegistry(cfg *setting.Cfg) LibraryElementService {
l := LibraryElementService{
SQLStore: nil,
Cfg: cfg,
}
overrideServiceFunc := func(d registry.Descriptor) (*registry.Descriptor, bool) {
descriptor := registry.Descriptor{
Name: "LibraryElementService",
Instance: &l,
InitPriority: 0,
}
return &descriptor, true
}
registry.RegisterOverride(overrideServiceFunc)
return l
}
func getCreatePanelCommand(folderID int64, name string) CreateLibraryElementCommand { func getCreatePanelCommand(folderID int64, name string) CreateLibraryElementCommand {
command := getCreateCommandWithModel(folderID, name, Panel, []byte(` command := getCreateCommandWithModel(folderID, name, Panel, []byte(`
{ {
@@ -294,17 +273,11 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
} }
orgID := int64(1) orgID := int64(1)
role := models.ROLE_ADMIN role := models.ROLE_ADMIN
cfg := setting.NewCfg()
// Everything in this service is behind the feature toggle "panelLibrary"
cfg.FeatureToggles = map[string]bool{"panelLibrary": true}
// Because the LibraryElementService is behind a feature toggle, we need to override the service in the registry
// with a Cfg that contains the feature toggle so migrations are run properly
service := overrideLibraryElementServiceInRegistry(cfg)
// We need to assign SQLStore after the override and migrations are done
sqlStore := sqlstore.InitTestDB(t) sqlStore := sqlstore.InitTestDB(t)
service.SQLStore = sqlStore service := LibraryElementService{
Cfg: setting.NewCfg(),
SQLStore: sqlStore,
}
user := models.SignedInUser{ user := models.SignedInUser{
UserId: 1, UserId: 1,

View File

@@ -55,15 +55,15 @@ type LibraryElementWithMeta struct {
Created time.Time Created time.Time
Updated time.Time Updated time.Time
FolderName string FolderName string
FolderUID string `xorm:"folder_uid"` FolderUID string `xorm:"folder_uid"`
Connections int64 ConnectedDashboards int64
CreatedBy int64 CreatedBy int64
UpdatedBy int64 UpdatedBy int64
CreatedByName string CreatedByName string
CreatedByEmail string CreatedByEmail string
UpdatedByName string UpdatedByName string
UpdatedByEmail string UpdatedByEmail string
} }
// LibraryElementDTO is the frontend DTO for entities. // LibraryElementDTO is the frontend DTO for entities.
@@ -91,9 +91,9 @@ type LibraryElementSearchResult struct {
// LibraryElementDTOMeta is the meta information for LibraryElementDTO. // LibraryElementDTOMeta is the meta information for LibraryElementDTO.
type LibraryElementDTOMeta struct { type LibraryElementDTOMeta struct {
FolderName string `json:"folderName"` FolderName string `json:"folderName"`
FolderUID string `json:"folderUid"` FolderUID string `json:"folderUid"`
Connections int64 `json:"connections"` ConnectedDashboards int64 `json:"connectedDashboards"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
Updated time.Time `json:"updated"` Updated time.Time `json:"updated"`
@@ -111,24 +111,24 @@ type LibraryElementDTOMetaUser struct {
// libraryElementConnection is the model for library element connections. // libraryElementConnection is the model for library element connections.
type libraryElementConnection struct { type libraryElementConnection struct {
ID int64 `xorm:"pk autoincr 'id'"` ID int64 `xorm:"pk autoincr 'id'"`
LibraryElementID int64 `xorm:"library_element_id"` ElementID int64 `xorm:"element_id"`
ConnectionKind int64 `xorm:"connection_kind"` Kind int64 `xorm:"kind"`
ConnectionID int64 `xorm:"connection_id"` ConnectionID int64 `xorm:"connection_id"`
Created time.Time Created time.Time
CreatedBy int64 CreatedBy int64
} }
// libraryElementConnectionWithMeta is the model for library element connections with meta. // libraryElementConnectionWithMeta is the model for library element connections with meta.
type libraryElementConnectionWithMeta struct { type libraryElementConnectionWithMeta struct {
ID int64 `xorm:"pk autoincr 'id'"` ID int64 `xorm:"pk autoincr 'id'"`
LibraryElementID int64 `xorm:"library_element_id"` ElementID int64 `xorm:"element_id"`
ConnectionKind int64 `xorm:"connection_kind"` Kind int64 `xorm:"kind"`
ConnectionID int64 `xorm:"connection_id"` ConnectionID int64 `xorm:"connection_id"`
Created time.Time Created time.Time
CreatedBy int64 CreatedBy int64
CreatedByName string CreatedByName string
CreatedByEmail string CreatedByEmail string
} }
// LibraryElementConnectionDTO is the frontend DTO for element connections. // LibraryElementConnectionDTO is the frontend DTO for element connections.

View File

@@ -13,12 +13,19 @@ import (
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
// Service is a service for operating on library panels.
type Service interface {
LoadLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error
CleanLibraryPanelsForDashboard(dash *models.Dashboard) error
ConnectLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error
}
// 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:""`
SQLStore *sqlstore.SQLStore `inject:""` SQLStore *sqlstore.SQLStore `inject:""`
RouteRegister routing.RouteRegister `inject:""` RouteRegister routing.RouteRegister `inject:""`
LibraryElementService *libraryelements.LibraryElementService `inject:""` LibraryElementService libraryelements.Service `inject:""`
log log.Logger log log.Logger
} }
@@ -32,22 +39,9 @@ func (lps *LibraryPanelService) Init() error {
return nil return nil
} }
// IsEnabled returns true if the Panel Library feature is enabled for this instance.
func (lps *LibraryPanelService) IsEnabled() bool {
if lps.Cfg == nil {
return false
}
return lps.Cfg.IsPanelLibraryEnabled()
}
// LoadLibraryPanelsForDashboard loops through all panels in dashboard JSON and replaces any library panel JSON // LoadLibraryPanelsForDashboard loops through all panels in dashboard JSON and replaces any library panel JSON
// with JSON stored for library panel in db. // with JSON stored for library panel in db.
func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error { func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error {
if !lps.IsEnabled() {
return nil
}
elements, err := lps.LibraryElementService.GetElementsForDashboard(c, dash.Id) elements, err := lps.LibraryElementService.GetElementsForDashboard(c, dash.Id)
if err != nil { if err != nil {
return err return err
@@ -112,7 +106,7 @@ func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqConte
"meta": map[string]interface{}{ "meta": map[string]interface{}{
"folderName": elementInDB.Meta.FolderName, "folderName": elementInDB.Meta.FolderName,
"folderUid": elementInDB.Meta.FolderUID, "folderUid": elementInDB.Meta.FolderUID,
"connectedDashboards": elementInDB.Meta.Connections, "connectedDashboards": elementInDB.Meta.ConnectedDashboards,
"created": elementInDB.Meta.Created, "created": elementInDB.Meta.Created,
"updated": elementInDB.Meta.Updated, "updated": elementInDB.Meta.Updated,
"createdBy": map[string]interface{}{ "createdBy": map[string]interface{}{
@@ -135,10 +129,6 @@ func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqConte
// CleanLibraryPanelsForDashboard loops through all panels in dashboard JSON and cleans up any library panel JSON so that // CleanLibraryPanelsForDashboard loops through all panels in dashboard JSON and cleans up any library panel JSON so that
// only the necessary JSON properties remain when storing the dashboard JSON. // only the necessary JSON properties remain when storing the dashboard JSON.
func (lps *LibraryPanelService) CleanLibraryPanelsForDashboard(dash *models.Dashboard) error { func (lps *LibraryPanelService) CleanLibraryPanelsForDashboard(dash *models.Dashboard) error {
if !lps.IsEnabled() {
return nil
}
panels := dash.Data.Get("panels").MustArray() panels := dash.Data.Get("panels").MustArray()
for i, panel := range panels { for i, panel := range panels {
panelAsJSON := simplejson.NewFromAny(panel) panelAsJSON := simplejson.NewFromAny(panel)
@@ -175,10 +165,6 @@ func (lps *LibraryPanelService) CleanLibraryPanelsForDashboard(dash *models.Dash
// ConnectLibraryPanelsForDashboard loops through all panels in dashboard JSON and connects any library panels to the dashboard. // ConnectLibraryPanelsForDashboard loops through all panels in dashboard JSON and connects any library panels to the dashboard.
func (lps *LibraryPanelService) ConnectLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error { func (lps *LibraryPanelService) ConnectLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error {
if !lps.IsEnabled() {
return nil
}
panels := dash.Data.Get("panels").MustArray() panels := dash.Data.Get("panels").MustArray()
var libraryPanels []string var libraryPanels []string
for _, panel := range panels { for _, panel := range panels {

View File

@@ -589,47 +589,10 @@ type libraryPanelResult struct {
Result libraryPanel `json:"result"` Result libraryPanel `json:"result"`
} }
func overrideLibraryServicesInRegistry(cfg *setting.Cfg) (*LibraryPanelService, *libraryelements.LibraryElementService) {
les := libraryelements.LibraryElementService{
SQLStore: nil,
Cfg: cfg,
}
elementsOverride := func(d registry.Descriptor) (*registry.Descriptor, bool) {
descriptor := registry.Descriptor{
Name: "LibraryElementService",
Instance: &les,
}
return &descriptor, true
}
registry.RegisterOverride(elementsOverride)
lps := LibraryPanelService{
SQLStore: nil,
Cfg: cfg,
LibraryElementService: &les,
}
panelsOverride := func(d registry.Descriptor) (*registry.Descriptor, bool) {
descriptor := registry.Descriptor{
Name: "LibraryPanelService",
Instance: &lps,
}
return &descriptor, true
}
registry.RegisterOverride(panelsOverride)
return &lps, &les
}
type scenarioContext struct { type scenarioContext struct {
ctx *macaron.Context ctx *macaron.Context
service *LibraryPanelService service Service
elementService *libraryelements.LibraryElementService elementService libraryelements.Service
reqContext *models.ReqContext reqContext *models.ReqContext
user models.SignedInUser user models.SignedInUser
folder *models.Folder folder *models.Folder
@@ -758,20 +721,19 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
ctx := macaron.Context{ ctx := macaron.Context{
Req: macaron.Request{Request: &http.Request{}}, Req: macaron.Request{Request: &http.Request{}},
} }
cfg := setting.NewCfg()
orgID := int64(1) orgID := int64(1)
role := models.ROLE_ADMIN role := models.ROLE_ADMIN
cfg := setting.NewCfg()
// Everything in this service is behind the feature toggle "panelLibrary"
cfg.FeatureToggles = map[string]bool{"panelLibrary": true}
// Because the LibraryPanelService is behind a feature toggle, we need to override the service in the registry
// with a Cfg that contains the feature toggle so migrations are run properly
service, elementService := overrideLibraryServicesInRegistry(cfg)
// We need to assign SQLStore after the override and migrations are done
sqlStore := sqlstore.InitTestDB(t) sqlStore := sqlstore.InitTestDB(t)
elementService.SQLStore = sqlStore elementService := libraryelements.LibraryElementService{
service.SQLStore = sqlStore Cfg: cfg,
SQLStore: sqlStore,
}
service := LibraryPanelService{
Cfg: cfg,
SQLStore: sqlStore,
LibraryElementService: &elementService,
}
user := models.SignedInUser{ user := models.SignedInUser{
UserId: 1, UserId: 1,
@@ -797,8 +759,8 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
sc := scenarioContext{ sc := scenarioContext{
user: user, user: user,
ctx: &ctx, ctx: &ctx,
service: service, service: &service,
elementService: elementService, elementService: &elementService,
sqlStore: sqlStore, sqlStore: sqlStore,
reqContext: &models.ReqContext{ reqContext: &models.ReqContext{
Context: &ctx, Context: &ctx,

View File

@@ -399,11 +399,6 @@ func (cfg Cfg) IsHTTPRequestHistogramEnabled() bool {
return cfg.FeatureToggles["http_request_histogram"] return cfg.FeatureToggles["http_request_histogram"]
} }
// IsPanelLibraryEnabled returns whether the panel library feature is enabled.
func (cfg Cfg) IsPanelLibraryEnabled() bool {
return cfg.FeatureToggles["panelLibrary"]
}
type CommandLineArgs struct { type CommandLineArgs struct {
Config string Config string
HomePath string HomePath string

View File

@@ -44,8 +44,6 @@ import { PanelRenderer } from './features/panel/PanelRenderer';
import { QueryRunner } from './features/query/state/QueryRunner'; import { QueryRunner } from './features/query/state/QueryRunner';
import { getTimeSrv } from './features/dashboard/services/TimeSrv'; import { getTimeSrv } from './features/dashboard/services/TimeSrv';
import { getVariablesUrlParams } from './features/variables/getAllVariableValuesForUrl'; import { getVariablesUrlParams } from './features/variables/getAllVariableValuesForUrl';
import { SafeDynamicImport } from './core/components/DynamicImports/SafeDynamicImport';
import { featureToggledRoutes } from './routes/routes';
import getDefaultMonacoLanguages from '../lib/monaco-languages'; import getDefaultMonacoLanguages from '../lib/monaco-languages';
// add move to lodash for backward compatabilty with plugins // add move to lodash for backward compatabilty with plugins
@@ -70,21 +68,6 @@ export class GrafanaApp {
} }
init() { init() {
if (config.featureToggles.panelLibrary) {
featureToggledRoutes.push({
path: '/dashboards/f/:uid/:slug/library-panels',
component: SafeDynamicImport(
() => import(/* webpackChunkName: "FolderLibraryPanelsPage"*/ 'app/features/folders/FolderLibraryPanelsPage')
),
});
featureToggledRoutes.push({
path: '/library-panels',
component: SafeDynamicImport(
() => import(/* webpackChunkName: "LibraryPanelsPage"*/ 'app/features/library-panels/LibraryPanelsPage')
),
});
}
initEchoSrv(); initEchoSrv();
addClassIfNoOverlayScrollbar(); addClassIfNoOverlayScrollbar();
setLocale(config.bootData.user.locale); setLocale(config.bootData.user.locale);

View File

@@ -1,4 +1,5 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { connect, MapDispatchToProps } from 'react-redux';
import { css, cx, keyframes } from '@emotion/css'; import { css, cx, keyframes } from '@emotion/css';
import { chain, cloneDeep, defaults, find, sortBy } from 'lodash'; import { chain, cloneDeep, defaults, find, sortBy } from 'lodash';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
@@ -18,7 +19,6 @@ import {
LibraryPanelsSearch, LibraryPanelsSearch,
LibraryPanelsSearchVariant, LibraryPanelsSearchVariant,
} from '../../../library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch'; } from '../../../library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch';
import { connect, MapDispatchToProps } from 'react-redux';
export type PanelPluginInfo = { id: any; defaults: { gridPos: { w: any; h: any }; title: any } }; export type PanelPluginInfo = { id: any; defaults: { gridPos: { w: any; h: any }; title: any } };
@@ -154,22 +154,18 @@ export const AddPanelWidgetUnconnected: React.FC<Props> = ({ panel, dashboard })
Add a new row Add a new row
</div> </div>
</div> </div>
{(config.featureToggles.panelLibrary || copiedPanelPlugins.length === 1) && ( <div className={styles.actionsRow}>
<div className={styles.actionsRow}> <div onClick={() => setAddPanelView(true)}>
{config.featureToggles.panelLibrary && ( <Icon name="book-open" size="xl" />
<div onClick={() => setAddPanelView(true)}> Add a panel from the panel library
<Icon name="book-open" size="xl" />
Add a panel from the panel library
</div>
)}
{copiedPanelPlugins.length === 1 && (
<div onClick={() => onPasteCopiedPanel(copiedPanelPlugins[0])}>
<Icon name="clipboard-alt" size="xl" />
Paste panel from clipboard
</div>
)}
</div> </div>
)} {copiedPanelPlugins.length === 1 && (
<div onClick={() => onPasteCopiedPanel(copiedPanelPlugins[0])}>
<Icon name="clipboard-alt" size="xl" />
Paste panel from clipboard
</div>
)}
</div>
</div> </div>
)} )}
</div> </div>

View File

@@ -45,6 +45,19 @@ exports[`Render should render component 1`] = `
Add a new row Add a new row
</div> </div>
</div> </div>
<div
className="css-l02n0m"
>
<div
onClick={[Function]}
>
<Icon
name="book-open"
size="xl"
/>
Add a panel from the panel library
</div>
</div>
</div> </div>
</div> </div>
`; `;

View File

@@ -1,17 +1,16 @@
import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { GrafanaTheme, PanelPluginMeta, SelectableValue } from '@grafana/data'; import { GrafanaTheme, PanelPluginMeta, SelectableValue } from '@grafana/data';
import { Icon, Input, RadioButtonGroup, CustomScrollbar, useStyles, Button } from '@grafana/ui'; import { Button, CustomScrollbar, Icon, Input, RadioButtonGroup, useStyles } from '@grafana/ui';
import { changePanelPlugin } from '../../state/actions'; import { changePanelPlugin } from '../../state/actions';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
import { PanelModel } from '../../state/PanelModel'; import { PanelModel } from '../../state/PanelModel';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { VizTypePicker, getAllPanelPluginMeta, filterPluginList } from '../VizTypePicker/VizTypePicker'; import { filterPluginList, getAllPanelPluginMeta, VizTypePicker } from '../VizTypePicker/VizTypePicker';
import { Field } from '@grafana/ui/src/components/Forms/Field'; import { Field } from '@grafana/ui/src/components/Forms/Field';
import { PanelLibraryOptionsGroup } from 'app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup'; import { PanelLibraryOptionsGroup } from 'app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup';
import { toggleVizPicker } from './state/reducers'; import { toggleVizPicker } from './state/reducers';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { config } from 'app/core/config';
interface Props { interface Props {
panel: PanelModel; panel: PanelModel;
@@ -107,11 +106,9 @@ export const VisualizationSelectPane: FC<Props> = ({ panel }) => {
onClick={onCloseVizPicker} onClick={onCloseVizPicker}
/> />
</div> </div>
{config.featureToggles.panelLibrary && ( <Field className={styles.customFieldMargin}>
<Field className={styles.customFieldMargin}> <RadioButtonGroup options={radioOptions} value={listMode} onChange={setListMode} fullWidth />
<RadioButtonGroup options={radioOptions} value={listMode} onChange={setListMode} fullWidth /> </Field>
</Field>
)}
</div> </div>
<div className={styles.scrollWrapper}> <div className={styles.scrollWrapper}>
<CustomScrollbar autoHeightMin="100%"> <CustomScrollbar autoHeightMin="100%">

View File

@@ -1,7 +1,6 @@
import { NavModel, NavModelItem } from '@grafana/data'; import { NavModel, NavModelItem } from '@grafana/data';
import { FolderDTO } from 'app/types'; import { FolderDTO } from 'app/types';
import { getConfig } from '../../../core/config';
export function buildNavModel(folder: FolderDTO): NavModelItem { export function buildNavModel(folder: FolderDTO): NavModelItem {
const model = { const model = {
@@ -22,15 +21,13 @@ export function buildNavModel(folder: FolderDTO): NavModelItem {
], ],
}; };
if (getConfig().featureToggles.panelLibrary) { model.children.push({
model.children.push({ active: false,
active: false, icon: 'library-panel',
icon: 'library-panel', id: `folder-library-panels-${folder.uid}`,
id: `folder-library-panels-${folder.uid}`, text: 'Panels',
text: 'Panels', url: `${folder.url}/library-panels`,
url: `${folder.url}/library-panels`, });
});
}
if (folder.canAdmin) { if (folder.canAdmin) {
model.children.push({ model.children.push({

View File

@@ -11,7 +11,6 @@ import { Redirect } from 'react-router-dom';
import ErrorPage from 'app/core/components/ErrorPage/ErrorPage'; import ErrorPage from 'app/core/components/ErrorPage/ErrorPage';
export const extraRoutes: RouteDescriptor[] = []; export const extraRoutes: RouteDescriptor[] = [];
export const featureToggledRoutes: RouteDescriptor[] = [];
export function getAppRoutes(): RouteDescriptor[] { export function getAppRoutes(): RouteDescriptor[] {
return [ return [
@@ -476,8 +475,19 @@ export function getAppRoutes(): RouteDescriptor[] {
() => import(/* webpackChunkName: "BenchmarksPage"*/ 'app/features/sandbox/BenchmarksPage') () => import(/* webpackChunkName: "BenchmarksPage"*/ 'app/features/sandbox/BenchmarksPage')
), ),
}, },
{
path: '/dashboards/f/:uid/:slug/library-panels',
component: SafeDynamicImport(
() => import(/* webpackChunkName: "FolderLibraryPanelsPage"*/ 'app/features/folders/FolderLibraryPanelsPage')
),
},
{
path: '/library-panels',
component: SafeDynamicImport(
() => import(/* webpackChunkName: "LibraryPanelsPage"*/ 'app/features/library-panels/LibraryPanelsPage')
),
},
...extraRoutes, ...extraRoutes,
...featureToggledRoutes,
{ {
path: '/*', path: '/*',
component: ErrorPage, component: ErrorPage,