Building a custom script Search widget

General Add comments
by:


The flexible platform that ServiceNow is has more than once challenged my capabilities of analysis. Especially errors that occur during the development of complex functionalities for a customer can sometimes be challenging.

JSLint will occasionally throw warnings and other log statements that unbeknownst to the user are actually small coding mistakes. Making mistakes is a human characteristic, so this isn’t necessarily bad, but it would be a waste to not try and fix them.

org.mozilla.javascript.EcmaError: “security_admin” is not defined.
Caused by error in <refname> at line 1

==> 1: security_admin

The above warning is not killing the system, nor is it influencing the functionality. Tackling it could however improve performance or fix some unknown missing functionality. I’ve been tasked in the past with debugging assessments on multiple ServiceNow instances and felt the log was lacking in a method of quickly finding the culprit in statements like this. I therefore coded a little solution for this myself which consists of 3 separate parts:

  • Script include
  • UI page
  • Adding it to a widget (bonus)

I’ll explain the basics of the components in both scripts, and give a step-by-step guide through the script. As this was sort of a nifty challenge for me, I disregarded the way this can be done with Global Search as well by setting up a new search group. Also note that the widget will basically be able to dynamically create the search groups, based on the element types looking for. You should however consider this fact when implementing the update set.

Ok, let’s get started.

Note:
You can download a zip file containing an Update Set using the download button below.
[download id=”1″]

Creating the script include

The script include will contain the bulk of the script. We will be making an AJAX call to this script containing the keyword we will be looking for, and as a return we want it to give all the records found across all tables that contain the provided keyword.

That being said, we actually need to perform multiple queries to get the correct results: one query that will look for the keyword in the set of data, and the other one to find the set of data. As this script will be dynamically searching through all the fields, we need to setup a dynamic query for the set.

NOTE: It is currently created as a script search widget, but it could be extended to search through incidents and other functionalities as well. This script include also has not been optimized. Future versions might include the ability to look for scripts by direct access to parameters, including properties for storing data and manipulating the looks, and better feedback for the UI for mismatches in data / query, as well as changing the formatting of the AJAX response. Since I’m quite busy I challenge you to build these features yourself for now…

  • Create the record

Create a script include named ‘ScriptSearchForm‘, and make it active and client callable.

  • Create the object

    As a general rule of thumb for Javascript, objects can be used as containers of certain functionality in the system, and have inheritance from their parent objects. Since we need to call it via AJAX, we will use the AbstractAjaxProcessor object as parent. Furthermore we will initiate several properties to contain data from the calculations we will make in the object.

    var ScriptSearchForm = Class.create();

    ScriptSearchForm.prototype = Object.extendsObject(AbstractAjaxProcessor, {

    objCreateSearch: method () {

    this._strKeywords = this.getParameter(‘keywords’);

    this._strElements = ”;

    this._strQuery = ”;

    this._count = 0;

    this._arrTables = []; // tablenames that were found from the sString

    this._arrElements = [‘script’, ‘xml’, ‘default_value’, ‘client_script’, ‘processing_script’]; //what types to use for querying for fields, default refers to script

    this._strReturn = ”;

    this._prepDictionarySearchString();

    this._prepQueryString();

    this._findAllTableNames();

    return this.search();

    },

     

    Properties can be overwritten to allow for a more concise object, but I’m a fan of debugging using multiple variables, instead of just one, which is why I used several containing similar information. Performance-loss is negligible.

    A list of properties and the relevant explanation:

    Attribute name Explanation
    _strKeywords Contains a string of the keywords that we want to find in the database.
    _strElements Contains an ‘encoded’ query formatted string with all the elements that we want to look for. We will use a dynamically created encoded query to make this possible.
    _strQuery Contains an ‘encoded’ query formatted string with all the keywords that we are searching, as well as the elements the keywords are contained in.
    _count Contains an integer with the amount of records found.
    _arrTables Contains the tables that have been found in the initial query against the dictionary.
    _arrElements Contains an array of stringed elements that are used in _strElements. This list defaults to possible scripts located in ServiceNow.
    _strReturn Contains the final return that will be processed by the client side script on the UI page.

    The following methods will be started during object initiation:

    Method Explanation
    _prepDictionarySearchString() This method will prepare _strElements, which will contain all elements that need to be checked against the dictionary table.
    _prepQueryString(); This method will prepare the actual query string (_strQuery), that is an encoded string that will be used by _findAllTableNames().
    _findAllTableNames(); This method will perform the actual query from _prepQueryString() against the dictionary table to fill the _arrTables array.
    search(); When all data is collected, this particular method will prepare the remainder of the information and prepare to return it to the UI Page. There are also several submethods triggered by this method to make this happen. Furthermore, the format in which this method returns the actual values is a string based with separators. It is string-based because of the lacking ability to transfer native objects from Java to Javascript.

    The explanation for these methods will be split into, where search() will receive it’s separate attention of the rest…

    Preparation methods

    As the script will be using the addEncodedQuery functionality of the GlideRecord class, in these methods we will be preparing a string in the proper syntax in order to find the necessary data:

    1. _prepDictionarySearchString : preparing for the initial query

    _prepDictionarySearchString: function () {

    for (var x = 0; x <= this._arrElements.length – 1; x++) {

    if (x < this._arrElements.length – 1) {

    this._strElements += ‘element=’ + this._arrElements[x] + ‘^OR’;

    } else {

    this._strElements += ‘element=’ + this._arrElements[x];

    }

    }

    },

     

    This method will loop through all the elements that have been set in the original array, and turn them into an encoded query script string. Now we have the elements in a properly formatted string we can continue with the preparation of another encoded query string.

    2. _prepQueryString : adding keywords to the query

    _prepQueryString: function () {

    if (this._strKeywords && this._strKeywords.length() > 0) {

    if (this._arrElements.length > 0) {

    for (var x = 0; x <= this._arrElements.length – 1; x++) {

    if (x < this._arrElements.length – 1) {

    this._strQuery += this._arrElements[x] + ‘LIKE’ + this._strKeywords + ‘^OR’;

    } else {

    this._strQuery += this._arrElements[x] + ‘LIKE’ + this._strKeywords;

    }

    }

    if (!this._strQuery.length > 0) {

    return ‘No query string found’;

    }

    }

    } else {

    return ‘At least one keyword is required to search’;

    }

    },

     

    This function creates an encoded string that contains both the elements as well as the keywords we were looking for. It will be used against the tables we find in the next function.

    3. _findAllTableNames : finding the results of the initial query

    _findAllTableNames: function () {

    if (this._strElements && this._strElements.length > 0) {

    var grDictionary = new GlideRecord(‘sys_dictionary’);

    grDictionary.addEncodedQuery(this._strElements);

    grDictionary.query();

    if (!grDictionary.next()) {

    return ‘No tables found containing mentioned elements’;

    }

    while (grDictionary.next()) {

    var elemName = grDictionary.name.toString();

    if (elemName.indexOf(‘var__m’) == -1) {

    this._arrTables.push(elemName);

    }

    }

    }

    },

     

    This function will find all the tables that have any of the elements in them, using the formatted string. The result will be a filled arrTables array, containing all the tables that need to be queried for the keyword in the elements.

    All we have done up to here were simple preparations for the actual query. We created a string to query for the tables (1), a string to query for querying the tables for the keywords we want to look for (2) and finally found all the tables that have the elements in them (3), using the string created at (1).

    All that is left now is to use the data that was prepared to query for the results.

    Search method

    The largest method contains the most checks and results in formatted string data that will be read by the UI page.

    4. _searchResult : contains the actual query and some checking functions

    _searchResult: function () {

    if (this._strQuery != ” && this._arrTables.length > 0) {

    var t1 = new Date().getTime();

    for (var t = 0; t < this._arrTables.length; t++) {

    var grValidField = new GlideRecord(this._arrTables[t].toString());

    q = this._setValidQuery(grValidField);

    //isValidField() destroys the gr, which means we need to create a new object.

    var grSearch = new GlideRecord(this._arrTables[t].toString());

    if (q != false) {

    grSearch.addEncodedQuery(q);

    } else {

    break;

    }

    grSearch.query();

    while (grSearch.next()) {

    if (grSearch.isValidField(‘name’)) {

    this._prepReturnString(grSearch.sys_id, grSearch.getTableName(), grSearch.name);

    } else if (grSearch.isValidField(‘number’)) {

    this._prepReturnString(grSearch.sys_id, grSearch.getTableName(), grSearch.number);

    } else {

    this._prepReturnString(grSearch.sys_id, grSearch.getTableName(), grSearch.sys_id);

    }

    this._count++;

    }

    }

    if (this._strReturn.length == 0) {

    this._strReturn = ‘No results matched’;

    return this._strReturn;

    } else {

    var t2 = new Date().getTime();

    this._finalizeReturnString(t1, t2); //’\n Amount of records found: ‘ + count + ‘\n Runtime: ‘ + (t2-t1) + ‘ milliseconds\n Search Results:\n’ + returnString + ‘</table>’;

    return this._strReturn;

    }

    } else if (!this._strQuery) {

    return ‘No query string found’;

    }

    },

    _setValidQuery: function (mp) {

    //Not every table has all fields available, so we need to determine what part of the query we can perform.

    //Has overlap with prepQueryString, which needs to be fixed.

    var pat = /OR$/;

    var qString = ”;

    for (var x = 0; x < this._arrElements.length; x++) {

    if (mp.isValidField(this._arrElements[x]) && x != this._arrElements.length) {

    qString += this._arrElements[x] + ‘LIKE’ + this._strKeywords + ‘^OR’;

    } else if (mp.isValidField(this._arrElements[x]) && x == this._arrElements.length) {

    qString += this._arrElements[x] + ‘LIKE’ + this._strKeywords;

    }

    }

    if (qString.search(pat) != -1) {

    qString = qString.slice(0, qString.search(pat) – 1);

    return qString;

    }

    if (qString.length == 0) {

    return false;

    }

    },

    _prepReturnString: function (sys_id, classname, name) {

    this._strReturn += ‘^^https://’ + gs.getProperty(‘instance_name’) + ‘.service-now.com/nav_to.do?uri=’ + classname + ‘.do?sys_id=’ + sys_id + ‘||’ + name + ‘~https://’ + gs.getProperty(‘instance_name’) + ‘.service-now.com/nav_to.do?uri=’ + classname + ‘_list.do|’ + classname;

    },

    _finalizeReturnString: function (t1, t2) {

    this._strReturn = this._count + ‘||’ + (t2 – t1) + ‘##’ + this._strReturn;

    },

     

    Initially we set the time, so we can figure out how long the query took the system. Then we need to figure out which tables to query and for what, otherwise the system will return an error. We need to figure this out for every element we’ve found, so we will use _setValidQuery for that. We pass the initial GlideRecord, and use .isValidField from the GlideRecord class to determine whether the field is found on the record. If so, we can add it to the string, if not we should skip it.

    As the strings will be built with ‘or’ it is possible that there will be a trailing ‘^OR’, which needs to be removed from the string (otherwise the query will not work). A simple pattern search will do that. If we do not find any results, a return of false should inform us on that.

    The result will be a ‘q’ variable, which will either be false or contains a string with a valid query string. If it is false, we do not need to query, otherwise the query starts, and we can start on the preparation of the data to be returned to the user.

    Now grSearch contains the results. We will use that instance to format the data so we can read it in the UI page. We have to do this for every record we found, which means two more functions are needed to do this:

    1. _prepReturnString : which prepares the data per record
    2. _finalizeReturnString : which prepares the set of records and includes the performance times

    Note: The buildup of _prepReturnString can be changed to include other parameters as well, like active, or updated time.

    Finally, the last part of the script will add the public function that will be used to call it. This will be the function used when clicking on the button to start the search.

    5. search: the public call

    //Public functions

    search: function () {

    return this._searchResult();

    }

     

    There is not really that much to say about this one. It just returns the data we find from searching.

    UI Page

    Now that we are ready for receiving our results, we want it nicely presented in a widget. We need a UI page to display the results, and need to create a widget to make it searchable. We will not be using the processing script as we will do everything client side. The wonders of AJAX help us with this.

    Let’s take a look at the UI page code.

    HTML

    <?xml version=”1.0″ encoding=”utf-8″ ?>

    <j:jelly trim=”false” xmlns:j=”jelly:core” xmlns:g=”glide” xmlns:j2=”null” xmlns:g2=”null”>

    <table>

    <tr><td><input id=”strSearch” onBlur=”setDefaultText()” onFocus=”removeDefaultText()” style=”color:grey;width:300px” value=”Type your search text here…” /></td><td><div id=’loading’></div></td></tr>

    </table><table width=”100%”>

    <tr><td><button id=”butSearch” onClick=”searchAnswer();”>${gs.getMessage(‘Search’)}</button></td></tr>

    <tr><td><div id=”searchReturn” value=””></div></td></tr>

    </table>

    </j:jelly>

     

    The HTML is pretty straightforward. It creates several elements and a table to distribute the results to. Also some inline styling to make it nice to the eye. Notice that there also some divs that are not in use directly, but will be used when activating the client script.

     

    Client Script

    function removeDefaultText(){

    var elInputText = gel(‘strSearch’);

    if(elInputText.value == ‘Type your search text here…’){

    elInputText.value = ”;

    }

    }

     

    function setDefaultText(){

    var elInpText = gel(‘strSearch’);

    if(elInpText.value == ”){

    elInpText.value = ‘Type your search text here…’;

    }

    }

     

    function searchAnswer(){

    var div = gel(‘searchReturn’);

    var loa = gel(‘loading’);

    var elAwait = document.createTextNode(‘Awaiting response…’);

    div.appendChild(elAwait);

    var elImg = document.createElement(‘img’);

    elImg.src = ‘images/animated/loading.gif’;

    loa.appendChild(elImg);

    var ga = new GlideAjax(‘ScriptSearchForm’);

    ga.addParam(‘sysparm_name’,’objCreateSearch’);

    ga.addParam(‘keywords’, getSearchValue());

    ga.getXMLWait();

    var gaResp = ga.getAnswer();

    div.innerHTML = ”;

    loa.innerHTML = ”;

    var htmlData = prepData(gaResp);

    div.innerHTML = htmlData;

    }

     

    function getSearchValue() {

    var elInpSearch = gel(‘strSearch’);

    return elInpSearch.value;

    }

     

    function prepData(strAnswer){

    var htmlOutput = ”;

    var arrMetaData = strAnswer.substring(0,strAnswer.indexOf(‘##’)).split(‘||’);

    if(arrMetaData[0] == 0){

    htmlOutput = ‘No records found’;

    return htmlOutput;

    }

    strAnswer = strAnswer.substring(strAnswer.indexOf(‘##’) + 2, strAnswer.length);

    var arrResultData = strAnswer.split(‘^^’);

    arrResultData = arrResultData.slice(1,arrResultData.length);

    for(var i = 0; i < arrResultData.length; i++){

    var strDirectLink = arrResultData[i].substring(0,arrResultData[i].indexOf(‘||’));

    var strName = arrResultData[i].substring(arrResultData[i].indexOf(‘||’)+2,arrResultData[i].indexOf(‘~’));

    arrResultData[i] = arrResultData[i].substring(arrResultData[i].indexOf(‘~’)+1, arrResultData[i].length);

    var strClassLink = arrResultData[i].substring(arrResultData[i].indexOf(‘~’)+1, arrResultData[i].indexOf(‘|’));

    var strClassName = arrResultData[i].substring(arrResultData[i].indexOf(‘|’)+1, arrResultData[i].length);

    htmlOutput += prepOutput(strDirectLink, strName, strClassLink, strClassName, i);

    }

    htmlOutput = ‘<table width=”100%”><tr><td style=”background-color:#C0C0C0;padding:2px;padding-left:4px;padding-right:4px;font-weight:bold;”>Nr.</td><td style=”background-color:#C0C0C0;padding:2px;padding-left:4px;padding-right:4px;font-weight:bold;”>Script name</td><td style=”background-color:#C0C0C0;padding:2px;padding-left:4px;padding-right:4px;font-weight:bold;”>Class name</td></tr>’ + htmlOutput + ‘</table>’;

    htmlOutput = ‘<b>Search Results:</br> Records found: ‘ + arrMetaData[0] + ‘ records. &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Processing time: ‘ + arrMetaData[1] + ‘ ms.</br></b>’+ htmlOutput;

    return htmlOutput;

    }

     

    function prepOutput(link, name, clLink, clName, num){

    var style = ‘background-color:#FFFFFF;padding:2px;padding-left:4px;padding-right:4px;’;

    if(num % 2 == 0){

    style = ‘background-color:#E0E0E0;padding:2px;padding-left:4px;padding-right:4px;’;

    }

    num++;

    return ‘<tr><td style=”‘ + style + ‘”>’ + num + ‘</td><td style=”‘ + style + ‘”><a href=”‘ + link + ‘” target=”_BLANK”>’ + name + ‘</a></td><td style=”‘ + style + ‘”><a href=”‘ + clLink + ‘” target=”_BLANK”>’ + clName + ‘</a></td></tr>’;

    }

     

    A list of the functions used in the client script:

    Function Explanation
    removeDefaultText When clicking on the input element, will remove the default text
    setDefaultText When blurring from the input element, will set the default text
    searchAnswer Will perform the call to the script include and start the search
    getSearchValue Returns the keyword from the input element
    prepData Prepares the data returned in a usable format and uses prepOutput for creating HTML to return to the UI page.
    prepOutput Prepares the output of the data

    All these functions working together create UI page that will search through all elements containing some sort of script, and will return to the end-user in a nicely readable format. When you press the link in a UI page it opens up a new page directly on the record you were clicking.

     

    I’m rushing through this as this is according to me not really ServiceNow related, but more DOM related Javascripting. A web developer will probably see that this is not the most optimal coding and that I’ve made mistakes here and there. Not to make excuses but coding client side for multiple Javascript engines is painful. Even though the page works in other browsers, it tends to work best for the Mozilla engine. Especially IE doesn’t like my coding, and I can assure you this feeling is mutual J. I also wasn’t able to test this on Safari or Opera, but I’m assuming there are no major problems with those either. If you do find a showstopper, please let me know.

     

    I was able to drastically increase the speed of my debugging using this gadget, and I hope it will do the same for you. And this was all there was to it. Congratulations, you’ve just created a rather basic search engine. Burt maybe we can add one more thing…

    Widget for admins (bonus)

    As a bonus, let’s put this page also in a widget, so we can give it to use to our admins from their homepages.

    1. Go to widgets in the navigator
    2. Create a new widget called Admin Widgets, make it active, add the admin role, and make it render as javascript
    3. Add the script code shown below to the script
    4. Save your work

    function sections() {
    return {
    ‘Script Searcher’ : { ‘name’ : ‘admin_script_search_form’}
    };
    }

    function render() {
    var name = renderer.getPreferences().get(“name”);
    var gf = new GlideForm(renderer.getGC(), name, 0);
    gf.setDirect(true);
    gf.setRenderProperties(renderer.getRenderProperties());
    return gf.getRenderedPage();
    }

    function getEditLink() {
    if (!gs.hasRole(‘admin’))
    return ”;

    return “sys_ui_page.do?sysparm_query=name=” + renderer.getPreferences().get(“name”);
    }

     

    Now admins can add additional Admin Widgets content to their pages, one we’ve just been creating. I hope you enjoyed the read, and if you do have any comments, please feel free to add them!

    You can contact me on wesley.bouwer@2e2.nl if you have any questions.

    6 Responses to “Building a custom script Search widget”

    1. Joe W Says:

      Wow, thank you so much for this… it’s simply amazing. This is what has been missing from ServiceNow. Also, found it helpful to include ‘condition’ & ‘reference_qual’ as additional types

    2. Wesley Bouwer Says:

      Hi Joe,

      Thank you for the feedback and kind comments, I’m really glad you appreciate it! I´m open for any suggestions for improvements, so please let me know if you come up with any additions.

      Wesley

    3. Jason Thomas Says:

      Hi Wes,

      Excellent article, I see you have learnt a lot from me 😉

      Jason

    4. Jason Thomas Says:

      Hi Wes

      Does the code highlight the active and inactive scripts? Maybe something you could add if not.

      Jason

    5. Wesley Bouwer Says:

      Hi Jason,

      Sorry for the extremely late response. I’ve been quite busy lately. It is a wonderful suggestion though, definitely will add it to the to-do list.

      Wesley

    6. hk seo Says:

      hello!,I like your writing very much! share we be in contact extra about your article on AOL?
      I require a specialist on this area to solve my problem.

      May be that is you! Taking a look forward to see you.

    Leave a Reply