Dashboard Library: Display datasource plugin dashboards in empty page (#111279)

This commit is contained in:
Juan Cabanas
2025-10-15 09:05:02 -03:00
committed by GitHub
parent 89b3f64479
commit b7a6e1ca53
31 changed files with 972 additions and 47 deletions
+49 -1
View File
@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboardimport"
"github.com/grafana/grafana/pkg/services/dashboardimport/utils"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/web"
@@ -23,15 +24,17 @@ type ImportDashboardAPI struct {
quotaService QuotaService
pluginStore pluginstore.Store
ac accesscontrol.AccessControl
features featuremgmt.FeatureToggles
}
func New(dashboardImportService dashboardimport.Service, quotaService QuotaService,
pluginStore pluginstore.Store, ac accesscontrol.AccessControl) *ImportDashboardAPI {
pluginStore pluginstore.Store, ac accesscontrol.AccessControl, features featuremgmt.FeatureToggles) *ImportDashboardAPI {
return &ImportDashboardAPI{
dashboardImportService: dashboardImportService,
quotaService: quotaService,
pluginStore: pluginStore,
ac: ac,
features: features,
}
}
@@ -43,9 +46,48 @@ func (api *ImportDashboardAPI) RegisterAPIEndpoints(routeRegister routing.RouteR
authorize(accesscontrol.EvalPermission(dashboards.ActionDashboardsCreate)),
routing.Wrap(api.ImportDashboard),
)
if api.features.IsEnabledGlobally(featuremgmt.FlagDashboardLibrary) {
route.Post(
"/interpolate",
authorize(accesscontrol.EvalPermission(dashboards.ActionDashboardsCreate)),
routing.Wrap(api.InterpolateDashboard),
)
}
}, middleware.ReqSignedIn)
}
// swagger:route POST /dashboards/interpolate dashboards interpolateDashboard
//
// Interpolate dashboard. This is an experimental endpoint under dashboardLibrary FF and is subject to change.
//
// Responses:
// 200: interpolateDashboardResponse
// 400: badRequestError
// 401: unauthorisedError
// 422: unprocessableEntityError
// 500: internalServerError
func (api *ImportDashboardAPI) InterpolateDashboard(c *contextmodel.ReqContext) response.Response {
req := dashboardimport.ImportDashboardRequest{}
if err := web.Bind(c.Req, &req); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
if req.PluginId == "" {
return response.Error(http.StatusUnprocessableEntity, "pluginId must be set", nil)
}
resp, err := api.dashboardImportService.InterpolateDashboard(c.Req.Context(), &req)
if err != nil {
return response.Error(http.StatusInternalServerError, "failed to interpolate dashboard", err)
}
resp.Del("__elements")
resp.Del("__inputs")
resp.Del("__requires")
return response.JSON(http.StatusOK, resp)
}
// swagger:route POST /dashboards/import dashboards importDashboard
//
// Import dashboard.
@@ -110,3 +152,9 @@ type ImportDashboardResponse struct {
// in: body
Body dashboardimport.ImportDashboardResponse `json:"body"`
}
// swagger:response interpolateDashboardResponse
type InterpolateDashboardResponse struct {
// in: body
Body interface{} `json:"body"`
}
+79 -4
View File
@@ -15,6 +15,7 @@ import (
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboardimport"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/web/webtest"
@@ -30,7 +31,7 @@ func TestImportDashboardAPI(t *testing.T) {
},
}
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true})
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, featuremgmt.WithFeatures())
routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
s := webtest.NewServer(t, routeRegister)
@@ -107,7 +108,7 @@ func TestImportDashboardAPI(t *testing.T) {
},
}
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true})
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, featuremgmt.WithFeatures())
routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
s := webtest.NewServer(t, routeRegister)
@@ -135,7 +136,7 @@ func TestImportDashboardAPI(t *testing.T) {
t.Run("Quota reached", func(t *testing.T) {
service := &serviceMock{}
importDashboardAPI := New(service, quotaServiceFunc(quotaReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true})
importDashboardAPI := New(service, quotaServiceFunc(quotaReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, featuremgmt.WithFeatures())
routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
@@ -159,8 +160,74 @@ func TestImportDashboardAPI(t *testing.T) {
})
}
func TestInterpolateDashboardFeatureFlag(t *testing.T) {
t.Run("Feature flag disabled - interpolate endpoint should return 404", func(t *testing.T) {
service := &serviceMock{}
// Create features with dashboardLibrary disabled
features := featuremgmt.WithFeatures()
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features)
routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
s := webtest.NewServer(t, routeRegister)
cmd := &dashboardimport.ImportDashboardRequest{
Dashboard: simplejson.New(),
}
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
UserID: 1,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionDashboardsCreate: {}},
},
})
resp, err := s.SendJSON(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusNotFound, resp.StatusCode)
})
t.Run("Feature flag enabled - interpolate endpoint should work", func(t *testing.T) {
interpolateDashboardServiceCalled := false
service := &serviceMock{
interpolateDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) {
interpolateDashboardServiceCalled = true
return simplejson.New(), nil
},
}
// Create features with dashboardLibrary enabled
features := featuremgmt.WithFeatures(featuremgmt.FlagDashboardLibrary)
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features)
routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
s := webtest.NewServer(t, routeRegister)
cmd := &dashboardimport.ImportDashboardRequest{
PluginId: "test-plugin",
}
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
UserID: 1,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionDashboardsCreate: {}},
},
})
resp, err := s.SendJSON(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode)
require.True(t, interpolateDashboardServiceCalled)
})
}
type serviceMock struct {
importDashboardFunc func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*dashboardimport.ImportDashboardResponse, error)
importDashboardFunc func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*dashboardimport.ImportDashboardResponse, error)
interpolateDashboardFunc func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error)
}
func (s *serviceMock) ImportDashboard(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*dashboardimport.ImportDashboardResponse, error) {
@@ -171,6 +238,14 @@ func (s *serviceMock) ImportDashboard(ctx context.Context, req *dashboardimport.
return nil, nil
}
func (s *serviceMock) InterpolateDashboard(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) {
if s.interpolateDashboardFunc != nil {
return s.interpolateDashboardFunc(ctx, req)
}
return simplejson.New(), nil
}
func quotaReached(c *contextmodel.ReqContext, target quota.TargetSrv) (bool, error) {
return true, nil
}
@@ -52,4 +52,5 @@ type ImportDashboardResponse struct {
// Service service interface for importing dashboards.
type Service interface {
ImportDashboard(ctx context.Context, req *ImportDashboardRequest) (*ImportDashboardResponse, error)
InterpolateDashboard(ctx context.Context, req *ImportDashboardRequest) (*simplejson.Json, error)
}
@@ -34,7 +34,7 @@ func ProvideService(routeRegister routing.RouteRegister,
features: features,
}
dashboardImportAPI := api.New(s, quotaService, pluginStore, ac)
dashboardImportAPI := api.New(s, quotaService, pluginStore, ac, features)
dashboardImportAPI.RegisterAPIEndpoints(routeRegister)
return s
@@ -48,7 +48,7 @@ type ImportDashboardService struct {
features featuremgmt.FeatureToggles
}
func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*dashboardimport.ImportDashboardResponse, error) {
func (s *ImportDashboardService) InterpolateDashboard(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) {
var draftDashboard *dashboards.Dashboard
if req.PluginId != "" {
loadReq := &plugindashboards.LoadPluginDashboardRequest{
@@ -70,6 +70,15 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb
return nil, err
}
return generatedDash, nil
}
func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*dashboardimport.ImportDashboardResponse, error) {
generatedDash, err := s.InterpolateDashboard(ctx, req)
if err != nil {
return nil, err
}
// Maintain backwards compatibility by transforming array of library elements to map
libraryElements := generatedDash.Get("__elements")
libElementsArr, err := libraryElements.Array()
+1 -1
View File
@@ -966,7 +966,7 @@ var (
Description: "Enable suggested dashboards when creating new dashboards",
Stage: FeatureStageExperimental,
Owner: grafanaSharingSquad,
FrontendOnly: true,
FrontendOnly: false,
},
{
Name: "logsExploreTableDefaultVisualization",
+1 -1
View File
@@ -125,7 +125,7 @@ cloudWatchNewLabelParsing,GA,@grafana/aws-datasources,false,false,false
disableNumericMetricsSortingInExpressions,experimental,@grafana/oss-big-tent,false,true,false
grafanaManagedRecordingRules,experimental,@grafana/alerting-squad,false,false,false
queryLibrary,preview,@grafana/sharing-squad,false,false,false
dashboardLibrary,experimental,@grafana/sharing-squad,false,false,true
dashboardLibrary,experimental,@grafana/sharing-squad,false,false,false
logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,false,false,true
alertingListViewV2,privatePreview,@grafana/alerting-squad,false,false,true
alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
125 disableNumericMetricsSortingInExpressions experimental @grafana/oss-big-tent false true false
126 grafanaManagedRecordingRules experimental @grafana/alerting-squad false false false
127 queryLibrary preview @grafana/sharing-squad false false false
128 dashboardLibrary experimental @grafana/sharing-squad false false true false
129 logsExploreTableDefaultVisualization experimental @grafana/observability-logs false false true
130 alertingListViewV2 privatePreview @grafana/alerting-squad false false true
131 alertingDisableSendAlertsExternal experimental @grafana/alerting-squad false false false
+6 -4
View File
@@ -1082,14 +1082,16 @@
{
"metadata": {
"name": "dashboardLibrary",
"resourceVersion": "1758902532246",
"creationTimestamp": "2025-09-26T16:02:12Z"
"resourceVersion": "1760051989635",
"creationTimestamp": "2025-09-26T16:02:12Z",
"annotations": {
"grafana.app/updatedTimestamp": "2025-10-09 23:19:49.635811 +0000 UTC"
}
},
"spec": {
"description": "Enable suggested dashboards when creating new dashboards",
"stage": "experimental",
"codeowner": "@grafana/sharing-squad",
"frontend": true
"codeowner": "@grafana/sharing-squad"
}
},
{