Luke Van Epen
Tera Guru

The goal

This article will show walk you through how to set up a custom 0-100% progress dialog in the same format as the Commit update set, or ATF progress modals, for your own application.

I have attached to this article a small update set which contains a UI action that triggers the below progress bar for testing.

 

find_real_file.png

 

 Hidden APIs

There are 2 hidden APIs which power both of these native progress bars.

The first is the GlideScriptedHierarchicalWorker.

The best way to use this is wrapped in an Ajax script include, where the parameters for the worker object are passed in as Ajax parameters. All the parameters are strings.

Simplified usage (A real example will be shown later)

var worker = new GlideScriptedHierarchicalWorker();
worker.setProgressName("My Sleepy Progress Worker"); // The name of the progress bar, e.g. "My Sleepy Progress Worker"
worker.setScriptIncludeName("ProcessWorkerExample"); //The name of the Script Include which contains your long-running function
worker.setScriptIncludeMethod("start"); //The name of the method in your script include you want to run
worker.setBackground(true); // This is required for the process to be offloaded from the server,
// otherwise you wont be able to cancel,
// and the worker will occupy your session until it finishes
worker.start(); //Invokes the method and kicks off the whole thing

worker.getProgressID(); // Returns the sys_id of the progress worker, which is used to fetch updates and/or cancel the progress worker and underlying scripts

 The second is SNC.GlideExecutionTracker

This provides methods to control the progress of the worker and relay messages between server and client.

Simplified usage

var tracker = new SNC.GlideExecutionTracker(trackerId);// Using the worker.getProgressID() above, we can find the tracker to cancel it

tracker.incrementPercentComplete(1); //Add 1% to the current percent
tracker.success("Finished Process");// Finish the tracker with a message
tracker.cancel(gs.getMessage("Cancelled"));// Kill the script early with a message
tracket.updateMessage(gs.getMessage("Seconds passed: {0}",i)); // show some message under the progress bar that might change midway through
tracker.updateResult({
    recordsProcessed: 1000
});// send an object back to the client with data about the execution for additional info

 

Hidden UI Page

To handle the Cllient Side, we render an OOB UI Page named hierarchical_progress_viewer which cannot be seen through the normal UI Page list.

However, it is necessary to use this because it contains a series of methods built in that we will use to control things like cancel buttons and displaying updates from the server.

Client Side Boilerplate

You will normally have this run from a Client UI Action, but technically any client side code would work, whether that is catalog, clients scripts or even UI Policy scripts.

UI Action Setup

Client: True

On Click: displayProgressWorker(this);

Script:

/*  one tricky thing to keep in mind is that isolate_script needs to be disabled for 'this' to be passed in as 'control'
	this ui action is sorted, but if ya copy and paste the code and get 'function displayProgressWorker() does not exist' errors, shoulda read the comments
*/
function displayProgressWorker(control) {
    /* these variables are all you really need to change for basic progress dialogs */
    var progressWorkerName = 'My Sleepy Progress Worker';
    var scriptIncludeName = 'ProgressWorkerExample';
    var scriptIncludeMethod = 'start';
	
    var dd = new GlideDialogWindow("hierarchical_progress_viewer", false, "40em", "10.5em");
    dd.setTitle("Watch progress happen...");

    // it's easiest to just have one AJAX script include for all progress dialogs and provide parameters here (as Preferences)
    dd.setPreference('sysparm_ajax_processor', 'ProgressWorkerAJAX');
    dd.setPreference('sysparm_progressWorkerName', progressWorkerName);
    dd.setPreference('sysparm_scriptIncludeName', scriptIncludeName);
    dd.setPreference('sysparm_scriptIncludeMethod', scriptIncludeMethod);

    // Running in background allows the dialog to be closed without cancelling / prevents UI from becoming non-interactive
    // You can add a Cancel option even if setBackground is true, so might as well leave it as true
    dd.setPreference('sysparm_setBackground', true);
	
    dd.setPreference('sysparm_button_close', "Close");

    // there are multiple events. you don't need to use them all
    // this 'executionStarted' event is from Retrieve Completed Update Sets, but it's not required. Having a cancel button is nice tho
    dd.on("executionStarted", function(response) {
        var trackerId = response.responseXML.documentElement.getAttribute("answer");

        var cancelBtn = new Element("button", {
            'id': 'sysparm_button_cancel',
            'type': 'button',
            'class': 'btn btn-default',
            'style': 'margin-left: 5px; float:right;'
        }).update("Cancel");

        cancelBtn.onclick = function() {
            var dialog = new GlideDialogWindow('glide_ask_standard');
            dialog.setTitle("Confirmation");
            dialog.setPreference('warning', true);
            dialog.setPreference('title', "Are you sure you want to cancel this progress??");
            dialog.setPreference('defaultButton', 'ok_button');
            dialog.setPreference('onPromptComplete', function() {
                cancelBtn.disable();
                var ajaxHelper = new GlideAjax('ProgressWorkerAJAX');
                ajaxHelper.addParam('sysparm_name', 'cancel');
                ajaxHelper.addParam('sysparm_tracker_id', trackerId);
                ajaxHelper.getXMLAnswer(_handleCancelResponse);
            });
            dialog.render();
            dialog.on("bodyrendered", function() {
                var okBtn = $("ok_button");
                if (okBtn) {
                    okBtn.className += " btn-destructive";
                }
            });
        };

        var _handleCancelResponse = function(answer) {
            var cancelBtn = $("sysparm_button_cancel");
            if (cancelBtn)
                cancelBtn.remove();
        };

        var buttonsPanel = $("buttonsPanel");
        if (buttonsPanel)
            buttonsPanel.appendChild(cancelBtn);
    });

    dd.on("bodyrendered", function(trackerObj) {
        var closeBtn = $("sysparm_button_close");
        if (closeBtn)
            closeBtn.disable();
    });


    // 'executionComplete' is one you will want to use to present a completion message or just exit the dialog
    dd.on("executionComplete", function(trackerObj) {

        var closeBtn = $("sysparm_button_close");
        if (closeBtn) {
            closeBtn.enable();
            closeBtn.onclick = function() {
                dd.destroy();

            };
        }

        var cancelBtn = $("sysparm_button_cancel");
        if (cancelBtn)
            cancelBtn.remove();

        alert("All done sleeping for " + trackerObj.result.sleepTimeInSeconds + " seconds");

        // quit the dialog and reload the window
        dd.destroy();
        reloadWindow(window);
    });

    //render the viewer
    dd.render();
}

 

Most of this code has been lifted from other UI Actions in the system, you can review their implementations by searching for UI Actions which have hierarchical_progress_viewer somewhere in the script.

A couple of areas you may want to configure for your use case:

dd.on("executionStarted",...

This event allows you to set up a Cancel button, because it contains a response parameter which has the sys_id of the tracker inside. This can be sent to the server via an Ajax call to cancel a running process if needed.

If cancelling is not required or not possible in your case, you can remove this section of the script.

dd.on("executionComplete",...

This event is called when the progress worker finishes, the trackerObj parameter which is passed into the handler contains any results set on the server side with tracker.updateResult which can then be used to present extra information to the client beyond a static completion message.

 

 

ProgressWorkerAjax Script Include

This script is intentionally generic so that you don't have to modify much when creating new UI Actions, and it allows you to pass the parameters from the UI Action into GlideScriptedHierarchicalWorker methods without needing multiple Ajax script includes.

var ProgressWorkerAJAX = Class.create();
ProgressWorkerAJAX.prototype = Object.extendsObject(AbstractAjaxProcessor, {

	
    start: function() {
        var worker = new GlideScriptedHierarchicalWorker();
        worker.setProgressName(this.getParameter('sysparm_progressWorkerName'));
        worker.setScriptIncludeName(this.getParameter('sysparm_scriptIncludeName'));
        worker.setScriptIncludeMethod(this.getParameter('sysparm_scriptIncludeMethod'));
        worker.setBackground(true);
        worker.start();

        return worker.getProgressID();
    },

   
	cancel: function() {
		var trackerId = this.getParameter("sysparm_tracker_id");
		if (!trackerId)
			return false;

		var execTracker = new SNC.GlideExecutionTracker(trackerId);
		execTracker.cancel(gs.getMessage("Cancelled"));
	},


    type: 'ProgressWorkerAJAX'
});

 

Processor Script Includes

This is where you set up the script that you want to monitor the progress of. Everything else up to this point has just been boilerplate to make the progress bar work.

In this example, we will use a dummy method that simply sleeps periodically then updates the percentage after each sleep cycle.

In a real world use case, this could be something like updating or searching through a bunch of records, fetching multiple things via API or some other long running, but trackable process.

 

var ProgressWorkerExample = Class.create();
ProgressWorkerExample.prototype = {

    initialize: function() {
        //get the tracker so we can send progress updates
        this.tracker = SNC.GlideExecutionTracker.getLastRunning();

        if (!this.tracker.getSysID()) {
            this.tracker = null;
            return;
        }
    },

    start: function() {
        // in a real script, this could be the number of records you need to modify
        // or a count from a query that requires processing
        var timeToWasteInSeconds = 60;

        // start the index at 1, since it represents time / progress
        for (var i = 1; i <= timeToWasteInSeconds; i++) {

            /* this if/else calculates percentages and shouldn't require changing */
            if (timeToWasteInSeconds < 100) {
                //determine how many percent to increment for each interval
                var intervalPercent = Math.floor(100 / timeToWasteInSeconds);
                this.tracker.incrementPercentComplete(intervalPercent);
            } else {
                //determine number of things for one percent
                var onePercentInterval = Math.floor(timeToWasteInSeconds / 100);
                if (i % onePercentInterval == 0) {
                    //increment one percent more
                    this.tracker.incrementPercentComplete(1);
                }
            }
	        this.tracker.updateMessage(gs.getMessage("Seconds passed: {0}",i));

            // since we're in a loop, we will sleep and then increment our progress above before sleeping again
			// this is where you'd perform whatever activity you needed a progress indicator for
            this._sleepForOneSecond();
        }

		// progress has completed and was either successful or failed. 
		// pass the appropriate message through the tracker methods
        this.tracker.success("Finished sleeping for " + timeToWasteInSeconds + " seconds");
/*      this.tracker.fail("Grr couldn't even sleep for " + timeToWasteInSeconds + " seconds"); */

		// in addition to success/fail messages, you can pass whatever data you need as a js object
        this.tracker.updateResult({
            sleepTimeInSeconds: timeToWasteInSeconds
        });
    },


    _sleepForOneSecond: function() {
        gs.sleep(1000);
		gs.log("slept");
    },


    type: 'ProgressWorkerExample'
};

 

 

Comments
Rohail1
Tera Explorer

Great article Luke. Thanks a ton. One question: How to restart the percentage within the same progress bar window for another set of records. For example if we have list of records to process such and 1000 incidents and 900 problems. The same progress bar can be reset to start with the next set of records?

 

Luke Van Epen
Tera Guru

@Rohail1 don't know! sounds like an interesting challenge. I would probably either run them in the same batch or separate the executions completely. Restarting sounds difficult and is not one of the methods on the tracker. From a UI perspective you would have to wait for tracker 1 to finish, destroy the first one and then immediately trigger the second, though this would not be the same window, it would be 2 in quick succession. 

Interested to know if you come up with something better

Luke Van Epen
Tera Guru

For those who might be wondering how to create the multi-level progress bars,

This is actually very straightforward.

In the processor script include: 

var child_tracker = this.tracker.createChild('Subprocess with its own bar');

the child_tracker object in this case has the same methods available as the parent. 

Calling child_tracker.fail('some failure message') will bubble up the message to the parent tracker and fail the whole thing. 

 

In the UI Action (or client side script that renders the modal)

    dd.setPreference('sysparm_renderer_expanded_levels', 1);

 This will cause the progress bar to render with the first level expanded initially.

Guy Sagi
Tera Contributor

Hey Luke,

 

Great article, thanks for all the information!

 

I tried to create a similar Progress Bar as shown in your article. After following all the steps above, I realized I could not trigger the hidden hierarchical_progress_viewer UI Page from a scoped app. Do you happen to be familiar with any alternatives for scoped applications? Can a custom Progress Bar be created for a scoped app?

 

Thanks,

Guy

Antonio39
Tera Contributor

Hi Luke,

thanks for sharing your wiseness.

 

I successfully installed your update set in my PDI, tested it and works like a charm.

 

I'd like to adopt your solution to the following scenario:

in HR Agent Workspace i built a custom table, every row is a document to be downloaded: i'd like to select from 1 to n rows and show a progress bar for the download activity.

 

Any advice about it?

 

Thanks in advance.

芷萌张
Mega Explorer

Thank you for your post - the content was very helpful. I now have a new issue: I've tried all the methods mentioned above. The Core UI works fine, but the workspace keeps throwing errors.

Luke Van Epen
Tera Guru

@芷萌张 I don't use workspaces, they have a different client side API I'm not familiar with. You would probably have more luck creating your own component in UI Builder than trying to get this to work in workspaces.

Version history
Last update:
‎02-03-2022 09:51 PM
Updated by: