An Example of a Custom Interactive Filters for Dashboards

Creating a custom scripted filter to make a dashboard more dynamic, in those cases when out-of-the-box filters just aren't enough.

Table of Contents

The limitations of standard filters

In my previous post, I showed you how you can leverage a database view to report on multiple task types in the same widget. For the Performance Analytics Widgets, we can use the dashboard breakdown to filter the widgets. That might not work for regular reporting widgets though, reporting widgets can only be filtered using interactive filters. However, the interactive filters we get out of the box can only filter on the following field types:

  • choices fields (which reference the sys_choice table)
  • reference fields
  • date fields
  • boolean fields.

The “sys_class_name” (Task Type) field on task, and in our database view, is a special type of field. It only contains the name of the system table the record belongs to. By joining the sys_db_object table to our database view, we can also include the sys_id of the table. However, a sys_id field still isn’t a field type that is supported by interactive filters (reference, boolean, choice, date).

We could work around this by adding a field to the task table. The field would be a reference field to the sys_db_object record and be populated by a business rule running on insert. That would result in a reference field for the task type that can be filtered by a standard interactive filter.

But if you don’t want to modify the task table, there is another way. We can build a custom interactive filter using content blocks and jelly.

Creating a filter

We will be basing our filter on an example by Arnoud Kooi.

We start by adding a dynamic content block to our dashboard by navigating to the “Content Block” category and selecting “*New Dynamic Content”:

Click the “Click Here” link in the added widget:

This takes us to the input form for the dynamic content block where we can start by giving it a name.

We then paste the following code into the script field. Notice that at line 5 we are limiting the options available to the ones we want to be able to filter for.

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<!-- By Arnoud Kooi Check https://youtu.be/I2eDVi9CAWI -->
<g:evaluate var="jvar_tasktypes" object="true" jelly="true">
var tables = 'incident,problem,change_request,sc_req_item,sc_task'; 
var obj=[];
var gr= new GlideRecord('sys_db_object');
  gr.addEncodedQuery('super_classISNOTEMPTY^ORname=task^nameIN' + tables);
  gr.addOrderBy('name');
  gr.query();
  while(gr.next()){
    obj.push([gr.getValue('name'),gr.getValue('label')]);
  }
  obj;
</g:evaluate>
 
    <select id='filter_task_type' class='select2-search' onchange='filterTaskType()'>
        <option value="">All</option>
        <j:forEach var="jvar_tasktype" items="${jvar_tasktypes}"> 
            <option value="${jvar_tasktype[0]}">${jvar_tasktype[1]}</option>        
        </j:forEach>
    </select>   
 
    <script>
     var dbh = new DashboardMessageHandler("filter_tasktype");
     function filterTaskType(){
        var taskType = $j('#filter_task_type').val();
        if (taskType)
            dbh.publishFilter('u_task_extended','task_sys_class_name=' + taskType);
        else
            dbh.removeFilter();
     }
     filterTaskType();
</script>
 
</j:jelly>

Click submit. This adds the filter to the dashboard. It looks like this:

We can then add a list report to our dashboard and set it to follow our filter by clicking the gear icon in the widget header and marking “Follow Interactive Filter”. This will let us filter the list using the filter widget:

As you can see, the filter applies and updates the list report.

Unfortunately, the drop down list is not recorded, but you get the point

How does the filter work?

Now we have a working filter. But it would be good if you actually understand how the script, and filter, works. Let’s go over each part:

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">

The first lines defines the xml version and encoding, and then sets the namespaces for the jelly and glide files so they can be used in the script. Jelly is a scripting language that lets us generate HTML from XML.

<g:evaluate var="jvar_tasktypes" object="true" jelly="true">

This line tells us we are starting an expression that will be evaluated on the server. The script returns a variable called “jvar_tasktypes”. The variable is an object and because “jelly” is set to true, the variable is available for use in a client side script later in the code.

var tables = 'incident,problem,change_request,sc_req_item,sc_task'; 
var obj=[];

Two variables are then created, “tables” containing a string of comma-seperated table names and an empty array named “obj”.

var gr= new GlideRecord('sys_db_object');
  gr.addEncodedQuery('super_classISNOTEMPTY^ORname=task^nameIN' + tables);
  gr.addOrderBy('name');
  gr.query();
  while(gr.next()){
    obj.push([gr.getValue('name'),gr.getValue('label')]);
  }
obj;

GlideRecord is an object that will query the sys_db_object table to return properties of records in it.

An encoded query is then added to the GlideRecord. The query specifies that “Extends tabel” (super_class) is not empty, meaning that we will only get names of those tables that extend other tables. There is then a condition to also include the task table (which does not extend a table).
The last part of the query adds the condition to only return records where the name field matches one of the tables specified in the “table” variable. The first two parts of the query could actually be removed, we only need the part that limits the results to those included in our tables-variable.
The results from sys_db_object will be sorted by their names thanks to gr.addOrderBy(‘name’);.

The query is then executed by calling the .query() function.
A loop is initialized by the while-statement. This loop will execute while there are results in the GlideRecord. This means that for each records found by the query, something will happen.

That something is that the “name” and “label” of each record will be added into an array, and that array will be added into the previously empty array called “obj”. So obj will be an array containing multiple other arrays:

ob[
[change_request,Change Request],
[incident,Incident],
[sc_req_item,Requested Item]
]

The final line returns the "obj" array for use in the script.
    <select id='filter_task_type' class='select2-search' onchange='filterTaskType()'>
        <option value="">All</option>
        <j:forEach var="jvar_tasktype" items="${jvar_tasktypes}"> 
            <option value="${jvar_tasktype[0]}">${jvar_tasktype[1]}</option>        
        </j:forEach>
    </select> 

In this part of the script, a drop-down box is added using HTMLs <select> tag. It has a class (select2-search) for CSS styling. When a selection is made, a function called filterTaskType() will be executed.

A forEach-loop using jelly is then started (j:forEach). It iterates through each of the items in the variable jvar_tasktypes. Remember, this variable was set at the start of the script to contain names and labels for task types from the sys_db_object table. For every item in the array, an option is added to the drop down. The label from the array is set as the display value while the name from the array becomes the actual selected value.

    <script>
     var dbh = new DashboardMessageHandler("filter_tasktype");
     function filterTaskType(){
        var taskType = $j('#filter_task_type').val();
        if (taskType)
            dbh.publishFilter('u_task_extended','task_sys_class_name=' + taskType);
        else
            dbh.removeFilter();
     }
     filterTaskType();
</script>

This part of the script is the javascript that is run client side, meaning in the user’s browser.

On the second line, we can see that a object is created from the DashboardMessageHandler class. It is given a unique ID (filter_tasktype) and assigned to a variable called “dbh”.
DashhoardMessageHandler is a ServiceNow class that can publish filters to other reporting widgets on the dashboard. You can view the entire class and all of it’s methods by accessing:
http://your-servicenow-instance/scripts/classes/DashboardMessageHandler.js

After the DashboardMessageHandler object is created, the function referenced previously is defined; filterTaskType(). This function will run each time the selection in the drop down box is changed. The function creates a variable called taskType and assigns it the value selected in the drop down. If there is a value, the publishFilter() method of the DashboardMessageHandler class is called. This class takes a table name as the first input (in this case set to the name of our database view, u_task_extended) and a encoded query string as the second parameter. The encoded query is made up of a static string combined with the value of the “taskType” variable, meaning the value selected in the drop down.

If the value selected in the drop down is empty, which it will be when selecting “All”, as no value was specified for this option in the earlier part of the script, the method removeFilter() of the DashboardMessageHandler object will be called.

The methods of DashboardMessageHandler both fire events when called. These events are sent to all reporting widgets on the dashboard tab, and if the widget is set to follow interactive filters, it listens for these events. If the report in the widget is based on the same table as the first parameter of the publishFilter() method, the widget will apply this filter.

Updating the filter to match Orlando functionality

Now, when I first tried the filter I thought it worked fine, as you can see in the short video below. But then I noticed that if I left the dashboard and navigated back to it, or reloaded the page, or opened the dashboard in a new browser tab – the filter stopped working. It seemed like it only worked in the first rendering of the dashboard.

I then noticed that this could be fixed by opening the dashboard context menu and selecting “Reset Filters”. See the video below for a demonstration of filters working, then not working after a page refresh, then working again after a filter reset. Notice also how the debug widget is always updating with the correct filter. It’s just the reporting widgets that wont apply it.

Now this is a little strange. The ServiceNow documentation specifically mentions that custom interactive filters are not retained between page loads. It also claims that when a report is refresh using the refresh-icon, the filter is removed. So a reload of the page should remove the saved filter, but that is apparently not the case.

The doc page for Custom Interactive Filters was updated for the Orlando release to include the following information:

The code that publishes the filter must call the SNC.canvas.interactiveFilters.setDefaultValue() method. It must also call the dashboardMessageHandler.publishFilter() method to publish the filter.

https://docs.servicenow.com/bundle/orlando-now-intelligence/page/use/dashboards/concept/c_CustomPublishers.html

The example custom interactive filter on the doc site has also been updated to include a call to SNC.canvas.interactiveFilters.setDefaultValue(). So even though the docs claim that interactive filters are not saved, that is no longer true since the Orlando release.

If we update our interactive filter to call the SNC.canvas.interactiveFilters.setDefaultValue() method, which requires us to put our filter into an object passing that object to the method, the bug is resolved. We need to call this method both when changing the filter to a new task type, as well as when showing all types. It is not clear to me exactly where this default value that needs to be cleared is saved, but setDefaultValuie() defined in:
http://your-instance-url/scripts/utils/InteractiveFilterUtils.js

        setDefaultValue: function(val, isPersistEnabled) {
            if (JSON.stringify(val.filters) !== JSON.stringify(this.emptyFilter))
                this.defaultValues[val.id] = val.filters;
            else {
                delete this.defaultValues[val.id];
                if (val.filters.selectedFilters) {
                    var filters = {};
                    filters.selectedFilters = val.filters.selectedFilters;
                    val.filters = filters;
                    this.defaultValues[val.id] = filters;
                }
            }
            if (isPersistEnabled) {
                this.saveDefaultValue(SNC.canvas.layoutJson.canvasSysId, {
                    id: val.id,
                    name: "default_value",
                    filter: val.filters
                }, SNC.dashboards.dashboard.dashboardSysId);
            }
        },

After we’ve added the new call to setDefaultValue(), our script should look like this, and function correctly:

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<!-- By Arnoud Kooi Check https://youtu.be/I2eDVi9CAWI -->
<g:evaluate var="jvar_tasktypes" object="true" jelly="true">
var tables = 'incident,problem,change_request,sc_req_item,sc_task'; 
var obj=[];
var gr= new GlideRecord('sys_db_object');
  gr.addEncodedQuery('super_classISNOTEMPTY^ORname=task^nameIN' + tables);
  gr.addOrderBy('name');
  gr.query();
  while(gr.next()){
    obj.push([gr.getValue('name'),gr.getValue('label')]);
  }
  obj;
</g:evaluate>
 
    <select id='filter_task_type' class='select2-search' onchange='filterTaskType()'>
        <option value="">All</option>
        <j:forEach var="jvar_tasktype" items="${jvar_tasktypes}"> 
            <option value="${jvar_tasktype[0]}">${jvar_tasktype[1]}</option>        
        </j:forEach>
    </select>   
 
    <script>
     var dbh = new DashboardMessageHandler("filter_tasktype");
     function filterTaskType(){
        var taskType = $j('#filter_task_type').val();
        
		 var filter_message = {};
		 filter_message.id = "taskFilter"
		 filter_message.table = "u_task_extended"
         
		if (taskType) {
			filter_message.filter = "task_sys_class_name="+taskType;	 
			SNC.canvas.interactiveFilters.setDefaultValue({
				id: filter_message.id,
                filters: [filter_message]
            }, false);
		 dbh.publishFilter(filter_message.table, filter_message.filter);
		}
        else {
		filter_message.filter = "";	 
			SNC.canvas.interactiveFilters.setDefaultValue({
				id: filter_message.id,
                filters: [filter_message]
            }, false);
			dbh.removeFilter();
		}
     }
     filterTaskType();
</script>
 
</j:jelly>

Leave a Reply

Your email address will not be published.