Transforming Analytics API Data for Lightning Components

Learn how to take the rich analytics API response and convert it into a user defined data structure optimized for displaying in our Salesforce1 Lightning Component reports.

Guest Post: Daniel Peter is a Lead Applications Engineer at Kenandy, Inc., building the next generation of ERP on the Salesforce Cloud.  You can reach him on Twitter @danieljpeter or www.linkedin.com/in/danieljpeter.

The first article in this series, Build Reporting Apps Using Lightning Components and the Analytics API,  showed how to create a simplified version of your data using the Reporting API. The followup article, Displaying Reports in Salesforce1 Using Lightning Components, showed how to display that data using Lightning Components. A List<List> works well for a quick view of data, but what if you need additional attributes? For example, records have Salesforce IDs that  can be useful for navigating to related records. Another use case might be when you need to know lookup field names and labels to help construct a more robust data table on the frontend.

We can define some user-defined data types as classes in Apex to contain a more complete representation of data:

public class tabularReportResponse {
    public List reportFields {get; set;}
    public List<List> fieldDataList {get; set;}
    public tabularReportResponse(){}
}

public class fieldDef {
    public String fieldName {get; set;}
    public String fieldLabel {get; set;}
    public String dataType {get; set;}
    public fieldDef(){}
}

public class fieldData {
    public String fieldValue {get; set;}
    public String fieldLabel {get; set;}
    public String dataType  {get; set;}
    public Boolean isHyperLink {get; set;}          
    public fieldData(){isHyperLink=false;}
}

The outermost container, tabularReportResponse, contains a reportFields object that describes the field metadata. It also contains fieldDataList, which contains all of our data. reportFields is a list of another user-defined data type that contains the name, label, and data type of the field. fieldDataList is a List<List>. The outer list is the rows, and the inner list is our last user-defined data type, fieldData, which contains a value, label, datatype, and whether or not it is a hyperlink. You can add any number of attributes to these objects, but this is a good place to start.

Next, create a more complex utility method to convert the analytics API objects into this data structure:

public Static tabularReportResponse getTabularReportResponse(Id reportId) {
    tabularReportResponse trr = new tabularReportResponse();
    List reportFields = new List(); 
    List<List> fieldDataList = new List<List>();  

    //get the report result
    Reports.ReportResults results = Reports.ReportManager.runReport('00Oj0000003OHhw', true);

    //get the metadata
    Reports.ReportMetadata reportMetadata = results.getReportMetadata();

    //get a string array of the field names
    List fieldNames = reportMetadata.getDetailColumns();        

    //get the extended metadata
    Reports.ReportExtendedMetadata reportExtendedMetadata = results.getReportExtendedMetadata();

    //get the map of the column names to their name and label
    Map<String, Reports.DetailColumn> detailColumnMap = reportExtendedMetadata.getDetailColumnInfo();

    //loop over the detailColumnMap and get the name, label, and data type
    for (String fieldName: fieldNames) {
        Reports.DetailColumn detailColumn = detailColumnMap.get(fieldName);
        fieldDef fd = new fieldDef();
        fd.fieldName = detailColumn.getName(); 
        fd.fieldLabel = detailColumn.getLabel();
        fd.dataType = detailColumn.getDataType().name();
        reportFields.add(fd);
    }

    // Get the fact map from the report results
    Reports.ReportFactWithDetails factDetails = (Reports.ReportFactWithDetails)results.getFactMap().get('T!T');     

    List reportDetailRowList = factDetails.getRows();

    //loop over the rows
    for (Reports.ReportDetailRow reportDetailRow: reportDetailRowList) {
        Integer cellCounter = 0;
        List fieldDataRow = new List();
        //loop over the cells in the row
        for (Reports.ReportDataCell reportDataCell: reportDetailRow.getDataCells()) {
            fieldData fd = new fieldData();
            fd.fieldValue = (String)reportDataCell.getValue();
            fd.fieldLabel = (String)reportDataCell.getLabel();
            fd.dataType = reportFields[cellCounter].dataType;
            cellCounter++;
            fieldDataRow.add(fd);
        }

        //add the row to the list
        fieldDataList.add(fieldDataRow);
    }

    trr.reportFields = reportFields;
    trr.fieldDataList = fieldDataList;
    return trr;
}

This method gets the extended metadata, builds the report fields, then builds the more complex data structure from the fields and data.

Here is a comparison of the original data structure from the first article, and the new data structure:

The new data structure is obviously more complex than the original two-dimensional array, but still far more simple than the Reports.ReportResult object as it is returned from Salesforce.

In order to take advantage of this new data structure, update the Apex controller:

public class LightningReportsController {

    @AuraEnabled
    public static List<List> getReportRows() {
        return AnalyticsUtils.getTabularReport('00Oj0000003OHhw');
    }

    @AuraEnabled
    public static String getReportResponse() {
        return JSON.serialize(AnalyticsUtils.getTabularReportResponse('00Oj0000003OHhw'));
    }

}

and Lightning Components bundle:

({
    getReportResponse : function(component) {
        // Load report data
        var action = component.get("c.getReportResponse");
        var self = this;
        action.setCallback(this, function(a){
            var reportResponseObj = JSON.parse(a.getReturnValue()); 
            component.set("v.reportResponse", reportResponseObj);

            // Display toast message to indicate load status
            var toastEvent = $A.get("e.force:showToast");
            if(action.getState() ==='SUCCESS'){
                toastEvent.setParams({
                    "title": "Success!",
                    "message": " Your report has been loaded successfully."
                });
            }else{
                toastEvent.setParams({
                    "title": "Error!",
                    "message": " Something has gone wrong."
                });
            }
            toastEvent.fire();
        });
         $A.enqueueAction(action);
    },
    loadResources : function() {
        this.loadCSS('/resource/jquery_mobile_145/jquery.mobile-1.4.5.min.css', function() {
        });
        this.loadJS('/resource/jquery_mobile_145/jquery-1.11.2.min.js', function() {
        });
        this.loadJS('/resource/jquery_mobile_145/jquery.mobile-1.4.5.min.js', function() {
        });
    },
    loadJS : function(source, callback) {
        var loadScript = document.createElement('script');
        loadScript.setAttribute('src', source);
        loadScript.onload = callback;
        document.head.appendChild(loadScript);
    },
    loadCSS : function(source, callback) {
        var fileref = document.createElement('link');
        fileref.setAttribute("rel", "stylesheet");
        fileref.setAttribute("type", "text/css");
        fileref.setAttribute("href", source);
        fileref.onload = callback;
        document.head.appendChild(fileref);
    }    
})

There are several ways to deal with returning a complex, user-defined object from Apex. The way I chose was to serialize the object to a string in my Apex controller, and then parse it into a JavaScript object in my JavaScript controller. Then there’s no need to tell Lightning Components that it is strongly typed as an AnalyticsUtils.tabularReportResponse object.

In our component, you have to dig a little deeper into the object to get the data you want for the header row and  fields, down to the v.reportResponse.reportFields and v.reportResponse.fieldDataList level.

<aura:component controller="LIGHTNINGREPORT.LightningReportsController" implements="force:appHostable">

    <!-- Handle component initialization in a client-side controller -->
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>

    <!-- Handle loading events by displaying a spinner -->
    <aura:handler event="aura:waiting" action="{!c.showSpinner}"/>
    <aura:handler event="aura:doneWaiting" action="{!c.hideSpinner}"/>

    <!-- Dynamically load the report rows -->
    <aura:attribute name="reportResponse" type="Object"/>

    <div><center><ui:spinner aura:id="spinner"/></center></div>

    <div data-role="page" data-theme="d" id="reportPage">
        <div role="main" class="ui-content">

            <!-- Iterate over the list of report rows and display them -->
            <!-- special case for the header row -->
            <table data-role="table" data-mode="columntoggle" id="report-table" class="custom-reponsive table-stroke">

                <thead>
                    <LIGHTNINGREPORT:reportRowComponent row="{!v.reportResponse.reportFields}" isHeader="true"/>
                </thead>
                <tbody>
                    <aura:iteration var="row" items="{!v.reportResponse.fieldDataList}">
                        <LIGHTNINGREPORT:reportRowComponent row="{!row}" isHeader="false"/>
                    </aura:iteration>
                </tbody>

            </table>

        </div>
    </div>

</aura:component>

And in our row component, we just pick out the labels for display:

<aura:component >
    <aura:attribute name="row" type="Object[]"/>
    <aura:attribute name="isHeader" type="Boolean"/>

    <tr>
        <aura:iteration var="cell" items="{!v.row}">
            <aura:renderIf isTrue="{!v.isHeader}">
                <th class="cell">{!cell.fieldLabel}</th>
                <aura:set attribute="else">
                    <td class="cell">{!cell.fieldLabel}</td>
                </aura:set>
            </aura:renderIf>

        </aura:iteration>
    </tr>

</aura:component>

The report looks a little better, since we have the header labels instead of the field names:

But most importantly, we now have more data we can access in our row component. If it is a header, we have:
cell.fieldLabel, cell.fieldName, and cell.dataType. And if it is a data row, we have cell.fieldLabel, cell.fieldValue (sometimes contains IDs we can hyperlink to), cell.dataType, and cell.isHyperLink.

To see this, let’s add in those additional data points to our component temporarily:

<aura:component >
    <aura:attribute name="row" type="Object[]"/>
    <aura:attribute name="isHeader" type="Boolean"/>

    <tr>
        <aura:iteration var="cell" items="{!v.row}">
            <aura:renderIf isTrue="{!v.isHeader}">
                <th class="cell">
                    {!cell.fieldLabel}<br/>
                    {!cell.fieldName}
                </th>
                <aura:set attribute="else">
                    <td class="cell">
                    {!cell.fieldLabel}<br/>
                    {!cell.fieldValue}<br/>
                    {!cell.dataType}<br/>
                    {!cell.isHyperLink}
                    </td>
                </aura:set>
            </aura:renderIf>

        </aura:iteration>
    </tr>

</aura:component>

And we can see all the additional metadata we now have:

In a future article, I’ll show how you can use this additional metadata behind the scenes to further enrich our reports.

Published
March 19, 2015

Leave your comments...

Transforming Analytics API Data for Lightning Components