Batch Class Progress Indicator In LWC

Batch class can be invoked dynamically from LWC. We are going to build a batch class progress indicator. Generally, To monitor or stop the execution of the batch Apex job, from Setup, enter Apex Jobs in the Quick Find box, then select Apex Jobs. In some cases, Users can’t be provided with view setup permission. Hence, We are providing a way to view batch class progress indicator directly within LWC.

Batch class can be invoked dynamically from LWC. We are going to build a batch class progress indicator. Generally, To monitor or stop the execution of the batch Apex job, from Setup, enter Apex Jobs in the Quick Find box, then select Apex Jobs. In some cases, Users can't be provided with view setup permission. Hence, We are providing a way to view batch class progress indicator directly within LWC.

Batch Class Progress Indicator LWC Code

Let’s create a Lightning Web Component with name batchProgressIndicator, an apex controller with name batchProgressIndicatorController and a batch class with name BatchApexRecipes.

batchProgressIndicatorController.cls

There are two methods in this class i.e. getJobDetails and executeBatch.

getJobDetails – It query on AsyncApexJob object and returns the list of matching records to view batch progress.

executeBatch – We can excute the any batch class dynamically by just passing the name of the class and size.

public with sharing class batchProgressIndicatorController {
    @AuraEnabled
    public static List<AsyncApexJob> getJobDetails(String jobId){
        try {
            List<AsyncApexJob> asyncJobList = [SELECT Id, Status, JobItemsProcessed, TotalJobItems,
                                                NumberOfErrors FROM AsyncApexJob where Id =: jobId ];
            return asyncJobList;
        } catch (Exception e) {
            throw new AuraHandledException(e.getMessage());
        }
   }

   @AuraEnabled
   public static string executeBatch(String className, Integer chunkSize){
        try {
            Type batchInstance = Type.forName(className);
            Id batchJobId = Database.executeBatch((Database.Batchable<sObject>)batchInstance.newInstance(), chunkSize);
            return batchJobId;
        } catch (Exception e) {
            throw new AuraHandledException(e.getMessage());
        }
   }
}

BatchApexRecipes.cls

This is a super simple batch class from famous apex recipes which shows the use of the Database.Batchable interface. The methods in this class are called by the system as the batch runs.

/**
 * @description Demonstrates the use of the Database.Batchable interface. The
 * methods in this class are called by the system as the batch executes.
 * To execute this batch use `Database.executeBatch(new BatchApexRecipes());`
 *
 * More on the Batchable interface:
 * https://sfdc.co/batch_interface
 *
 * @group Async Apex Recipes
 */
public with sharing class BatchApexRecipes implements Database.Batchable<SObject>, Database.Stateful {
    // These next two lists hold the ids of sucessful and failed updates.
    private List<Id> successes = new List<Id>();
    private List<Id> failures = new List<Id>();
    // A constant representing the initial query to run.
    // This is used in the start() method.
    private final String queryString = 'SELECT Id, Name FROM Account';

    // Having a static variable here, let's us test the output of the Finish
    // method below
    @testVisible
    private static String result = '';
    // This allows us to cause a DML failure in execute batch, enabling testing.
    @testVisible
    private Boolean throwError = false;

    /**
     * @description This method is required by the Batchable interface.
     * It's responsible for identifying the records that will be affected
     * Your start method can either return a QueryLocator or an Iterable
     * (List) The records identified here will be made available to the
     * execute method below, in batches of up to 200 records at a time.
     * @param context dependency injected by the system
     * @return Database.QueryLocator QueryLocator object used for context
     * @example
     * Database.executeBatch(new BatchApexRecipes());
     **/
    public Database.QueryLocator start(Database.BatchableContext context) {
        return Database.getQueryLocator(queryString);
    }

    /**
     * @description This method is where the actual work occurs. It's run once
     * per batch.
     * @param context dependency injected by the system in this batch. It's this
     * mechanism of breaking a large number of records into smaller batches
     * called scope (in this example) that make this easier.
     * @param scope a list of up to 200 SObject records to be processed
     * @example
     * Database.executeBatch(new BatchApexRecipes());
     **/
    public void execute(
        Database.BatchableContext context,
        List<Account> scope
    ) {
        // Batch Processing
        for (Account acct : scope) {
            acct.Name += ' Edited by Batch class';

            /**
             * This is an example of a circuit breaker pattern. It's not
             * something I routinely recommend, but it has it's uses. Here we're
             * using it to intentionally cause the save result to fail in a way
             * that will provide us actual DML error messages. Flipping the
             * breaker, allows us to test the finish() method of this batch
             * class and illustrate how it works.
             */
            if (this.throwError) {
                acct.Name = null;
            }
        }
        /**
         * it's useful to use the Database.* methods inside Batch classes to
         * separate succeses and failures. But the tracking of these across many
         * scopes is only possible when your batch class also extends
         * Database.stateful. Without implementing Database.Stateful the
         * successes and failures class variables would be *reset* for every
         * batch.
         */
        List<Database.SaveResult> saveResults = new List<Database.SaveResult>();
        saveResults = Database.update(scope, false);
        for (Database.SaveResult sr : saveResults) {
            if (sr.isSuccess()) {
                // Only available *across* scope executions because of
                // Database.stateful
                successes.add(sr.id);
            } else {
                // Only available *across* scope executions because of
                // Database.stateful
                failures.add(sr.id);
            }
        }
    }

    /**
     * @description This method is called by the system when all the individual
     * batches have completed. Intrepid developers may send emails, or otherwise
     * notify others of the job's completion here.
     * @param context dependency injected by the system
     * @example
     * Database.executeBatch(new BatchApexRecipes());
     **/
    public void finish(Database.BatchableContext context) {
        BatchApexRecipes.result =
            'Successes: ' +
            successes.size() +
            ' Failures: ' +
            failures.size();
    }
}

batchProgressIndicator.html

HTML file of the Batch Class Progress lwc has :-

  1. Input field accepts the batch class name and chunk size
  2. Button to get the status of batch running and refresh the status
  3. Progress bar to show the completion percentage in LWC
<template>
    <lightning-layout>
        <lightning-layout-item size="12" small-device-size="6" medium-device-size="4" large-device-size="6"
            padding="around-small">
            <lightning-input label="Enter Batch Class Name" variant="label-inline"
                onchange={handleBatchNameChange}></lightning-input>
        </lightning-layout-item>
        <lightning-layout-item size="12" small-device-size="6" medium-device-size="4" large-device-size="4"
            padding="around-small">
            <lightning-input label="Enter Batch Size" variant="label-inline" type="number"
                onchange={handleBatchSizeChange}></lightning-input>
        </lightning-layout-item>
        <lightning-layout-item size="12" small-device-size="6" medium-device-size="4" large-device-size="2"
            padding="around-small">
            <lightning-button label="Execute Batch" variant="brand" onclick={handleExecuteBatch}
                class="slds-p-top_large" disabled={disableExecuteBatch}></lightning-button>
        </lightning-layout-item>
    </lightning-layout>
    <lightning-layout if:true={batchJobId}>
        <lightning-layout-item size="12" small-device-size="6" medium-device-size="4" large-device-size="6"
            padding="around-small">
            <lightning-input label="Generated Batch Id" value={batchJobId} disabled variant="label-inline"
                class="slds-p-bottom_medium"></lightning-input>
        </lightning-layout-item>
        <lightning-layout-item size="12" small-device-size="6" medium-device-size="4" large-device-size="2"
            padding="around-small">
            <lightning-button label="Get Batch Status" variant="Success" disabled={executedBatch}
                onclick={getBatchStatus}></lightning-button>
        </lightning-layout-item>
        <lightning-layout-item size="12" small-device-size="6" medium-device-size="4" large-device-size="2"
            padding="around-small">
            <lightning-button label="Refresh" variant="brand" onclick={handleRefreshView}></lightning-button>
        </lightning-layout-item>
    </lightning-layout>
    <lightning-layout if:true={batchJobId}>
        <lightning-layout-item size="12" small-device-size="12" medium-device-size="12" large-device-size="12"
            padding="around-small">
            <div class="slds-p-top_large">
                Batch Percentage Executed: <b>{executedPercentage}%</b>
            </div>
            <div class="slds-p-top_large">
                <lightning-progress-bar value={executedIndicator} size="large"></lightning-progress-bar>
            </div>
            <div class="slds-p-top_large">
                Batch Executed <b>{executedBatch}</b> of <b>{totalBatch}</b>
            </div>
        </lightning-layout-item>
    </lightning-layout>
</template>

batchProgressIndicator.js

handleBatchNameChange – This is onchange handler for input with label Enter Batch Class Name

handleBatchSizeChange – This is onchange handler for input with label Enter Batch Size

handleExecuteBatch – This is onclick handler of button with label Execute Batch

getBatchStatus – This is onclick handler of button with label Get Batch Status

handleRefreshView – This is onclick handler of button with label Refresh

import { LightningElement } from 'lwc';
//importing the apex methods
import getJobDetails from '@salesforce/apex/batchProgressIndicatorController.getJobDetails';
import executeBatch from '@salesforce/apex/batchProgressIndicatorController.executeBatch';

export default class BatchProgressIndicator extends LightningElement {
    batchJobId;
    executedPercentage;
    executedIndicator;
    executedBatch;
    totalBatch;
    isBatchCompleted = false;
    batchClassName;
    batchSize;
    disableExecuteBatch = false;

    handleBatchNameChange(event) {
        this.batchClassName = event.currentTarget.value;
    }

    handleBatchSizeChange(event) {
        this.batchSize = parseInt(event.currentTarget.value);
    }

    //execute the batch class
    handleExecuteBatch() {
        this.disableExecuteBatch = true;
        executeBatch({
            className: this.batchClassName,
            chunkSize: this.batchSize
        }).then(res => {
            console.log('response => ', res);
            if (res) {
                this.batchJobId = res;
                //this.getBatchStatus();
            }
        }).catch(err => {
            console.log('err ', err);

        })
    }
    
    //get the batch status
    getBatchStatus() {
        getJobDetails({ jobId: this.batchJobId }).then(res => {
            console.log('response => ', res);
            if (res[0]) {
                this.totalBatch = res[0].TotalJobItems;
                if (res[0].TotalJobItems == res[0].JobItemsProcessed) {
                    this.isBatchCompleted = true;
                }
                this.executedPercentage = ((res[0].JobItemsProcessed / res[0].TotalJobItems) * 100).toFixed(2);
                this.executedBatch = res[0].JobItemsProcessed;
                var executedNumber = Number(this.executedPercentage);
                this.executedIndicator = Math.floor(executedNumber);
                // this.refreshBatchOnInterval();  //enable this if you want to refresh on interval
            }
        }).catch(err => {
            console.log('err ', err);

        })
    }

    handleRefreshView() {
        this.getBatchStatus();
    }

    refreshBatchOnInterval() {
        this._interval = setInterval(() => {
            if (this.isBatchCompleted) {
                clearInterval(this._interval);
            } else {
                this.getBatchStatus();
            }
        }, 10000); //refersh view every time
    }
}

batchProgressIndicator.js-meta.xml

Batch Class Progress component is exposed to lightning record page, home page and app page. We can extend this as per our business needs.

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>55.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__HomePage</target>
        <target>lightning__RecordPage</target>
        <target>lightning__Tab</target>
    </targets>
</LightningComponentBundle>

Demo – Batch Class Progress Indicator

Do you need help?

Are you stuck while working on this requirement? Do you want to get review of your solution? Now, you can book dedicated 1:1 with me on Lightning Web Component completely free.

GET IN TOUCH

Schedule a 1:1 Meeting with me

Also check out https://salesforcediaries.com/category/lightning-web-compont

Leave a Reply