Enhanced Event Management for ServiceNow

“Great things are done by a series of small things brought together.”
Vincent Van Gogh

The one property of a ServiceNow Event that we virtually skipped over last time was the additional_info property. This is pretty much a catch-all for any other thing that you might want to record along with the Event itself. The additional_info property is stored in the database as a JSON-formatted string, which you can instantiate in use and then access like any other Javascript object. By leveraging the additional_info property, we can inject standard elements into the Event so that the reporting module does not have to include the code to provide that information. One such bit of info could be details on the currently logged on user. Another might be a Stack Trace containing the details of how we arrived at the point of an Event occurring.

The one thing that we would not want to do, however, would be to overlay any information that the reporting entity has provided, so it will be important to first check for the presence of any data in the additional_info object before we set any values of our own. The first thing that we would have to do would be to check to see if an additional_info value was even provided, and that it was an object to which we could add additional values. Here is one way to approach such a check:

if (additional_info) {
	if (typeof additional_info != 'object') {
		additional_info = {info: additional_info};
	}
} else {
	additional_info = {};
}

This ensures that we have an object, and that we have preserved whatever non-object (string, boolean, number,etc.) values that may have been provided instead of an object. Once we know we have an object to work with, then we can check the object for other properties, and if not already provided, provide a standard value. For example, here is how we could potentially include the various details on the user:

if (!additional_info.user) {
	additional_info.user = {};
	additional_info.user.sys_id = gs.getUserID();
	additional_info.user.id = gs.getUserName();
	additional_info.user.name = gs.getUserDisplayName();
}

Injecting a Stack Trace could be handled in a similar fashion:

if (!additional_info.stackTrace) {
	additional_info.stackTrace = this.getStackTrace();
}

Of course, a server side Stack Trace is of little value if your issue is a client side Event, so you would probably want to snag a client side Stack Trace while you were on the client side, before you sent everything over to the server side to be reported. We can steal some of the code from our server side counterpart to enhance the client side function and turn it into something like this:

logEvent: function(source, resource, metric_name, severity, description, additional_info) {
	if (additional_info) {
		if (typeof additional_info != 'object') {
			additional_info = {info: additional_info};
		}
	} else {
		additional_info = {};
	}
	if (!additional_info.stackTrace) {
		additional_info.stackTrace = this.getStackTrace();
	}
	var ga = new GlideAjax('ServerEventUtil');
	ga.addParam('sysparm_name', 'logClientEvent');
	ga.addParam('sysparm_source', source);
	ga.addParam('sysparm_resource', resource);
	ga.addParam('sysparm_metric_name', metric_name);
	ga.addParam('sysparm_severity', severity);
	ga.addParam('sysparm_description', description);
	ga.addParam('sysparm_additional_info', JSON.stringify(additional_info));
	ga.getXML();
}

By creating a common Event reporting utility function and leveraging the additional_info property for specific selected values, virtually all of the Events reported by ServiceNow components can share a common set of properties. This creates opportunities for common Event processing scripts and generic reporting possibilities that would not exist if everyone were simply following their own unique approach to reporting Events. And once you establish an organizational standard for common values stored in the additional_info property, adding additional items of interest at a future point in time is simply a matter of updating the common routine that everyone calls to report Events.

We still need to put together that testing page that we talked about last time out, but at this point, I think that will have to be a project for another day

Update: There is an even better version here.

Event Management for ServiceNow

“If you add a little to a little, and then do it again, soon that little shall be much.”
Hesiod

The Event Management service built into ServiceNow is primarily designed for collecting and processing events that occur outside of ServiceNow. However, there is no reason that you cannot leverage that very same capability to handle events that occur in your own ServiceNow applications and customizations. To do that easily and consistently, it’s helpful to bundle up all of the code to make that happen into a function that can be called from a variety of potential users. A server-side Script Include can handle that quite nicely:

var EventUtil = Class.create();
EventUtil.prototype = {
	initialize: function() {
	},
	logEvent: function(source, resource, metric_name, severity, description) {
		var event = new GlideRecord('em_event');
		event.initialize();
		event.source = source;
		event.event_class = gs.getProperty('instance_name');
		event.resource = resource;
		event.node = 'ServiceNow';
		event.metric_name = metric_name;
		event.type = 'ServiceNow';
		event.severity = severity;
		event.description = description;
		event.insert();
	},
	type: 'EventUtil'
};

There are a number of properties associated with Events in ServiceNow. Here is the brief explanation of each as explained in the ServiceNow Event Management documentation:

VariableDescription
SourceThe name of the event source type. For example, SCOM or SolarWinds.
Source Instance (event_class)Specific instance of the source. For example, SCOM 2012 on 10.20.30.40
nodeThe node field should contain an identifier for the Host (Server/Switch/Router/etc.) that the event was triggered for. The value of the node field can be one of the following identifiers of the Host:
  • Name
  • FQDN
  • IP
  • Mac Address
If it exists in the CMDB, this value is also used to bind the event to the corresponding ServiceNow CI.
resourceIf the event refers to a device, such as, Disk, CPU, or Network Adapter, or to an application or service running on a Host, the name of the device or application must be populated in this field. For example, Disk C:\ or Nic 001 or Trade web application.
metric_nameUsed Memory or Total CPU utilization.
typeThe type of event. This type might be similar to the metric_name field, but is used for general grouping of event types.
message_keyThis value is used for de-duplication of events. For example, there might be two events for the same CI, where one event has CPU of 50% and the next event has CPU of 99%. Where both events must be mapped to the same ServiceNow alert, they should have the same message key. The field can be left empty, in which case the field value defaults to source+node+type+resource+metric_name. The message_key should be populated only when there is a better identifier than the default.
severitySeverity of the event. ServiceNow values for severity range from 1 – Critical to 5 – Info, with the severity of 0 – Clear. Original severity values should be sent as part of the additional information.
additional_infoThis field is in JSON key/value format, and is meant to contain any information that might be of use to the user. It does not map to a pre-defined ServiceNow event field. Examples include IDs of objects in the event source, event priority (if it is not the same as severity), assignment group information, and so on. Values in the Additional information field of an Event that are not in JSON key/value format are normalized to JSON format when the event is processed.
time_of_eventTime when the event occurred on the event origin. The format is: yyyy-MM-dd HH:mm:ss GMT
resolution_stateOptional – To indicate that an event has been resolved or no longer occurring, some event monitors use ‘clear’ severity, while other event monitors use a ‘close’ value for severity. This field is used for those monitors proffering the latter. Valid values are New and Closing.

Generating an Event in ServiceNow is simply writing a record to the em_event table. To reduce the amount of info that needs to be passed to the utility, our example function assumes a standard value for a number of properties of the Event, such as the event_class, node, and type, and leaves out completely those things that will receive a default value from the system such as message_key, time_of_event, and resolution_state. For our purpose, which is a means to generate internal Events within ServiceNow, we can accept all of those values as standard defaults. The rest will need to be passed in from the process reporting the Event.

For the source value, I like to use the name of the object (Widget, UI Script, Script Include, etc.) reporting the Event. For the resource value, I like to use something that describes the data involved, such as the Incident number or User ID. The source is the tool, and the resource is the specific data that is being processed by that tool. The other three data points that we pass are metric_name, severity, and description, all of which further classify and describe the event.

The example above takes care of the server side, but what about the client side? To support client-side event reporting, we can add an Ajax version of the function to our server-side Script Include, and then create a client-side UI Script that will make the Ajax call. The modified Script Include looks like this:

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

	logClientEvent: function() {
		this.logEvent(
			this.getParameter('sysparm_source'),
			this.getParameter('sysparm_resource'),
			this.getParameter('sysparm_metric_name'),
			this.getParameter('sysparm_severity'),
			this.getParameter('sysparm_description'));
	},

	logEvent: function(source, resource, metric_name, severity, description) {
		var event = new GlideRecord('em_event');
		event.initialize();
		event.source = source;
		event.event_class = gs.getProperty('instance_name');
		event.resource = resource;
		event.node = 'ServiceNow';
		event.metric_name = metric_name;
		event.type = 'ServiceNow';
		event.severity = severity;
		event.description = description;
		event.insert();
	},

	type: 'ServerEventUtil'
});

To access this code from the client side of things, a new UI Script will do the trick:

var ClientEventUtil = {

	logEvent: function(source, resource, metric_name, severity, description, additional_info) {
		var ga = new GlideAjax('ServerEventUtil');
		ga.addParam('sysparm_name', 'logClientEvent');
		ga.addParam('sysparm_source', source);
		ga.addParam('sysparm_resource', resource);
		ga.addParam('sysparm_metric_name', metric_name);
		ga.addParam('sysparm_severity', severity);
		ga.addParam('sysparm_description', description);
		ga.getXML();
	},

	type: 'ClientEventUtil'
};

Now that we have created our utility functions to do all of the heavy lifting, reporting an Event is a simple matter of calling the logEvent function from the appropriate module. On the server side, that would something like this:

var seu = new ServerEventUtil();
seu.logEvent(this.type, gs.getUserID(), 'Unauthorized Access Attemtp', 3, 'User ' + gs.getUserName() + ' attempted to access ' + functionName + ' without the required role.');

On the client side, where we don’t have to instantiate a new object, the code is event simpler:

ClientEventUtil.logEvent('some_page.do', NOW.user.userID, 'Unauthorized Access Attemtp', 3, 'User ' + NOW.user.name + ' attempted to access ' + functionName + ' without the required role.');

To test all of this out, we should be able to build a simple UI Page with a couple of test buttons on it (one for the server-side test and one for the client-side test). This will allow us to both test the utility modules and also see what happens to the Events once they get generated. That sounds like a good project for next time out.

But wait … there’s more!

“Waiting is one of the great arts.”
Margery Allingham

Every once in a while, I will run into a situation where my code needs to wait for some other asynchronous process to complete. You don’t really want to go anywhere, and you don’t want to do anything. You just want to wait. On the client side, this is easily accomplished with a simple setTimeout. This is usually done with a basic recursive loop that repeats until the conditions are right to proceed.

function waitForSomething() {
	if (something) {
		itIsNowOkToProceed();
	} else {
		setTimeout(waitForSomething, 500);
	}
}

Unfortunately, on the server side, where this need usually arises, the setTimeout function is not available. However, there is a widely known, but poorly documented, GlideSystem function called sleep that you can use in the global scope to provide virtually the same purpose.

function waitForSomething() {
	if (something) {
		itIsNowOkToProceed();
	} else {
		gs.sleep(500);
		waitForSomething();
	}
}

Don’t try this in a scoped application, though, because you will just get a nasty message about how gs.sleep() is reserved for the global scope only. Fortunately, like a lot of things in ServiceNow, if you know the secret, you can get around such restrictions. In this case, all you really need is a Script Include that IS in the global scope, and then you can call that script from your scoped application and it will work just fine. Here is a simple example of just such as script:

var Sleeper = Class.create();
Sleeper.prototype = {
    initialize: function() {
    },

	sleep: function(ms) {
		gs.sleep(ms);
	},

    type: 'Sleeper'
};

Once you have your global-scoped Script Include set up, you can now call it from a scoped app and it will actually work.

function waitForSomething() {
	if (something) {
		itIsNowOkToProceed();
	} else {
		new global.Sleeper().sleep(500);
		waitForSomething();
	}
}

Reference Type System Properties, Part III

“Everything should be made as simple as possible, but not simpler.”
Albert Einstein

Last time, we got far enough along in the System Property value page modifications to demonstrate that we could replace the rendered input element with something else of our own design. Not having a design of our own for an adequate replacement, we implemented the popular creative avoidance strategy by working on all of the other parts and pieces first until we finally ran out of other parts and pieces. Now it is time to come up with a plan and finish this little project up.

I have to admit that I’m not all that proud of what I eventually came up with, but it does satisfy Rule #1, so at this point, I’ll take it and call it good. I tried a number of other things first, but none of those really got me what I wanted, so here we are. The basic plan is pretty simple, and consists of two parts: 1) a hidden input element to replace the text input element so that it can be submitted with the form, and 2) an iframe into which we will put our new input element via a stand-alone page designed for that purpose. I don’t really like the iframe approach, but it does have the benefit of being independently rendered, which gives us the opportunity to leverage the snRecordPicker for our input, which we really cannot do by simply modifying the main page directly after it has been delivered.

So let’s start out with the script that will build the HTML that we will use to replace the original text input element:

function buildHTML(prop) {
	var html = "";
	html += "<input type=\"hidden\" id=\"" + prop.property + "\" name=\"" + prop.property + "\"/>\n";
	html += "<div style=\"width: 100%; height: auto;\">\n";
	html += " <iframe id=\"frame." + prop.property + "\" src=\"/$sp.do?id=reference_properties&sysparm_property=" + prop.property + "&sysparm_table=" + prop.tableName + "&sysparm_column=" + prop.column + "&sysparm_value=" + prop.value + "\" style=\"border: none; height: 65px; width: 100%;\"></iframe>\n";
	html += "</div>\n";
	return html;
}

The hidden input element virtually replaces the original text input element, having the same name and same id. The iframe element is pretty vanilla stuff as well; the only thing of interest really is the src parameter, which points to the Portal Page that we are about to create, and passes along all of the various values needed to make the page do what we want. The Portal Page itself is just a single page with a single container filled with a single widget. The page is not really worth looking at, so let’s just jump right into the widget, as that’s where all of the magic happens. Here is the HTML:

<div id="pickerdiv">
  <sn-record-picker field="field" table="c.data.table" display-field="c.data.column" value-field="'sys_id'" search-fields="c.data.column" page-size="c.data.pageSize"></sn-record-picker>
</div>

Not much to see there, either. It’s just your standard snRecordPicker with pretty much every attribute defined by a variable. We’ll snag the values for those variables off of the URL for the page, which we populated when we constructed the src attribute for the iframe tag. The widget’s client-side script does most of the heavy lifting here:

function($scope, $location) {
	var c = this;
	var qp = $location.search();
	c.data.property = qp.sysparm_property;
	c.data.table = qp.sysparm_table;
	c.data.column = qp.sysparm_column;
	c.data.pageSize = 20;
	c.data.fieldValue = '';
	c.data.fieldDisplayValue = '';
	if (qp.sysparm_page_size) {
		c.data.pageSize = qp.sysparm_page_size;
	}
	if (qp.sysparm_value) {
		c.data.fieldValue = qp.sysparm_value;
		c.server.update().then(function(response) {
			c.data.fieldDisplayValue = response.fieldDisplayValue;
			$scope.field = {
				displayValue: c.data.fieldDisplayValue,
				value: c.data.fieldValue,
				name: 'field'
			};
		});		
	} else {
		$scope.field = {
			displayValue: '',
			value: '',
			name: 'field'
		};
	}
	$scope.$on('field.change', function(evt, parms) {
		if (parms.field.name == 'field') {
			parent.updateReferenceProperty(c.data.property, parms.field.value);
		}
	});
}

The only reason for the server-side script is to fetch the display value of the property if the property is valued at the time that the page is delivered to the browser.

(function() {
	if (input) {
		if (input.fieldValue) {
			var gr = new GlideRecord(input.table);
			gr.get(input.fieldValue);
			data.fieldDisplayValue = gr.getDisplayValue(input.column);
		} else {
			data.fieldDisplayValue = '';
		}
	}
})();

That’s about all there is to it. For every property on the page where Type=reference, the standard text input element is replaced with a hidden input element and an iframe, and inside the iframe is a ServiceNow Service Portal page that contains a single widget containing a single snRecordPicker. The parameters for the picker are passed from the iframe to the portal page via URL parameters, which are picked up by the widget and used to configure the snRecordPicker. All changes to the snRecordPicker are copied over to the hidden input field, so when the form is submitted, the selected value is sent to the server and posted to the database.

There was a minor problem with this initial version when trying to figure out the optimum height for the iframe. The height of the snRecordPicker depends on whether or not the drop-down list of choices is present, and I couldn’t find a CSS-centric way of having the iframe automatically adjust for the change in height, nor could I find a way to have the drop-down list of selectable choices overlay the content below, which is outside of the iframe. Finally, I resorted to plain old Javascript, and set up a variable called c.data.expanded to indicate whether or not the pick list was present on the screen. With a little view selection source magic, I was able to figure out that the component to watch had an id of select2-drop-mask, and so I modified the widget’s client-side code to check the required iframe height every second and adjust if needed:

function($scope, $location) {
	var c = this;
	var qp = $location.search();
	c.data.property = qp.sysparm_property;
	c.data.table = qp.sysparm_table;
	c.data.column = qp.sysparm_column;
	c.data.pageSize = 20;
	c.data.fieldValue = '';
	c.data.fieldDisplayValue = '';
	c.data.expanded = false;
	if (qp.sysparm_page_size) {
		c.data.pageSize = qp.sysparm_page_size;
	}
	if (qp.sysparm_value) {
		c.data.fieldValue = qp.sysparm_value;
		c.server.update().then(function(response) {
			c.data.fieldDisplayValue = response.fieldDisplayValue;
			$scope.field = {
				displayValue: c.data.fieldDisplayValue,
				value: c.data.fieldValue,
				name: 'field'
			};
		});		
	} else {
		$scope.field = {
			displayValue: '',
			value: '',
			name: 'field'
		};
	}
	$scope.$on('field.change', function(evt, parms) {
		if (parms.field.name == 'field') {
			parent.updateReferenceProperty(c.data.property, parms.field.value);
		}
	});
	checkHeight();
	function checkHeight() {
		var elem = document.getElementById('select2-drop-mask');
		if (elem) {
			if (elem.style.display == 'none') {
				if (c.data.expanded) {
					c.data.expanded = false;
					setHeight('65px');
				}
			} else {
				if (!c.data.expanded) {
					c.data.expanded = true;
					setHeight('300px');
				}
			}
		}
		setTimeout(checkHeight, 1000);
	}
	function setHeight(newHeight) {
		parent.updateFrameHeight(c.data.property, newHeight);
	}
}

Once that code was in place, the unexpanded state looked like this:

Modified input element with choices collapsed

… and the expanded state looked like this:

Modified input element with choices expanded

It still disturbs my sense of The Way Things Ought To Be for the left-hand edge of the revised input element not to line up with the left-hand edge of all of the other original input elements, but a fix to that was not readily apparent to me, so I have managed to let that go for now and move on to more important things. One day, though, I am going to figure out a way to fix that!

Just to recap, we modified a choice list and added an additional field to a table to provide the capability to define properties of type reference. We then created a UI Script and a Script Include so that we could replace the original input element on the property UI page, and then we created a Service Portal page and associated widget to provide the replacement for the original input element. As soon as I get a chance, I will see if I can wrap all of that up into a single Update Set and get it posted out here in case anyone wants just grab the whole package. All in all, it was a fun little project, but one that I hope to throw away one day when ServiceNow actually supports reference type properties right out of the box and having this little tweak is no longer needed.

Update: Well, it took a little longer than I had hoped to get around to this, but here is that Update Set finally.

Reference Type System Properties, Part II

“I shall either find a way or make one.”
Hannibal Barca

Last time out, we modified the sys_properties table and associated form so that we could create System Properties with a field type of Reference. This was only half the battle, however, and the easy half at that. The more difficult task will be to figure out how to get the page used to set the value of System Properties to properly render the property for user input.

Even though there is a Value field on the default System Properties form, system property values are generally set using the system_properties_ui.do page, which takes a title and one or more categories as URL parameters. You can see several examples of this in the out-of-the-box left-hand navigation menu, such as the one pictured below from the Live Feed section:

Example System Property value page

You can see from the rendered form that properties of different Types have different input mechanisms. There is a checkbox for the boolean properties and text input for the string and number properties. All we need to do is figure out how to convince it to render the Reference type properties with a selection list from the specified table. How hard can that be?

Up to this point, everything that we have done has been fairly vanilla ServiceNow stuff. Modifying Choice Lists, adding columns to Tables, manipulating Form Layouts, and creating UI Policies are all bread and butter ServiceNow sysadmin activities. There has been no need to resort to any kind of creative hackery to accomplish what we wanted do, which is always a good thing. Unfortunately, that’s all about to change.

As far as I can tell, the system_properties_ui.do page is yet another one of those items that is an integral part of the product and is not defined in the system database in a way that you can alter it in any way. As with the Email Client, we will have to rely on a script to modify the page after it has been delivered to the browser. To get an idea of how we want to do that, let’s define a property of type reference and see what the out-of-the-box functionality does with it when it renders the page. Entering sys_properties.list in the left-hand navigation search box will get us to the full list of System Properties where we can click on the New button to create our new property. At this point, I am just trying to create something that I can use to see how the input screen turns out, so I just enter Test as the Name, Test as the Description, select reference as the Type, and then select the sys_user table as the Table. Once the property has been defined, I can go back in and assign it to the Category Test by scrolling down to the bottom of the form and clicking on the New button in the Category list.

Once assigned to a Category, I can bring up the standard property value maintenance page with the URL https://<instance>.service-now.com/nav_to.do?uri=%2Fsystem_properties_ui.do%3Fsysparm_title%3DTest%2520Properties%26sysparm_category%3DTest. The good news is that the page didn’t choke on the new, unknown property type, and simply rendered the property as if the type were string:

Out-of-the-box rendering of a reference type property

More important than how it looks, though, is what’s under the hood … let’s inspect that HTML:

<tr>
 <td class="tdwrap label_left" oncontextmenu="return showPropertyContext(event, 'Test');">
  <span>
   <label class="label-inline" for="98c30fe6db362300f9699006db961935">Test</label>
   <button type="button" data-toggle="tooltip" aria-label="Property name: Test" title="" class="btn btn-icon-small btn-icon-white icon-help sn-tooltip-basic" data-original-title="Property name: Test"></button>
  </span>
 </td>
</tr>
<tr>
 <td>
  <input name="98c30fe6db362300f9699006db961935" id="98c30fe6db362300f9699006db961935" value="" aria-label="" style="width: 700px" autocomplete="off">
  <br>
  <br>
 </td>
</tr>

Pretty straightforward stuff, just a couple of single cell table rows, with the label in one row and the input box in the other. It looks like both the name and id of the input element are the sys_id of the property, so it would appear that we have everything that we need to have our way with this code, once it has been delivered to the browser.

So, here’s the plan: given the property categories available from the URL of the page, we should be able to determine all of the properties in the specified category or categories where Type=reference. Looping through that list of properties, we can find the existing input field based on the property’s sys_id, and then replace it with a more appropriate input mechanism to support the selection of records from the specified table. The only question, really, is with what will we replace the input element? If this were a Service Portal widget, we could leverage the snRecordPicker tag, but tags are useless once the page has been delivered to the browser. We could emulate everything that it does, and generate all of the HTML, CSS, and Javascript on our own, but that seems like considerably more work than I care to contemplate right now. We’ll have to give this one a little thought.

In the meantime, let’s jump on that UI Script that pulls the categories off of the URL and makes the Ajax call to the guy that will find all of the properties where Type=reference. That one shouldn’t be much work at all …

if (window.location.pathname == '/system_properties_ui.do') {
	if (window.location.href.indexOf('sysparm_category=') != -1) {
		fetchReferenceProperties();
	}
}

function fetchReferenceProperties() {
	var thisUrl = window.location.href;
	var category = thisUrl.substring(thisUrl.indexOf('sysparm_category=') + 17);
	if (category.indexOf('&') != -1) {
		category = category.substring(0, category.indexOf('&'));
	}
	if (category > '') {
		var ga = new GlideAjax('Reference_Properties');
		ga.addParam('sysparm_name', 'fetchReferenceProperties');
		ga.addParam('sysparm_category', category);
		ga.getXML(processProperties);
	}
}

function processProperties(response) {
	var answer = response.responseXML.documentElement.getAttribute("answer");
	if (answer > '') {
		var property = JSON.parse(answer);
		for (var i=0; i<property.length; i++) {
			var prop = property[i];
			var elem = gel(prop.property);
			elem.parentNode.innerHTML = buildHTML(prop);
		}
	}
}

function buildHTML(prop) {
	// we'll need to figure this out one day ...
}

Well, that wasn’t so bad. While we are thinking about the HTML that we will need to build to replace the original input tag, let’s go ahead and create that Script Include that will gather up all of the properties where Type=reference.

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

	fetchReferenceProperties: function() {
		var property = [];
		var catList = this.getParameter('sysparm_category');
		if (catList > '') {
			catList = decodeURIComponent(catList);
			var category = catList.split(',');
			for (var i=0; i<category.length; i++) {
				var gr = new GlideRecord('sys_properties_category_m2m');
				gr.addQuery('category.name', category[i]);
				gr.addQuery('category.sys_scope', gs.getCurrentApplicationId());
				gr.addQuery('property.type', 'reference');
				gr.query();
				while (gr.next()) {
					property.push({property: gr.getValue('property')});
				}
			}
		}
		for (var i=0; i<property.length; i++) {
			var sys_id = property[i].property;
			var gr = new GlideRecord('sys_properties');
			gr.get(sys_id);
			property[i].table = gr.getValue('u_table');
			property[i].tableName = gr.getDisplayValue('u_table.name');
			property[i].value = gr.getValue('value');
			property[i].column = this.getDisplayColumn(property[i].tableName);
			property[i].displayValue = this.getDisplayValue(property[i].tableName, property[i].value, property[i].displayColumn);
		}
		return JSON.stringify(property);
	},

	getDisplayColumn: function(table) {
		if (!this.displayColumn[table]) {
			this.displayColumn[table] = this.fetchDisplayColumn(table);
		}
		return this.displayColumn[table];
	},

	fetchDisplayColumn: function(table) {
		var displayColumn = 'sys_id';
		var possibleColumn = ['name','short_description','title','description'];
		for (var i=0; i<possibleColumn.length && displayColumn == 'sys_id'; i++) {
			if (this.columnPresent(table, possibleColumn[i])) {
				displayColumn = possibleColumn[i];
			}
		}
		return displayColumn;
	},

	columnPresent: function(table, column) {
		columnPresent = false;
		var gr = new GlideRecord('sys_dictionary');
		gr.addQuery('name',  table);
		gr.addQuery('element', column);
		gr.query();
		if (gr.next()) {
			columnPresent = true;
		}
		return columnPresent;
	},

	getDisplayValue: function(table, value, column) {
		var displayValue = '';
		if (value > '') {
			var gr = new GlideRecord(table);
			gr.get(value);
			displayValue = gr.getDisplayValue(column);
		}
		return displayValue;
	},

	displayColumn: {},

	type: 'Reference_Properties'
});

This one is pretty self-explanatory, but here is the basic premise for the code: for every category in the list, we search for properties where Type=reference, and then push each one into a single pile of property objects that contain the sys_id of the property record. When we are through creating the pile, we then loop through the pile and fetch the property records using the stored sys_id so that we can add additional data to the objects, including the reference table and the current value. One of the data points that we will undoubtedly need will be the name of the field on the table that contains the “display” value. Although we could have added yet another field to the sys_properties table and had the user provide that information, for now I just hunt for it using a few potential candidates, and then fall back to sys_id if nothing else is available.

At this point, we can actually try out what we have so far, even though we still haven’t figured out what we are going to use to replace the original input element. For now, we can just dump the values onto the screen and make sure that all of the parts and pieces that we have build so far are doing what we would expect them to do. We can do that by adding a little code to the buildHTML function of our UI Script:

function buildHTML(prop) {
	return JSON.stringify(prop);
}

Now we can another little test using our previously defined test property by pulling up the same page that we did at the start.

Modified rendering of a reference type property

What that proves is that we can grab the categories from the URL, use them to find all of the reference properties in those categories, use the list of reference properties to find their corresponding input elements on the page, and then replace those input elements with something else. Now all that we have to figure out is what we really want to use to replace those input elements so that we have a pick list from the records in the specified table.

Right about now, that sounds like an interesting exercise for next time out

Fun with the User Menu

“Any sufficiently advanced technology is indistinguishable from magic.”
Arthur C. Clarke

In the upper right-hand corner of the ServiceNow UI, you can click on your name and a little menu will drop down with a series of choices:

The ServiceNow User Menu

There is something similar on a ServiceNow Portal, with similar options. To easily move back and forth between the main Portal and the primary UI, I wanted to add an option to each environment’s User Menu to navigate to the other environment. On the Portal, this was easy enough to do by editing the HTML on the Stock Header. In the primary UI, however, this was not quite so easy.

The content of the drop-down User Menu is yet another one of those things that is not stored in the system database, but is rather an integral part of the product itself. As far as I can tell, there is no place to go where you can edit these options. But you know how that goes: if you can’t change what gets sent to the page, that doesn’t mean you can’t alter what is on the page once the page has been delivered. With a little View Source investigating, we can see that the menu is an HTML unordered list (<ul>) with a class of dropdown-menu. That’s enough to get a handle on things and build a quick little UI Script:

addLoadEvent(function() {
	try {
		var ssp = (window.top.$j("#self_service_link").length == 0);
		if (ssp) {
			var sspItem = "<li><a href='/sp' id='self_service_link'>" + getMessage("Self Service Portal") + "</a></li>";
			var menuList = window.top.$j("ul.dropdown-menu");
			if (menuList.length > 0) {
				menuList.prepend(sspItem);
			}
		}
	} catch(e) {
		//
	}
});

The script is pretty straightforward. First we check to make sure that we haven’t already stuffed the extra item out there (sometimes these things have a way of running more than one time), and if we haven’t, then we create the new menu item, locate the existing list, and prepend the new item to the existing list. After this has been activated, the menu now looks like this:

The modified User Menu

I was really quite proud of myself there for a while, and then I realized that on the Portal, the Profile option was still the first option and the ServiceNow option was the second option. By using the prepend function to insert the new menu item, I made it the first item on the menu instead of the second. That really disturbs my sense of The Way Things Ought To Be, so I needed to figure out how to make it the second option and not the first.

It’s true that I could have just reversed things on the Portal side of the house, which is much easier to manipulate, but that disturbs my sense of The Way Things Ought To Be as well. On a User Menu, the Profile link should always be the first and the Logout link should always be the last. Always. I can’t explain it, but that’s just The Way Things Ought To Be. Period.

I’m not going to reveal how many different things that I tried in an effort to insert the new item into the second slot, but let’s just say it’s more than I’d really care to discuss. In the end, I was able to cut and paste some code that I found out on the Interwebs and it did the trick, so that was good enough for me. Don’t ask me to explain how it works, because I haven’t a clue. Although I am pretty handy with Javascript, I don’t know much about AngularJS and I know even less about jQuery. This line of code is pure jQuery voodoo magic as far as I can tell, but it does satisfy rule #1: it works. Yes, that’s unquestionably Cargo Cult Coding, but I’ve never been too proud to leverage someone else’s code, even when I can’t possibly begin to fully understand it myself. You can see the updated script here:

addLoadEvent(function() {
	try {
		var ssp = (window.top.$j("#self_service_link").length == 0);
		if (ssp) {
			var sspItem = "<li><a href='/sp' id='self_service_link'>" + getMessage("Self Service Portal") + "</a></li>";
			var menuList = window.top.$j("ul.dropdown-menu");
			if (menuList.length > 0) {
				window.top.$j("ul.dropdown-menu li:eq(0)").after(sspItem);
			}
		}
	} catch(e) {
		//
	}
});

Now, I can actually look at the menu again without those uncomfortable Rain Man impulses to set things right:

The properly modified User Menu

Well, that’s it for today. If you want a copy to play around with, you can get one here.

OK, that works!

“A good plan, violently executed now, is better than a perfect plan next week.”
General George S. Patton

Well, it’s not the way that I like to do things, but it does work, so there is at least that. If I can ever stumble across a way to do it right, I will. For now, though, this will have to do. I never could find a modifiable source for either the pop-up e-mail form or the drop-down menu, so I eventually gave up on trying to send the right content for the page to the browser from the start. My fallback plan was to modify the page after it was rendered on the browser to achieve my desired results. That’s not really the way that I like to do things, but hey — It works!

I inspected the Email option of the drop-down menu and determined that it had an ID of email_client_open. Armed with that useful tidbit of information, I was able to create the following Client Script linked to the Incident table:

function onLoad() {
	var elem = gel('email_client_open');
	if (elem) {
		elem.onclick = function() {
			var dialog = new GlideDialogWindow('email_recipient_selector');
			dialog.setSize(400,400);
			dialog.setTitle('Select Recipient');
			dialog.render();
		};
	}
}

All in all, it took four independent components to pull this off: 1) the Client Script (above), 2) the UI Page that served as the content of the modal dialog, 3) a UI Script to run on the original Email Client pop-up to pull in the selections passed in the URL, and 4) a Script Include to go get the appropriate email addresses based on the user’s selections on the modal dialog. Now, when you click on the Email menu option, a new interim modal dialog pops up asking you to select the recipients of the email:

The UI Page is the most straightforward component of the lot. It’s just a standard page using standard approaches to collect information from the user and pass it along to the other elements in the chain. Here is the HTML in its entirety:

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
  <div style="padding: 20px;">
    Please select the recipients to whom you would like to send e-mail from the list below:
    <input type="checkbox" style="margin: 0px 8px 5px 30px;" id="caller"/>Caller
    <input type="checkbox" style="margin: 0px 8px 5px 30px;" id="group"/>Assignment Group
    <input type="checkbox" style="margin: 0px 8px 5px 30px;" id="tech"/>Assigned Technician
    <button class="button" onclick="composeEmail();">Compose E-mail</button>
  </div>
</j:jelly>

And here is the associated script:

function composeEmail() {
    var caller = gel('caller').checked;
    var group = gel('group').checked;
    var tech = gel('tech').checked;
    if (caller || group || tech) {
        continueToOriginalEmailPopUp(caller, group, tech);
    } else {
        alert('You must select at least one recipient to send an e-mail');
    }
}
 
function continueToOriginalEmailPopUp(caller, group, tech) {
    var id = g_form.getUniqueValue();
    if (id) {
        var url = new GlideURL("email_client.do");
        url.addParam("sysparm_table", 'incident');
        url.addParam("sysparm_sys_id", id);
        url.addParam("sysparm_target", 'incident');
        if (caller) {
            url.addParam("sysparm_to_caller", "true");
        }
        if (group) {
            url.addParam("sysparm_to_group", "true");
        }
        if (tech) {
            url.addParam("sysparm_to_tech", "true");
        }
        popupOpenEmailClient(url.getURL() + g_form.serializeChangedAll());
        GlideDialogWindow.get().destroy();
    }
}

To read the new parameters added to the URL when the email client page comes up, I needed to attach a script to that page. Here again, the problem is that I have been unable to find any way to modify that page directly. I’m not too proud of the solution that I came up with to address this problem, but again, it does work. To make it happen, I created a global UI Script that will now be included on every single page on this instance, but will look at the current location to see if needs to do anything. If the current page is not the email client pop-up, then it does nothing, but it still has to ask, which is a virtually pointless exercise on any page other than the one that I want. Still, it’s a way to get something to run on that page, and that’s what I was after. Here is the code:

if (window.location.pathname == '/email_client.do') {
    setupRecipients();
}
 
function setupRecipients() {
    var qp = {};
    var qs = window.location.search.substring(1);
    var parts = qs.split('&');
    for (var i=0; i<parts.length; i++) {
        var temp = parts[i].split('=');
        qp[temp[0]] = decodeURIComponent(temp[1]);
    }
 
    if (qp['sysparm_table'] == 'incident') {
        var caller = qp['sysparm_to_caller'];
        var group = qp['sysparm_to_group'];
        var tech = qp['sysparm_to_tech'];
        var ga = new GlideAjax('EmailRecipientTool');
        ga.addParam('sysparm_name', 'getRecipients');
        ga.addParam('sysparm_sys_id', qp['sysparm_sys_id']);
        ga.addParam('sysparm_to_caller', caller);
        ga.addParam('sysparm_to_group', group);
        ga.addParam('sysparm_to_tech', tech);
        ga.getXML(function(response) {
            var string = response.responseXML.documentElement.getAttribute("answer");
            var recipient = JSON.parse(string);
            var spans = $(document.body).select(".address");
            for (var i=0; i<spans.length; i++) {
                spans[i].className = 'addressHighlight';
            }
            deleteHighlightedAddresses();
            for (var i=0; i<recipient.length; i++) {
                var input = {};
                input.value = recipient[i].email;
                addEmailAddressToList('MsgToUI', input);
            }
        });
    }
}

To fetch the requested recipient e-mail addresses from the system database, the above script makes an Ajax call to the last component in the solution, the Script Include developed for this purpose:

var EmailRecipientTool = Class.create();
EmailRecipientTool.prototype = Object.extendsObject(AbstractAjaxProcessor, {
 
    getRecipients: function() {
        var list = [];
        var gr = new GlideRecord('incident');
        gr.get(this.getParameter('sysparm_sys_id'));
        if (this.getParameter('sysparm_to_caller') == 'true') {
            this.addRecipient(list, {sys_id: gr.getDisplayValue('caller_id.sys_id'), name: gr.getDisplayValue('caller_id.name'), email: gr.getDisplayValue('caller_id.email')});
        }
        if (this.getParameter('sysparm_to_group') == 'true') {
            this.addGroup(list, gr.getValue('assignment_group'));
        }
        if (this.getParameter('sysparm_to_tech') == 'true') {
            this.addRecipient(list, {sys_id: gr.getDisplayValue('assigned_to.sys_id'), name: gr.getDisplayValue('assigned_to.name'), email: gr.getDisplayValue('assigned_to.email')});
        }
        return JSON.stringify(list);
    },
 
    addGroup: function(list, group) {
        if (group > '') {
            var gr = new GlideRecord('sys_user_grmember');
            gr.addQuery('group', group);
            gr.query();
            while (gr.next()) {
                this.addRecipient(list, {sys_id: gr.getDisplayValue('user.sys_id'), name: gr.getDisplayValue('user.name'), email: gr.getDisplayValue('user.email')});
            }
        }
    },
 
    addRecipient: function(list, recipient) {
        if (recipient.sys_id > '') {
            var duplicate = false;
            for (var i=0; i<list.length; i++) {
                if (recipient.sys_id == list[i].sys_id) {
                    duplicate = true;
                }
            }
            if (!duplicate) {
                list.push(recipient);
            }
        }
    },
    type: 'EmailRecipientTool'
});

Well, that’s it. If anyone is interested, I bundled all four parts into an Update Set, which you can grab here. I was able to do that, by the way, thanks to this tool, which I cannot recommend highly enough. I can’t even begin to count how many times I’ve used that since I first installed it. The folks that put that together and keep it maintained do very nice work indeed, and have both my admiration and my appreciation.