Use a Lightning Pill as a simple KPI. And the power of Flows!

How can we display KPIs in any place of the Record Page?



Using a very simple component to show KPIs

Lightning Componets and Flows are a very powerful set of tools that allow us to simplify the application configuration and ease the maintenance.

In this article I share a simple component that use a Flow to count records of any Object and show the number as a KPI.

Objective

We will create a component and a very simple Flow that will allow us to show the number of records of any object related (at any level) to the active object in the current Record Page.

Component structure

 


Specification


Name Label Type Required Default Description
label Label String false Message The text label that display in the KPI after the (number)
description Tooltip String false Show more info on mouse over
iconName Icon String false Provide a valid SLDS icon like 'standard:account'. Check https://www.lightningdesignsystem.com/icons/
errorFormula Alert when String false Insert a formula like: =0, >=3, <8, etc.
Operators:
=   equal
<   less than
>   greater than
<= less or equal than
>= greater or equal than
<> different
flowName Flow String true N/A API name of the Flow. Should have a 'recordCount' output property.
The datasource of this property is provided by the APEX class ComponentDesignFlowPicklist (the only piece of APEX server side code in the component, but guest what? it is only used at design time, when you are editing the Lightning Record Page and the component properties)

 


KPI Component

<aura:component implements="flexipage:availableForRecordHome,force:hasRecordId" access="global">
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
    
    <aura:attribute name="label" type="String" default="The text label that displays in the KPI after the (number)"/>
    <aura:attribute name="description" type="String" default=""/>
    <aura:attribute name="iconName" type="String" default=""/>
    <aura:attribute name="hasError" type="Boolean" default="false"/>
    <aura:attribute name="errorFormula" type="String" default=""/>
    <aura:attribute name="count" type="Integer" default="0"/>
    <aura:attribute name="flowName" type="String" required="true"/>
    
    <div>
        <lightning:pill label="{!'(' + v.count + ') ' + v.label}" title="{!v.description}" onclick="{!c.onClick}" hasError="{!v.hasError}">
            <aura:set attribute="media">
                <aura:if isTrue="{!not(empty(v.iconName))}">
                    <lightning:icon iconName="{!v.iconName}" alternativeText="{!v.description}"/>
                </aura:if>
            </aura:set>
        </lightning:pill>
        <div class="slds-hide">
            <lightning:flow aura:id="flowToExecute" onstatuschange="{!c.onFlowFinished}" />
        </div>
    </div>
</aura:component>

 

KPI Design

<design:component label="KPI Integer">
    <design:attribute name="label" label="Label" description="The text label that displays in the KPI after the (number)" default="Message"/>
    <design:attribute name="description" label="Tooltip" description="Show more info on mouse over" default=""/>
    <design:attribute name="iconName" label="Icon" description="Provide a valid SLDS icon like 'standard:account'. Check https://www.lightningdesignsystem.com/icons/" default=""/>
    <design:attribute name="errorFormula" label="Alert when" description="Insert a formula like: =0, >=3, <8, etc." default=""/>
    <design:attribute name="flowName" label="Flow" description="API name of the Flow. Should have a 'recordCount' output property" datasource="apex://ComponentDesignFlowPicklist" default="N/A"/>
</design:component>

 

KPI Controller

({
    doInit : function(component, event, helper) 
    {
        var flow = component.find("flowToExecute");
        var inputVariables = [ { name : "recordId", type : "String", value: component.get("v.recordId") }, ];
        flow.startFlow(component.get("v.flowName"), inputVariables);
    },
    
    onFlowFinished : function(component, event, helper) 
    {
        if(event.getParam("status") === "FINISHED_SCREEN") 
        {
            // Get the output variables and iterate over them
            var outputVariables = event.getParam("outputVariables");
            var outputVar;

            for (var i = 0; i < outputVariables.length; i++) 
            {
                outputVar = outputVariables[i];
                // Pass the values to the component's attributes
                if(outputVar.name === "recordCount") // REQUIRED: Flow should have an output variable recordCount
                {
                    component.set("v.count", outputVar.value);
                    break;
                }
             }
             
             helper.checkForAlertValue(component, event, helper);
          }
    },

    onClick : function(component, event, helper) 
    {
        var flow = component.find("flowToExecute");
        var inputVariables = [ { name : "recordId", type : "String", value: component.get("v.recordId") }, ];
        flow.startFlow(component.get("v.flowName"), inputVariables);
    },
})

 

KPI Helper

({
    checkForAlertValue : function(component, event, helper) 
    {
        let errorFormula = component.get("v.errorFormula");
        let count = component.get("v.count");
        let value = -1;
        let hasError = false;

        try 
        {
            if (errorFormula)
            {
                if (errorFormula.startsWith("="))
                {
                    value = parseInt(errorFormula.split("=")[1]);
                    hasError = count == value;
                }
                else if (errorFormula.startsWith("<="))
                {
                    value = parseInt(errorFormula.split("<=")[1]);
                    hasError = count <= value;
                }
                else if (errorFormula.startsWith(">="))
                {
                    value = parseInt(errorFormula.split(">=")[1]);
                    hasError = count >= value;
                }
                else if (errorFormula.startsWith("<>"))
                {
                    value = parseInt(errorFormula.split("<>")[1]);
                    hasError = count != value;
                }
                else if (errorFormula.startsWith("<"))
                {
                    value = parseInt(errorFormula.split("<")[1]);
                    hasError = count < value;
                }
                else if (errorFormula.startsWith(">"))
                {
                    value = parseInt(errorFormula.split(">")[1]);
                    hasError = count > value;
                }
                else
                {
                    throw new Error("KPI Integer ERROR: the 'Alert when' value is wrong.");        
                }
    
                component.set('v.hasError', hasError);
            }
        }
        catch(error) 
        {
            console.log(error.message);
            throw new Error("KPI Integer ERROR: the 'Alert when' value is wrong.");  
        }        
    }
})

 

ComponentDesignFlowPicklist (APEX)

This is a global APEX class that extends VisualEditor.DynamicPickList and returns a list of VisualEditor.DataRows composed by the flow Label and Name. I will leave this to you 😀. 

At the moment I wrote this, it was not possible to provide a datasource for the Flow property (design time) using only Point & Click functionalities.

Nevertheless, if you are not in the mood to develop such piece of code, remove the "datasource= "apex:// ComponentDesignFlowPicklist"" attribute's property in the Component Design, and keep in mind that you should use the Flow API Name.

 

What we have until now?

We have a very simple component with the power of showing us interesting information like "If the customer has three pending payments, show me an alert somewhere!!", ""Show me the total number of incidents in the building, no matter the apartment (Building -> Apartment -> Incident)".

This component refresh when the Object's Record Page opens, and every time the user click it.

 

How complex are the Flows behind this Component?

Very simple Flow to count Orders of a Client
It doesn't look that complex, isn't it? 

This Flow has two main and required variables: recordId (Text) that is the INPUT variable that will store the Record Page recordId (interface force:hasRecordId in the Component), and the recordCount (Number) that is the OUTPUT variable where the Assignment Count will store the count result. The KPI component is expecting these two variables in the Flow.

But, if we are counting only the first level of Objects related to recordId, why we don't use a standard component like Related List Quick Links? After all, the number of records are shown in it.

Well, imaging that you need to show a KPI in an Asset Hierarchy, about all the Assets in the second or third level. With a Flow, you can count them. It won't be as simple as the one in the image above, but it won't be complex at all. By the way, this was the actual reason I made all of this, and the second reason was that I don't believe that everything should be solved with a Field in the Object and a Trigger, a background Job, or a ETL job that will fill it!! 😀.

Anyway, if you need the number in an Object's field, you can use the the Flow to do it.

 

Is there any known problem with this Component?

Not as far as I know, but everything related to Flow Limits can happen (check Salesforce docs). Other things that comes to my mind?, well, probably errors because of a huge number of KPIs in the page, the complexity of the Flows used, and the number of concurrent users in the same Record Page (but not the same record) when there are many KPIs in it.

Anyway, I will keep you posted, if I see anything 😏.

 

Wrap-up

At this moment, you are already aware that this Component was designed as a general solution that can be used with any Standard or Custom Object, in their pages.

Think about that solving a problem like this (show KPIs in the record page) using an APEX approach, means a Trigger or something like that, where the developer has the opportunity to count the records and store it in a field. But, what about if you need the same for 10, 20 objects? Yes, you end up with a huge and complex system that is error prone and quite difficult to maintain.

With this Component, you develop it once (now it is more easy, the code is above 😉), and use it infinite times. Think about that the development of a Flow is very easy and an administrator can do it too. When there are problems, you don't need to wait for weeks because an expert developer (I always wonder if this exists) is checking the code to identify and solve the errors.

And more, think about this, if you have an actual requirement to show KPIs using a component like this, the Flows you create will be a copy of the first you did for the first KPI!. Even when you develop a Flow to count records related in a second, third level, the next time, if you need to do the same for other object, you can reuse the first Flow (and adapt it). 

Hence, keep it simple, use Components, use Flows. 

 

Resources and Acknowledgement

Salesforce Developers and Ligthning Design System where very useful to do this component.

 

What Next?

Yes, it is only a component that shows very small in the page and there is no way to show several KPI components in an horizontal list using a standard container (check the image at the top). Then, there should be other new component, that will allow us to have an horizontal bar of KPIs, side by side. I will write about it next time. 


"... the best code is no code at all. Every new line of code you willingly bring into the world is code that has to be debugged, code that has to be read and understood, code that has to be supported. Every time you write new code, you should do so reluctantly, under duress, because you completely exhausted all your other options ..."
Jeff Atwood, Stack Overflow co-founder 

Comments