Library Panels: Modify connection api endpoint to be compatible with unified storage (#107088)
This commit is contained in:
@@ -42,6 +42,7 @@ type DashboardService interface {
|
||||
UnstructuredToLegacyDashboard(ctx context.Context, item *unstructured.Unstructured, orgID int64) (*Dashboard, error)
|
||||
ValidateDashboardRefreshInterval(minRefreshInterval string, targetRefreshInterval string) error
|
||||
ValidateBasicDashboardProperties(title string, uid string, message string) error
|
||||
GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*DashboardRef, error)
|
||||
}
|
||||
|
||||
type PermissionsRegistrationService interface {
|
||||
@@ -104,4 +105,6 @@ type Store interface {
|
||||
DeleteDashboardsInFolders(ctx context.Context, request *DeleteDashboardsInFolderRequest) error
|
||||
|
||||
GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*Dashboard, error)
|
||||
|
||||
GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*DashboardRef, error)
|
||||
}
|
||||
|
||||
@@ -94,6 +94,36 @@ func (_m *FakeDashboardService) CountDashboardsInOrg(ctx context.Context, orgID
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetDashboardsByLibraryPanelUID provides a mock function with given fields: ctx, libraryPanelUID, orgID
|
||||
func (_m *FakeDashboardService) GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*DashboardRef, error) {
|
||||
ret := _m.Called(ctx, libraryPanelUID, orgID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetDashboardsByLibraryPanelUID")
|
||||
}
|
||||
|
||||
var r0 []*DashboardRef
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, int64) ([]*DashboardRef, error)); ok {
|
||||
return rf(ctx, libraryPanelUID, orgID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, int64) []*DashboardRef); ok {
|
||||
r0 = rf(ctx, libraryPanelUID, orgID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*DashboardRef)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok {
|
||||
r1 = rf(ctx, libraryPanelUID, orgID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CountInFolders provides a mock function with given fields: ctx, orgID, folderUIDs, user
|
||||
func (_m *FakeDashboardService) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) (int64, error) {
|
||||
ret := _m.Called(ctx, orgID, folderUIDs, user)
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements/model"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
||||
@@ -70,6 +71,33 @@ func (d *dashboardStore) emitEntityEvent() bool {
|
||||
return d.features != nil && d.features.IsEnabledGlobally(featuremgmt.FlagPanelTitleSearch)
|
||||
}
|
||||
|
||||
func (d *dashboardStore) GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*dashboards.DashboardRef, error) {
|
||||
ctx, span := tracer.Start(ctx, "dashboards.database.GetDashboardsByLibraryPanelUID")
|
||||
defer span.End()
|
||||
|
||||
connectedDashboards := make([]*dashboards.DashboardRef, 0)
|
||||
recursiveQueriesAreSupported, err := d.store.RecursiveQueriesAreSupported()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = d.store.WithDbSession(ctx, func(session *db.Session) error {
|
||||
builder := db.NewSqlBuilder(d.cfg, d.features, d.store.GetDialect(), recursiveQueriesAreSupported)
|
||||
builder.Write("SELECT d.*")
|
||||
builder.Write(" FROM " + model.LibraryElementConnectionTableName + " AS lec")
|
||||
builder.Write(" INNER JOIN " + model.LibraryElementTableName + " AS le ON lec.element_id = le.id")
|
||||
builder.Write(" INNER JOIN dashboard AS d ON lec.connection_id = d.id")
|
||||
builder.Write(` WHERE le.uid=? AND le.org_id=?`, libraryPanelUID, orgID)
|
||||
if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&connectedDashboards); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return connectedDashboards, err
|
||||
}
|
||||
|
||||
func (d *dashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dash *dashboards.Dashboard, overwrite bool) (bool, error) {
|
||||
ctx, span := tracer.Start(ctx, "dashboards.database.ValidateDashboardBeforesave")
|
||||
defer span.End()
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||
libmodel "github.com/grafana/grafana/pkg/services/libraryelements/model"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/search/model"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
@@ -1145,6 +1146,94 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationGetDashboardsByLibraryPanelUID(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
sqlStore, cfg := db.InitTestDBWithCfg(t)
|
||||
dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore))
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("Should return dashboards connected to a library panel", func(t *testing.T) {
|
||||
libraryElement := insertTestLibraryElement(t, sqlStore, "test-library-panel", 1, "", "Test Library Panel")
|
||||
dash1 := insertTestDashboard(t, dashboardStore, "Dashboard 1", 1, 0, "", false, "prod")
|
||||
dash2 := insertTestDashboard(t, dashboardStore, "Dashboard 2", 1, 0, "", false, "webapp")
|
||||
dash3 := insertTestDashboard(t, dashboardStore, "Dashboard 3", 1, 0, "", false, "backend")
|
||||
// connect all but the last one
|
||||
insertTestLibraryElementConnection(t, sqlStore, libraryElement.ID, dash1.ID)
|
||||
insertTestLibraryElementConnection(t, sqlStore, libraryElement.ID, dash2.ID)
|
||||
|
||||
connectedDashboards, err := dashboardStore.GetDashboardsByLibraryPanelUID(context.Background(), libraryElement.UID, 1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, connectedDashboards, 2)
|
||||
|
||||
uids := []string{connectedDashboards[0].UID, connectedDashboards[1].UID}
|
||||
require.Contains(t, uids, dash1.UID)
|
||||
require.Contains(t, uids, dash2.UID)
|
||||
require.NotContains(t, uids, dash3.UID)
|
||||
})
|
||||
|
||||
t.Run("Returns nothing when library panel has no connections", func(t *testing.T) {
|
||||
libraryElement := insertTestLibraryElement(t, sqlStore, "empty-library-panel", 1, "", "Empty Library Panel")
|
||||
connectedDashboards, err := dashboardStore.GetDashboardsByLibraryPanelUID(context.Background(), libraryElement.UID, 1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, connectedDashboards, 0)
|
||||
})
|
||||
|
||||
t.Run("Returns nothing when library panel does not exist", func(t *testing.T) {
|
||||
connectedDashboards, err := dashboardStore.GetDashboardsByLibraryPanelUID(context.Background(), "non-existent-uid", 1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, connectedDashboards, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func insertTestLibraryElement(t *testing.T, sqlStore db.DB, uid string, orgID int64, folderUID string, name string) *libmodel.LibraryElement {
|
||||
t.Helper()
|
||||
|
||||
element := &libmodel.LibraryElement{
|
||||
OrgID: orgID,
|
||||
FolderUID: folderUID,
|
||||
UID: uid,
|
||||
Name: name,
|
||||
Kind: 1,
|
||||
Type: "text",
|
||||
Description: "Test library element",
|
||||
Model: []byte(`{"type": "text", "content": "test"}`),
|
||||
Version: 1,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
CreatedBy: 1,
|
||||
UpdatedBy: 1,
|
||||
}
|
||||
|
||||
err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
|
||||
_, err := sess.Insert(element)
|
||||
return err
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return element
|
||||
}
|
||||
|
||||
func insertTestLibraryElementConnection(t *testing.T, sqlStore db.DB, elementID int64, dashboardID int64) {
|
||||
t.Helper()
|
||||
|
||||
connection := &libmodel.LibraryElementConnection{
|
||||
ElementID: elementID,
|
||||
Kind: 1,
|
||||
ConnectionID: dashboardID,
|
||||
Created: time.Now(),
|
||||
CreatedBy: 1,
|
||||
}
|
||||
|
||||
err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
|
||||
_, err := sess.Insert(connection)
|
||||
return err
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func insertTestRule(t *testing.T, sqlStore db.DB, foderOrgID int64, folderUID string) {
|
||||
err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
|
||||
type alertQuery struct {
|
||||
|
||||
@@ -299,6 +299,8 @@ type DashboardRef struct {
|
||||
UID string `xorm:"uid"`
|
||||
Slug string
|
||||
FolderUID string `xorm:"folder_uid"`
|
||||
// Deprecated: use UID instead
|
||||
ID int64 `xorm:"id"`
|
||||
}
|
||||
|
||||
type GetDashboardRefByIDQuery struct {
|
||||
|
||||
@@ -60,6 +60,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/search"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/util/retryer"
|
||||
)
|
||||
@@ -480,6 +481,43 @@ func (dr *DashboardServiceImpl) Count(ctx context.Context, scopeParams *quota.Sc
|
||||
return dr.dashboardStore.Count(ctx, scopeParams)
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*dashboards.DashboardRef, error) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) && dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesLibraryPanels) {
|
||||
res, err := dr.k8sclient.Search(ctx, orgID, &resourcepb.ResourceSearchRequest{
|
||||
Options: &resourcepb.ListOptions{
|
||||
Fields: []*resourcepb.Requirement{
|
||||
{
|
||||
Key: search.DASHBOARD_LIBRARY_PANEL_REFERENCE,
|
||||
Operator: string(selection.Equals),
|
||||
Values: []string{libraryPanelUID},
|
||||
},
|
||||
},
|
||||
},
|
||||
Limit: listAllDashboardsLimit,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results, err := dashboardsearch.ParseResults(res, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dashes := make([]*dashboards.DashboardRef, 0, len(results.Hits))
|
||||
for _, row := range results.Hits {
|
||||
dashes = append(dashes, &dashboards.DashboardRef{
|
||||
UID: row.Name,
|
||||
FolderUID: row.Folder,
|
||||
ID: row.Field.GetNestedInt64(resource.SEARCH_FIELD_LEGACY_ID), // nolint:staticcheck
|
||||
})
|
||||
}
|
||||
return dashes, nil
|
||||
}
|
||||
|
||||
return dr.dashboardStore.GetDashboardsByLibraryPanelUID(ctx, libraryPanelUID, orgID)
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) CountDashboardsInOrg(ctx context.Context, orgID int64) (int64, error) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
resp, err := dr.k8sclient.GetStats(ctx, orgID)
|
||||
|
||||
@@ -46,6 +46,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/search"
|
||||
)
|
||||
|
||||
func TestDashboardService(t *testing.T) {
|
||||
@@ -2918,3 +2919,106 @@ func createTestUnstructuredDashboard(uid, title string, resourceVersion string)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDashboardsByLibraryPanelUID(t *testing.T) {
|
||||
fakeStore := dashboards.FakeDashboardStore{}
|
||||
fakePublicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t)
|
||||
defer fakeStore.AssertExpectations(t)
|
||||
|
||||
k8sCliMock := new(client.MockK8sHandler)
|
||||
|
||||
folderSvc := foldertest.NewFakeService()
|
||||
service := &DashboardServiceImpl{
|
||||
cfg: setting.NewCfg(),
|
||||
log: log.New("test.logger"),
|
||||
dashboardStore: &fakeStore,
|
||||
folderService: folderSvc,
|
||||
ac: actest.FakeAccessControl{ExpectedEvaluate: true},
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders, featuremgmt.FlagKubernetesLibraryPanels),
|
||||
publicDashboardService: fakePublicDashboardService,
|
||||
k8sclient: k8sCliMock,
|
||||
}
|
||||
|
||||
searchResponse := &resourcepb.ResourceSearchResponse{
|
||||
TotalHits: 3,
|
||||
Results: &resourcepb.ResourceTable{
|
||||
Columns: []*resourcepb.ResourceTableColumnDefinition{
|
||||
{Name: resource.SEARCH_FIELD_TITLE, Type: resourcepb.ResourceTableColumnDefinition_STRING},
|
||||
{Name: resource.SEARCH_FIELD_FOLDER, Type: resourcepb.ResourceTableColumnDefinition_STRING},
|
||||
{Name: resource.SEARCH_FIELD_TAGS, Type: resourcepb.ResourceTableColumnDefinition_STRING},
|
||||
{Name: resource.SEARCH_FIELD_LEGACY_ID, Type: resourcepb.ResourceTableColumnDefinition_INT64},
|
||||
},
|
||||
Rows: []*resourcepb.ResourceTableRow{
|
||||
{
|
||||
Key: &resourcepb.ResourceKey{
|
||||
Name: "dashboard1",
|
||||
Resource: "dashboard",
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("Dashboard 1"),
|
||||
[]byte("folder1"),
|
||||
[]byte("[]"),
|
||||
[]byte("1"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: &resourcepb.ResourceKey{
|
||||
Name: "dashboard2",
|
||||
Resource: "dashboard",
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("Dashboard 2"),
|
||||
[]byte("folder2"),
|
||||
[]byte("[]"),
|
||||
[]byte("2"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: &resourcepb.ResourceKey{
|
||||
Name: "dashboard3",
|
||||
Resource: "dashboard",
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("Dashboard 3"),
|
||||
[]byte(""),
|
||||
[]byte("[]"),
|
||||
[]byte("3"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool {
|
||||
return len(req.Options.Fields) == 1 &&
|
||||
req.Options.Fields[0].Key == search.DASHBOARD_LIBRARY_PANEL_REFERENCE &&
|
||||
req.Options.Fields[0].Values[0] == "test-library-panel"
|
||||
})).Return(searchResponse, nil).Once()
|
||||
|
||||
results, err := service.GetDashboardsByLibraryPanelUID(context.Background(), "test-library-panel", 1)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 3)
|
||||
|
||||
resultMap := make(map[string]*dashboards.DashboardRef)
|
||||
for _, result := range results {
|
||||
resultMap[result.UID] = result
|
||||
}
|
||||
|
||||
expectedDashboards := map[string]struct {
|
||||
folderUID string
|
||||
id int64
|
||||
}{
|
||||
"dashboard1": {folderUID: "folder1", id: 1},
|
||||
"dashboard2": {folderUID: "folder2", id: 2},
|
||||
"dashboard3": {folderUID: "", id: 3},
|
||||
}
|
||||
for uid, expected := range expectedDashboards {
|
||||
result, exists := resultMap[uid]
|
||||
require.True(t, exists, "Expected dashboard %s not found", uid)
|
||||
require.Equal(t, expected.folderUID, result.FolderUID, "Folder UID mismatch for %s", uid)
|
||||
require.Equal(t, expected.id, result.ID, "ID mismatch for %s", uid) // nolint:staticcheck
|
||||
}
|
||||
|
||||
k8sCliMock.AssertExpectations(t)
|
||||
}
|
||||
|
||||
@@ -628,6 +628,36 @@ func (_m *FakeDashboardStore) UnprovisionDashboard(ctx context.Context, id int64
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetDashboardsByLibraryPanelUID provides a mock function with given fields: ctx, libraryPanelUID, orgID
|
||||
func (_m *FakeDashboardStore) GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*DashboardRef, error) {
|
||||
ret := _m.Called(ctx, libraryPanelUID, orgID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetDashboardsByLibraryPanelUID")
|
||||
}
|
||||
|
||||
var r0 []*DashboardRef
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, int64) ([]*DashboardRef, error)); ok {
|
||||
return rf(ctx, libraryPanelUID, orgID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, int64) []*DashboardRef); ok {
|
||||
r0 = rf(ctx, libraryPanelUID, orgID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*DashboardRef)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok {
|
||||
r1 = rf(ctx, libraryPanelUID, orgID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ValidateDashboardBeforeSave provides a mock function with given fields: ctx, dashboard, overwrite
|
||||
func (_m *FakeDashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dashboard *Dashboard, overwrite bool) (bool, error) {
|
||||
ret := _m.Called(ctx, dashboard, overwrite)
|
||||
|
||||
Reference in New Issue
Block a user