Dashboard Library: Integrate community dashboards on Suggested Dashboards Flow (#112808)
* Extend interpolate endpoint to support community dashboard json interpolation Added unit tests * Implement Frontend Side - Show tabs - Fetch Community dashboads - Use DashboardCard component - Search grafana dashboard in the community tab - Make Tabs and pagination sticky - Adjust titles to be scoped by datasource name/type - Add skeleton loading for community tabs and pagination - Add dashboard details tooltip - Bring old datasource-provisioned box back and rely on new feature toggle for community dashboards - update i18n - update swagger --------- Co-authored-by: Juan Cabanas <juan.cabanas@grafana.com> Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
This commit is contained in:
co-authored by
Juan Cabanas
nmarrs
parent
34e85113d2
commit
62eef87208
@@ -189,7 +189,7 @@ func TestInterpolateDashboardFeatureFlag(t *testing.T) {
|
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Feature flag enabled - interpolate endpoint should work", func(t *testing.T) {
|
||||
t.Run("dashboardLibrary 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) {
|
||||
@@ -223,6 +223,232 @@ func TestInterpolateDashboardFeatureFlag(t *testing.T) {
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.True(t, interpolateDashboardServiceCalled)
|
||||
})
|
||||
|
||||
t.Run("suggestedDashboards 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 suggestedDashboards enabled
|
||||
features := featuremgmt.WithFeatures(featuremgmt.FlagSuggestedDashboards)
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInterpolateDashboardAPI(t *testing.T) {
|
||||
features := featuremgmt.WithFeatures(featuremgmt.FlagDashboardLibrary)
|
||||
|
||||
t.Run("Backward compatibility - plugin-based flow still works", func(t *testing.T) {
|
||||
var capturedReq *dashboardimport.ImportDashboardRequest
|
||||
interpolateDashboardServiceCalled := false
|
||||
service := &serviceMock{
|
||||
interpolateDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) {
|
||||
interpolateDashboardServiceCalled = true
|
||||
capturedReq = req
|
||||
result := simplejson.New()
|
||||
result.Set("title", "Test Dashboard")
|
||||
return result, nil
|
||||
},
|
||||
}
|
||||
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",
|
||||
Path: "dashboards/test.json",
|
||||
}
|
||||
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)
|
||||
require.NotNil(t, capturedReq)
|
||||
require.Equal(t, "test-plugin", capturedReq.PluginId)
|
||||
require.Equal(t, "dashboards/test.json", capturedReq.Path)
|
||||
})
|
||||
|
||||
t.Run("New flow with dashboard JSON - should call service with dashboard", func(t *testing.T) {
|
||||
var capturedReq *dashboardimport.ImportDashboardRequest
|
||||
interpolateDashboardServiceCalled := false
|
||||
service := &serviceMock{
|
||||
interpolateDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) {
|
||||
interpolateDashboardServiceCalled = true
|
||||
capturedReq = req
|
||||
result := simplejson.New()
|
||||
result.Set("title", "Community Dashboard")
|
||||
result.Set("panels", []interface{}{})
|
||||
return result, nil
|
||||
},
|
||||
}
|
||||
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features)
|
||||
|
||||
routeRegister := routing.NewRouteRegister()
|
||||
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
|
||||
s := webtest.NewServer(t, routeRegister)
|
||||
|
||||
// Create a test dashboard with datasource that needs interpolation
|
||||
testDashboard := simplejson.New()
|
||||
testDashboard.Set("title", "Test Community Dashboard")
|
||||
testDashboard.Set("panels", []interface{}{
|
||||
map[string]interface{}{
|
||||
"datasource": "${DS_PROMETHEUS}",
|
||||
},
|
||||
})
|
||||
|
||||
cmd := &dashboardimport.ImportDashboardRequest{
|
||||
Dashboard: testDashboard,
|
||||
Inputs: []dashboardimport.ImportDashboardInput{
|
||||
{Name: "DS_PROMETHEUS", Type: "datasource", PluginId: "prometheus", Value: "my-prometheus"},
|
||||
},
|
||||
}
|
||||
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)
|
||||
require.NotNil(t, capturedReq)
|
||||
require.NotNil(t, capturedReq.Dashboard)
|
||||
require.Len(t, capturedReq.Inputs, 1)
|
||||
require.Equal(t, "DS_PROMETHEUS", capturedReq.Inputs[0].Name)
|
||||
})
|
||||
|
||||
t.Run("Validation - both pluginId and dashboard missing should return error", func(t *testing.T) {
|
||||
service := &serviceMock{}
|
||||
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: "",
|
||||
Dashboard: nil,
|
||||
}
|
||||
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.StatusUnprocessableEntity, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Response should not include internal fields", func(t *testing.T) {
|
||||
service := &serviceMock{
|
||||
interpolateDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) {
|
||||
result := simplejson.New()
|
||||
result.Set("title", "Test Dashboard")
|
||||
result.Set("__elements", map[string]interface{}{"test": "value"})
|
||||
result.Set("__inputs", []interface{}{})
|
||||
result.Set("__requires", []interface{}{})
|
||||
result.Set("panels", []interface{}{})
|
||||
return result, nil
|
||||
},
|
||||
}
|
||||
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)
|
||||
defer func() { require.NoError(t, resp.Body.Close()) }()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// Parse response body and verify internal fields are removed
|
||||
var responseBody map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&responseBody)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Test Dashboard", responseBody["title"])
|
||||
require.NotContains(t, responseBody, "__elements")
|
||||
require.NotContains(t, responseBody, "__inputs")
|
||||
require.NotContains(t, responseBody, "__requires")
|
||||
})
|
||||
|
||||
t.Run("Not signed in should return 401", func(t *testing.T) {
|
||||
service := &serviceMock{}
|
||||
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))
|
||||
resp, err := s.SendJSON(req)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
type serviceMock struct {
|
||||
|
||||
Reference in New Issue
Block a user