diff --git a/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts index 4d23ca92514..ed3eac501b0 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts @@ -244,6 +244,7 @@ const injectedRtkApi = api folder: queryArg.folder, facet: queryArg.facet, tags: queryArg.tags, + libraryPanel: queryArg.libraryPanel, sort: queryArg.sort, limit: queryArg.limit, explain: queryArg.explain, @@ -608,6 +609,8 @@ export type GetSearchApiArg = { facet?: string[]; /** tag query filter */ tags?: string[]; + /** find dashboards that reference a given libraryPanel */ + libraryPanel?: string; /** sortable field */ sort?: string; /** number of results to return */ diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index aedd222d94b..7bbf48ab0f2 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -438,7 +438,6 @@ func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.S } ctx = c.Req.Context() - var err error var userID int64 if id, err := identity.UserIdentifier(c.GetID()); err == nil { @@ -518,12 +517,6 @@ func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.S return apierrors.ToDashboardErrorResponse(ctx, hs.pluginStore, saveErr) } - // connect library panels for this dashboard after the dashboard is stored and has an ID - err = hs.LibraryPanelService.ConnectLibraryPanelsForDashboard(ctx, c.SignedInUser, dashboard) - if err != nil { - return response.Error(http.StatusInternalServerError, "Error while connecting library panels", err) - } - c.TimeRequest(metrics.MApiDashboardSave) return response.JSON(http.StatusOK, util.DynMap{ "status": "success", diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 7c47656e208..362be520230 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -20,7 +20,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" @@ -39,7 +38,6 @@ import ( "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/foldertest" libraryelementsfake "github.com/grafana/grafana/pkg/services/libraryelements/fake" - "github.com/grafana/grafana/pkg/services/librarypanels" "github.com/grafana/grafana/pkg/services/licensing/licensingtest" "github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/org" @@ -265,7 +263,6 @@ func TestHTTPServer_DeleteDashboardByUID_AccessControl(t *testing.T) { hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) hs.starService = startest.NewStarServiceFake() - hs.LibraryPanelService = &mockLibraryPanelService{} hs.LibraryElementService = &libraryelementsfake.LibraryElementService{} middleware := publicdashboards.NewFakePublicDashboardMiddleware(t) @@ -791,7 +788,6 @@ func TestIntegrationDashboardAPIEndpoint(t *testing.T) { ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()), Live: newTestLive(t, db.InitTestDB(t)), QuotaService: quotatest.New(false, nil), - LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &libraryelementsfake.LibraryElementService{}, DashboardService: dashboardService, SQLStore: dbtest.NewFakeDB(), @@ -853,7 +849,6 @@ func TestIntegrationDashboardAPIEndpoint(t *testing.T) { hs := &HTTPServer{ Cfg: setting.NewCfg(), ProvisioningService: fakeProvisioningService, - LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &libraryelementsfake.LibraryElementService{}, dashboardProvisioningService: dashboardProvisioningService, SQLStore: mockSQLStore, @@ -887,7 +882,6 @@ func TestIntegrationDashboardAPIEndpoint(t *testing.T) { hs := &HTTPServer{ Cfg: setting.NewCfg(), ProvisioningService: fakeProvisioningService, - LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &libraryelementsfake.LibraryElementService{}, dashboardProvisioningService: dashboardProvisioningService, SQLStore: mockSQLStore, @@ -928,7 +922,6 @@ func TestIntegrationDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", org.RoleEditor, func(sc *scenarioContext) { hs := &HTTPServer{ Cfg: setting.NewCfg(), - LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &libraryelementsfake.LibraryElementService{}, SQLStore: mockSQLStore, AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true}, @@ -1087,7 +1080,6 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s Live: newTestLive(t, db.InitTestDB(t)), QuotaService: quotatest.New(false, nil), pluginStore: &pluginstore.FakePluginStore{}, - LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &libraryelementsfake.LibraryElementService{}, DashboardService: dashboardService, folderService: folderService, @@ -1127,7 +1119,6 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()), Live: newTestLive(t, db.InitTestDB(t)), QuotaService: quotatest.New(false, nil), - LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &libraryelementsfake.LibraryElementService{}, DashboardService: mock, SQLStore: sqlStore, @@ -1177,15 +1168,3 @@ func (s mockDashboardProvisioningService) GetProvisionedDashboardDataByDashboard ) { return nil, nil } - -type mockLibraryPanelService struct{} - -var _ librarypanels.Service = (*mockLibraryPanelService)(nil) - -func (m *mockLibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, dash *dashboards.Dashboard) error { - return nil -} - -func (m *mockLibraryPanelService) ImportLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error { - return nil -} diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index d8b252ba406..2d3bd11ab37 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -164,7 +164,6 @@ type HTTPServer struct { LoggerMiddleware loggermw.Logger SQLStore db.DB AlertNG *ngalert.AlertNG - LibraryPanelService librarypanels.Service LibraryElementService libraryelements.Service SocialService social.Service Listener net.Listener @@ -319,7 +318,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi ContextHandler: contextHandler, LoggerMiddleware: loggerMiddleware, AlertNG: alertNG, - LibraryPanelService: libraryPanelService, LibraryElementService: libraryElementService, QuotaService: quotaService, tracer: tracer, diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client.go b/pkg/registry/apis/dashboard/legacysearcher/search_client.go index 24c49cc5624..f729822e599 100644 --- a/pkg/registry/apis/dashboard/legacysearcher/search_client.go +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client.go @@ -229,12 +229,32 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resourcepb.Reso return nil, fmt.Errorf("only one repo name is supported") } query.ManagerIdentity = vals[0] + case unisearch.DASHBOARD_LIBRARY_PANEL_REFERENCE: if len(vals) != 1 { return nil, fmt.Errorf("only one library panel uid is supported") } - return c.getLibraryPanelConnections(ctx, user, vals[0], req.Options.Key.Namespace) + // Make sure the query does not include incompatible combinations + for _, f := range req.Options.Fields { + switch f.Key { + case resource.SEARCH_FIELD_NAME: + return nil, fmt.Errorf("libraryPanel query must not include explicit names") + } + } + + query.DashboardUIDs, err = c.getLibraryPanelConnections(ctx, user, vals[0]) + if err != nil { + return nil, err + } + if len(query.DashboardUIDs) == 0 { + // Empty results + return &resourcepb.ResourceSearchResponse{ + TotalHits: 0, + Results: &resourcepb.ResourceTable{}, + }, nil + } + case resource.SEARCH_FIELD_TITLE_PHRASE: if len(vals) != 1 { return nil, fmt.Errorf("only one title supported") @@ -362,32 +382,18 @@ func getResourceKey(item *dashboards.DashboardSearchProjection, namespace string } } -// retrieves all the dashboards that are connected to the given library panel -func (c *DashboardSearchClient) getLibraryPanelConnections(ctx context.Context, user identity.Requester, libraryElementUID, namespace string) (*resourcepb.ResourceSearchResponse, error) { +// retrieves all dashboard UIDs connected to a given library panel +func (c *DashboardSearchClient) getLibraryPanelConnections(ctx context.Context, user identity.Requester, libraryElementUID string) ([]string, error) { connections, err := c.dashboardStore.GetDashboardsByLibraryPanelUID(ctx, libraryElementUID, user.GetOrgID()) if err != nil { return nil, err } - columns := c.getColumns("", &dashboards.FindPersistedDashboardsQuery{}) - list := &resourcepb.ResourceSearchResponse{ - Results: &resourcepb.ResourceTable{ - Columns: columns, - }, + uids := make([]string, len(connections)) + for i, dashboard := range connections { + uids[i] = dashboard.UID } - - for _, dashboard := range connections { - cells := c.createCommonCells("", dashboard.FolderUID, dashboard.ID, nil) // nolint:staticcheck - list.Results.Rows = append(list.Results.Rows, &resourcepb.ResourceTableRow{ - Key: getResourceKey(&dashboards.DashboardSearchProjection{ - UID: dashboard.UID, - }, namespace), - Cells: cells, - }) - } - - list.TotalHits = int64(len(list.Results.Rows)) - return list, nil + return uids, nil } func (c *DashboardSearchClient) GetStats(ctx context.Context, req *resourcepb.ResourceStatsRequest, _ ...grpc.CallOption) (*resourcepb.ResourceStatsResponse, error) { diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go b/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go index 489331db2b7..74f0cc2622c 100644 --- a/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go @@ -581,6 +581,11 @@ func TestDashboardSearchClient_Search(t *testing.T) { {UID: "dashboard2", FolderUID: "folder2", ID: 2}, }, nil).Once() + mockStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{ + {UID: "dashboard1", FolderUID: "folder1", ID: 1}, + {UID: "dashboard2", FolderUID: "folder2", ID: 2}, + }, nil).Once() + req := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ Key: dashboardKey, diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index 7dba6e1f8f1..d471cba752a 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -121,6 +121,15 @@ func (s *SearchHandler) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) * Schema: spec.ArrayProperty(spec.StringProperty()), }, }, + { + ParameterProps: spec3.ParameterProps{ + Name: "libraryPanel", + In: "query", + Description: "find dashboards that reference a given libraryPanel", + Required: false, + Schema: spec.StringProperty(), + }, + }, { ParameterProps: spec3.ParameterProps{ Name: "sort", @@ -363,6 +372,15 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { }} } + // The libraryPanel filter + if libraryPanel, ok := queryParams["libraryPanel"]; ok { + searchRequest.Options.Fields = []*resourcepb.Requirement{{ + Key: search.DASHBOARD_LIBRARY_PANEL_REFERENCE, + Operator: "=", + Values: libraryPanel, + }} + } + // The names filter names := queryParams["name"] diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index 65ca0dd7ea0..ea37867d8c0 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -630,6 +630,7 @@ func (d *dashboardStore) deleteDashboard(cmd *dashboards.DeleteDashboardCommand, {SQL: "DELETE FROM dashboard_version WHERE dashboard_id = ?", args: []any{dashboard.ID}}, {SQL: "DELETE FROM dashboard_provisioning WHERE dashboard_id = ?", args: []any{dashboard.ID}}, {SQL: "DELETE FROM dashboard_acl WHERE dashboard_id = ?", args: []any{dashboard.ID}}, + {SQL: "DELETE FROM library_element_connection WHERE connection_id = ?", args: []any{dashboard.ID}}, } if dashboard.IsFolder { diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json index e53e6509bc1..42af6d54a98 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json @@ -1812,6 +1812,14 @@ } } }, + { + "name": "libraryPanel", + "in": "query", + "description": "find dashboards that reference a given libraryPanel", + "schema": { + "type": "string" + } + }, { "name": "sort", "in": "query",