Tooling API + JS Remoting + JSRender + VisualStrap = Awesome

Since salesforce moved code coverage to developer console, I somehow didn't like it,waited for a while.... and again for like months and no go :( , Salesforce never brought back the old listviews, I was quite missing this  little feature that lets you to have a quick glance of the code coverage. After few weeks finally planned to build something for myself.
This was a great chance to learn a thing or two about Tooling API and a chance to mix some js libraries together to brew something really cool.

After thinking for a while I think these are things I will need to build the page
  • Tooling API : To bring the org code coverage
  • JSRemoting : To bring data to page without the viewstate and in a fast manner
  • JSRender : JsRender is jQuery Templating plugin that lets you create HTML from predefined templates
  • VisualStrap : And the VisualStrap to generate a BootStrap responsive UI for both mobile and desktop
The page in action

Tooling API


The page uses the Tooling API REST service to retrieve the code coverage result.

 private static String sendToolingQueryRequest(String queryStr){  
     HttpRequest req = new HttpRequest();  
     req.setEndpoint(TOOLINGAPI_ENDPOINT+'query/?q='+queryStr);  
     /*Set authorization by using current users session Id*/  
     req.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionID());  
     req.setHeader('Content-Type', 'application/json');      
     req.setMethod('GET');  
     Http http = new Http();  
     HTTPResponse res = http.send(req);  
     return res.getBody();  
   }  


To get the data from the endpoint the method sends the query along with the session id to get the response as JSON string which again used in the JS to render the UI.

{  
   "size": 1,  
   "totalSize": 1,  
   "done": true,  
   "records": [{  
     "attributes": {  
       "type": "ApexCodeCoverage",  
       "url": "/services/data/v29.0/tooling/sobjects/ApexCodeCoverage/71490000002RqOVAA0"  
     },  
     "NumLinesCovered": 2,  
     "ApexClassOrTriggerId": "01p90000001MTXTAA4",  
     "ApexClassOrTrigger": {  
       "attributes": {  
         "type": "Name",  
         "url": "/services/data/v29.0/tooling/sobjects/ApexClass/01p90000001MTXTAA4"  
       },  
       "Name": "jQueryUIBlockDemo_Con"  
     },  
     "NumLinesUncovered": 0  
   }],  
   "queryLocator": null,  
   "entityTypeName": "ApexCodeCoverage"  
 }  


JSRemoting


JSRemoting does the job of bringing the data from controller


JSRender


JSRender takes the job of rendering the data received from the service and use them to generate the table. The templates are pretty easy to handle once you have the data and the decided upon the HTML structure, you can easily create them.

I wanted my page to look like a list so the obvious choice was a table and all the data received should be represented as row, and hence we need a template to generate the rows or "<tr>" for the table. In JSRender JSON data are binded by {{>MY_JSON_FIELDNAME}} 

So the template should be 

<script id="coverageRowTemplate" type="text/x-jsrender">  
     <tr>  
       <td width="20px">  
         <a href="/{{>ApexClassOrTriggerId}}" target="_blank" class="btn btn-xs btn-info"> <span class="glyphicon glyphicon-export"/> view </a>  
       </td>  
       <td>  
         {{>ApexClassOrTrigger.Name}}  
       </td>  
       <td>  
         {{>NumLinesUncovered}}  
       </td>  
       <td>  
         {{>NumLinesCovered}}  
       </td>    
     </tr>  
 </script>

The above template just displays the data received from the remoting method,  lets extend the template to show more info like percentage, totalNumber of lines and may be a background color ?

So to do that we will need some helper methods for the template

$.views.helpers({  
         calculatePercentage: function(NumLinesUncovered,NumLinesCovered){  
           return ((NumLinesCovered/(NumLinesCovered+NumLinesUncovered))*100).toFixed(2);  
         },  
         totalLines:function(NumLinesUncovered,NumLinesCovered){  
           return NumLinesUncovered + NumLinesCovered;  
         },  
         rowStatusClass: function(NumLinesUncovered,NumLinesCovered){  
           var sclass='danger';  
           var percentG = ((NumLinesCovered/(NumLinesCovered+NumLinesUncovered))*100).toFixed(2);  
           if(percentG >= 90){  
             sclass = 'success'  
           }  
           else if(percentG >= 75){  
             sclass = 'warning';  
           }  
           return sclass;  
         }  
 });  

The above code piece defines some helper methods and register them so that they can be used with JSRender templates. So the final template will look like


<script id="coverageRowTemplate" type="text/x-jsrender">  
     <tr class="{{:~rowStatusClass(NumLinesUncovered,NumLinesCovered)}}">  
       <td width="20px">  
         <a href="/{{>ApexClassOrTriggerId}}" target="_blank" class="btn btn-xs btn-info"> <span class="glyphicon glyphicon-export"/> view </a>  
       </td>  
       <td>  
         {{>ApexClassOrTrigger.Name}}  
       </td>  
       <td>  
         {{>NumLinesUncovered}}  
       </td>  
       <td>  
         {{>NumLinesCovered}}  
       </td>  
       <td>  
         {{:~totalLines(NumLinesUncovered,NumLinesCovered)}}  
       </td>        
       <td>  
         {{:~calculatePercentage(NumLinesUncovered,NumLinesCovered )}}  
       </td>  
     </tr>  
   </script>

Now the template serves most of the fields, to generate the HTML 

var html = $( "#JSRENDER_TEMPLATEID" ).render(JSON_DATA );  

and this html can be appended to an existing table in the page to generate a table with the data.

VisualStrap 

VisualStrap is used to generate the Mobile friendly good looking responsive layout along with status classes displayed based on the code coverage percentage

  • Above 90 : Green (css  class = "success")
  • Above 75 : Yellow (css class = "warning")
  • For everything else : Red (css class = "danger")

The mobile layout
So the final product a fast good looking page to view the org code coverage.

Installation : You can follow the project detail link to install a unmanaged package of this page. If you already have visualstrap unmanaged package installed you may have to remove it or you can use source from github to install the same.

VisualForce Page

 <apex:page controller="ApexCodeCoverageList_Con" sidebar="false">  
   <c:importvisualstrap />  
   <apex:includeScript value="{!$Resource.JSRender}"/>  
   <script>  
     function getCodeCoverage(){              
       var rBtn = $('#refreshBtn').button('loading');  
       Visualforce.remoting.Manager.invokeAction(  
         '{!$RemoteAction.ApexCodeCoverageList_Con.fetchCodeCoverage}',  
         function(result,event){  
           if(event.status){  
             console.log(result);  
             var parsedResult = jQuery.parseJSON(result);  
             /*render html using jsrender and attach it to the table*/  
             $('#coverageTableBody').html($( "#coverageRowTemplate" ).render( parsedResult.records ));  
           }  
           else{  
             alert(event.message);  
           }  
           rBtn.button('reset');  
         },  
         {escape: false}  
       );        
     }  
     function getOrgCoverage(){  
       Visualforce.remoting.Manager.invokeAction(  
         '{!$RemoteAction.ApexCodeCoverageList_Con.fetchOrgCoverage}',  
         function(result,event){  
           if(event.status){  
             var parsedResult = jQuery.parseJSON(result);  
             $('#orgCoverage').html(parsedResult.records[0].PercentCovered);  
           }  
           else{  
             alert(event.message);  
           }  
         },  
         {escape: false}  
       );      
     }  
     function getCoverage(){  
       getOrgCoverage();  
       getCodeCoverage();  
     }  
     /*JSrender helper methods*/  
     function initHelperMethods(){  
       $.views.helpers({  
         calculatePercentage: function(NumLinesUncovered,NumLinesCovered){  
           return ((NumLinesCovered/(NumLinesCovered+NumLinesUncovered))*100).toFixed(2);  
         },  
         totalLines:function(NumLinesUncovered,NumLinesCovered){  
           return NumLinesUncovered + NumLinesCovered;  
         },  
         rowStatusClass: function(NumLinesUncovered,NumLinesCovered){  
           var sclass='danger';  
           var percentG = ((NumLinesCovered/(NumLinesCovered+NumLinesUncovered))*100).toFixed(2);  
           if(percentG >= 90){  
             sclass = 'success'  
           }  
           else if(percentG >= 75){  
             sclass = 'warning';  
           }  
           return sclass;  
         }  
       });  
     }  
     $(function(){  
       initHelperMethods();  
       getCoverage();  
     })  
   </script>  
   <!-- JS render template -->  
   <script id="coverageRowTemplate" type="text/x-jsrender">  
     <tr class="{{:~rowStatusClass(NumLinesUncovered,NumLinesCovered)}}">  
       <td width="20px">  
         <a href="/{{>ApexClassOrTriggerId}}" target="_blank" class="btn btn-xs btn-info"> <span class="glyphicon glyphicon-export"/> view </a>  
       </td>  
       <td>  
         {{>ApexClassOrTrigger.Name}}  
       </td>  
       <td>  
         {{>NumLinesUncovered}}  
       </td>  
       <td>  
         {{>NumLinesCovered}}  
       </td>  
       <td>  
         {{:~totalLines(NumLinesUncovered,NumLinesCovered)}}  
       </td>        
       <td>  
         {{:~calculatePercentage(NumLinesUncovered,NumLinesCovered )}}  
       </td>  
     </tr>  
   </script>  
   <c:visualstrapblock >  
     <c:panel type="primary">  
       <center>  
         <c:pageheader icon="cog" title="Apex Code Coverage" subtitle="All Classes"/>  
         <div class="text-muted" style="position:absolute;top:20px;right:20px">Using Tooling API, JS Remoting, JSRender and VisualStrap</div>  
        </center>  
       <apex:outputPanel layout="block" styleClass="well well-sm">  
         <center>  
           <button id="refreshBtn" onclick="getCoverage();return false;" class="btn btn-success" data-loading-text="Refreshing...">  
             <c:glyph icon="refresh"/> Refresh  
           </button>  
         </center>  
       </apex:outputPanel>  
       <apex:outputPanel layout="block" styleClass="row">  
         <apex:outputPanel layout="block" styleClass="col-md-10">  
           <table class="table table-bordered table-striped table-hover table-condensed">  
             <thead>  
               <tr>  
                 <th>  
                   Action  
                 </th>  
                 <th>  
                   Apex Class/ Trigger  
                 </th>  
                 <th>  
                   Lines Not Covered  
                 </th>  
                 <th>  
                   Lines Covered  
                 </th>  
                 <th>  
                   Total Lines  
                 </th>  
                 <th>  
                   Coverage Percentage  
                 </th>  
               </tr>  
             </thead>  
             <tbody id="coverageTableBody">  
             </tbody>  
           </table>  
         </apex:outputPanel>  
         <apex:outputPanel layout="block" styleClass="col-md-2">  
           <vs:panel type="primary" title="Overall Coverage" >  
             <center>  
               <h2 style="font-size:54"><span id="orgCoverage"/> %</h2>  
               <p class="text-muted infolabel">Across all apex classes and triggers</p>   
             </center>  
           </c:panel>  
         </apex:outputPanel>  
       </apex:outputPanel>  
     </c:panel>  
   </c:visualstrapblock>  
 </apex:page>

Apex Class

/*  
 *  @Author : Avi (avidev9@gmail.com)  
 *  @Description : Controller class for ApexCodeCoverageList page, Contains remoted method and method to call tooling api  
 *  
 **/  
 public class ApexCodeCoverageList_Con{  
   private static FINAL String ORG_INSTANCE;  
   private static FINAL String TOOLINGAPI_ENDPOINT;  
   static{  
     ORG_INSTANCE = getInstance();  
     TOOLINGAPI_ENDPOINT = 'https://'+ORG_INSTANCE+'.salesforce.com/services/data/v29.0/tooling/';  
   }  
   @RemoteAction  
   public static String fetchCodeCoverage(){  
     return sendToolingQueryRequest('SELECT+NumLinesCovered,ApexClassOrTriggerId,ApexClassOrTrigger.Name,NumLinesUncovered+FROM+ApexCodeCoverage');  
   }  
   @RemoteAction  
   public static String fetchOrgCoverage(){  
     return sendToolingQueryRequest('SELECT+PercentCovered+FROM+ApexOrgWideCoverage');  
   }  
   /*Method to send query request to tooling api endpoint*/  
   private static String sendToolingQueryRequest(String queryStr){  
     HttpRequest req = new HttpRequest();  
     req.setEndpoint(TOOLINGAPI_ENDPOINT+'query/?q='+queryStr);  
     /*Set authorization by using current users session Id*/  
     req.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionID());  
     req.setHeader('Content-Type', 'application/json');      
     req.setMethod('GET');  
     Http http = new Http();  
     HTTPResponse res = http.send(req);  
     return res.getBody();  
   }  
   /*Method to get org instance*/  
   private static String getInstance(){  
     String instance;  
     List<String> parts = System.URL.getSalesforceBaseUrl().getHost().replace('-api','').split('\\.');  
     if (parts.size() == 3 ) Instance = parts[0];  
     else if (parts.size() == 5 || parts.size() == 4) Instance = parts[1];  
     else Instance = null;  
     return instance;  
   }  
 }  

8 comments:

  1. Replies
    1. Well I tried out the package just now and looks like its working. Did you flow all the instrcution from the project page ? like remotesite setting etc.

      Delete
    2. hi maiti when iam trying to install the packge its throwing error like contact salesforce.com support. could you please help me on this.

      Delete
    3. I just tried with a fresh org looks like its working for me. Can you send me the screenshot of the error ? or may be try in another org ?

      Delete
    4. The "Read Time Out" Error shows below on Console of Developer Tools:
      Visualforce Remoting Exception: Read timed out
      object.f {statusCode: 400, type: "exception", tid: 5, ref: false, action: "ApexCodeCoverageList_Con"…}
      action: "ApexCodeCoverageList_Con"
      data: ""
      message: "Read timed out"
      method: "fetchCodeCoverage"
      ref: false
      result: null
      statusCode: 400
      tid: 5
      type: "exception"
      vfDbg: true
      vfTx: true
      where: ""
      __proto__: g
      VFRemote.js:116
      $VFRM.Util.error VFRemote.js:116
      (anonymous function) VFRemote.js:132
      a.Event.fire VFRemote.js:52
      a.Observable.fireEvent VFRemote.js:47
      VFExt3.Direct.VFExt3.extend.onProviderData VFRemote.js:86
      a.Event.fire VFRemote.js:52
      a.Observable.fireEvent VFRemote.js:47
      VFExt3.direct.RemotingProvider.VFExt3.extend.onData VFRemote.js:94
      VFExt3.extend.handleResponse VFRemote.js:75
      a VFRemote.js:39
      (anonymous function)

      Delete
    5. There are more than 800 classes in my org. Can you please help me with this issue?

      Delete
  2. awesome post i really like this post.keep sharing the post.
    more info

    ReplyDelete