Parts is Parts

“Parts is parts.”
Wendy’s TV commercial

I love making parts. One of the reasons that ServiceNow is such a powerful platform is that it is built on reusable parts, and the platform encourages the development and use of reusable parts. I have known a number of developers who are extraordinary coders that can lay down reliable code at an amazing rate of speed, but no one can code faster than they can pull an existing part down from a shelf. When you build a part in such a way that you can reuse it again in another context, you not only solve your current problem, but you also create the solution for problems that you haven’t even encountered yet. That doesn’t just improve productivity, it also increases reliability. Parts on the shelf are on the shelf because you have used them somewhere before, and if you’ve used them before, then you’ve tested them before, which means that you’ve already gone through the exercise of shaking out all of those initial bugs. Faster and better — it’s a win/win.

When I started my Highcharts experiment, my goal was to create a reusable component for displaying any Highcharts graph. I was able to do that with my Generic Chart widget, but in the process, I also ended up showcasing a number of the other little parts and pieces that have been developing on the Now Platform. For example, when I wanted to add the ability to click on the chart and drill down to the underlying data, I ended up linking to my enhanced Data Table widget. I actually built that to support my configurable content selector, but once created (and tested) and placed on the shelf, it was there to later pull down, dust off, and utilize for other, previously unforeseen purposes.

To make the various selections for the four different parameters used in the workload chart, I ended up using the angular provider that I put together to produce form fields on the Service Portal. I didn’t create that to support the workload chart, but once it was created, there is was on the shelf to pull down and put to use.

I built my dynamic Server Portal breadcrumbs because I really didn’t like the way that the out-of-the-box breadcrumbs widget required you to pre-define the trail of pages displayed by the widget. That philosophy assumes that each of your pages can only be reached through a single path. For someone who likes to build and leverage reusable parts, this just seemed a little too restrictive to me. This was yet another “part” that I had build with my content selector in mind, but which was equally useful once I started linking out from my workload chart. In fact, it was while working with my workload chart that I discovered a flaw in my breadcrumbs widget. That’s another nice thing about working with reusable parts: when you fix a problem, you don’t just fix it for your purposes; you fix it for everyone else who is using it for whatever other purpose. That’s the difference between cloning and reusing. If you clone a part and find a flaw, you fix your copy, but the original and any other clones are unaffected. If you reuse a part and fix a flaw, you fix it for everyone.

That’s why I love making parts.

Dynamic Service Portal Breadcrumbs, Corrected

“Most good programmers do programming not because they expect to get paid or get adulation by the public, but because it is fun to program.”
Linus Torvalds

While I was playing around with my workload chart, I noticed a little bug in my dynamic breadcrumbs widget: returning to the Home page does not reset the breadcrumbs for a new trail out from the home page. Instead, all of the previous trail of pages remains intact. This is not the way that this is supposed to work. Once you return to the home page, the trail should start over. At first, I never noticed this, because I never added the breadcrumbs widget to the home page. Without the breadcrumbs widget on the home page, I wouldn’t expect it to reset. But once I did that and it still didn’t work, I had to get busy trying to figure out why that was.

The breadcrumbs data that was left behind on the previous page is retrieved from the User Preference on the server side, and then the new breadcrumbs data is established on the client side. Initially, the breadcrumbs data is set to an empty Array, and then if we are not on the home page, we push all of the existing pages onto the array until we come across the current page or run out of existing pages. In the case of the home page, the empty array is all that remains. Here is the relevant code:

c.breadcrumbs = [];
var thisPage = {url: $location.url(), id: $location.search()['id'], label: c.data.page || document.title};
if (thisPage.id != $rootScope.portal.homepage_dv) {
	var pageFound = false;
	for (var i=0;i<c.data.breadcrumbs.length && !pageFound; i++) {
		if (c.data.breadcrumbs[i].id == thisPage.id) {
			c.breadcrumbs.push(thisPage);
			pageFound = true;
		} else {
			c.breadcrumbs.push(c.data.breadcrumbs[i]);
		}
	}
	if (!pageFound) {
		c.breadcrumbs.push(thisPage);
	}
}
c.data.breadcrumbs = c.breadcrumbs;
c.server.update();

On the server side of things, my intent was to save the breadcrumbs once established, and here is the code that was supposed to handle that:

if (input.breadcrumbs) {
	gs.getUser().setPreference('snhbc', JSON.stringify(input.breadcrumbs));
}

My thinking was that, if the breadcrumbs were established on the client side, then save them to the User Preference. Unfortunately, the simple conditional input.breadcrumbs returns false for an empty array, not true as I had assumed (hey, an empty array is still something and not null!); therefore, the saving of the breadcrumbs was not executed when on the home page. I should have know that, I guess, but I’m no longer young enough to know everything. I still get to learn something new every day. Once I figured that out, I changed it to this:

if (Array.isArray(input.breadcrumbs)) {
	gs.getUser().setPreference('snhbc', JSON.stringify(input.breadcrumbs));
}

A simple change, but one that made it work the way that I had intended instead of the way that I had coded it. That took care of that little issue. I included the updated breadcrumbs widget in the last Update Set for my Highcharts example, but for those of you who are only interested in the breadcrumbs widget, I created a separate Update Set, which you can grab here.

Update: There is an even better version, which you can find here.

Fun with Highcharts, Part VII

“Time, which alone makes the reputation of men, ends by making their defects respectable.”
Voltaire

Once again, things were not quite as bad as I had originally envisioned. I definitely disliked returning to my workload chart page and having it revert back to all of the original, default settings. This was not good. But how best to fix it? I didn’t have that problem with my Data Table Content Selector, which also has multiple options, but that was built using a little bit different approach. Although there are two independent widgets on the page, they don’t really communicate with one another. The selector on the current page communicates with the Data Table widget on the next page, passing all of the information via URL parameters when it builds the URL for the next page. Reconfiguring the workload chart widget and underlying generic chart widget to adopt this philosophy seemed like a lot more work that I really wanted take on right at the moment.

Instead, I decided to leverage the same hack that I used in my dynamic breadcrumbs widget, leveraging the User Preferences feature to preserve the selections and then picking them back up each time the page loads. Here is the additional code I added to save the user’s selections in a User Preference that I called workload_selections:

var selections = {};
selections.group = data.group;
selections.type = data.type;
selections.frequency = data.frequency;
selections.ending = data.ending;
gs.getUser().setPreference('workload_selections', JSON.stringify(selections));

User Preferences are strings, so you have to convert the object to a string before you save it, and convert it back to an object when you fetch it back. Here is the code that I used to retrieve it once the page loads again:

selections = gs.getPreference('workload_selections');
if (selections) {
	selections = JSON.parse(selections);
	if (selections.group) {
		data.group = selections.group;
		data.type = getDefaultType();
		data.type = selections.type;
		data.frequency = selections.frequency;
		data.ending = selections.ending;
	}
}

You might have noticed that the value for the type selection was set twice, once to the default value and then again to the value saved in the User Preference. That’s because the getDefaultType function is actually a dual purpose function; not only does it return the default type value, but before it does so, it sets up the type options that are appropriate for the selected group (different groups are associated with different types of tasks). Since the group value preserved may be different than the default value, the appropriate list of type choices needs to be generated for that group before the preserved type value is established. Other than that, it’s pretty straightforward stuff.

That resolved the last known defect that I have been able to find so far, so it’s finally time to release another Update Set. If you happen to pull it down and try to use it, let me know if you find any others … thanks!

Update: There is a better (expanded) version here.

Fun with Highcharts, Part VI

“If you’re going through Hell, keep going.”
Someone other than Winston Churchill

Well, it turns out that it wasn’t as bad as I had originally imagined. I converted my server side GenericChartUtil Script Include into a client side UI Script, then created a Widget Dependency referencing the UI Script, and then finally associated the Widget Dependency to my Generic Chart widget. That pushed the chart object generation from the server side to client side (where I would no longer lose any functions in the chart object), but to make it all work, I needed to pass the chart data and chart type around rather than the completed chart object.

On my Generic Chart widget, I removed the chartObject option and replaced it with two new widget options, chartType and chartData. Then I added a line of code in the client side script to pass the chartData and chartType to the new client side UI Script to generate the chartObject. The client side code for the widget now looks like this:

function($scope, $rootScope, $location) {
	var c = this;
	if (c.data.chartData) {
		c.data.chartData.location = $location;
		$scope.chartOptions = genericChartUtil.getChartObject(c.data.chartData, c.data.chartType);
	}
	if (c.options.listen_for) {
		$rootScope.$on(c.options.listen_for, function (event, config) {
			if (config.chartData) {
				config.chartData.location = $location;
				$scope.chartOptions = genericChartUtil.getChartObject(config.chartData, config.chartType);
			}
		});
	}
}

On my Workload Chart widget, I removed all references to the deleted Script Include on the server side, and then modified the broadcast message on the client side to pass the chartData and chartType rather than the entire generated chartObject. That code now looks like this:

function($scope, $rootScope) {
	var c = this;
	$scope.updateChart = function() {
		c.server.update().then(function(response) {
			c.data.config = response.config;
			c.data.group = response.group;
			c.data.type = response.type;
			c.data.frequency = response.frequency;
			c.data.ending = response.ending;
			c.data.chartData = response.chartData;
			$rootScope.$broadcast('refresh-workload', {chartData: c.data.chartData, chartType: 'workload'});
		});
	}
}

That solved my earlier problem of losing the functions built into the chart objects that were generated on the server side. Now I could get back to what I was trying to do in the first place, which was to set things up so that you could click on any given data point on the chart and pull up a list of the records that were represented by that value. This code was now working as it should:

plotOptions: {
        series: {
            cursor: 'pointer',
            point: {
                events: {
                    click: function () {
                        alert('Category: ' + this.category + ', value: ' + this.y);
                    }
                }
            }
        }
    },

For my link URL to show the list of records represented by the chart item clicked, I was going to need the name of the table and the filter used in the GlideAggregate that calculated the value. Neither one of those was currently passed in the chart data, so the first thing that I needed to do was to modify the code that generated the chartData object to include those values. That code now looks like this:

function gatherChartData() {
	var task = new GlideAggregate(data.type);
	var periodData = getPeriodData();
	var chartData = {};
	chartData.table = data.type;
	chartData.filter = {Received: [], Completed: [], Backlog: []};
	var filter = '';
	chartData.title = task.getPlural() + ' assigned to ' + findOption(data.config.groupOptions, data.group).label;
	chartData.subtitle = periodData.frequencyInfo.label + ' through ' + periodData.endingDateInfo.label;
	chartData.labels = periodData.labels;
	chartData.received = [];
	chartData.completed = [];
	chartData.backlog = [];
	for (var i=1; i<periodData.endDate.length; i++) {
		// received
		filter = 'assignment_group=' + data.group + '^opened_at>' + periodData.endDate[i-1] + '^opened_at<=' + periodData.endDate[i];
		task.initialize();
		task.addAggregate('COUNT');
		task.addEncodedQuery(filter);
		task.query();
		task.next();
		chartData.received.push(task.getAggregate('COUNT') * 1);
		chartData.filter.Received.push(filter);
		// completed
		filter = 'assignment_group=' + data.group + '^closed_at>' + periodData.endDate[i-1] + '^closed_at<=' + periodData.endDate[i];
		task.initialize();
		task.addAggregate('COUNT');
		task.addEncodedQuery(filter);
		task.query();
		task.next();
		chartData.completed.push(task.getAggregate('COUNT') * 1);
		chartData.filter.Completed.push(filter);
		// backlog
		filter = 'assignment_group=' + data.group + '^opened_at<=' + periodData.endDate[i] + '^closed_at>' + periodData.endDate[i] + '^ORclosed_atISEMPTY';
		task.initialize();
		task.addAggregate('COUNT');
		task.addEncodedQuery(filter);
		task.query();
		task.next();
		chartData.backlog.push(task.getAggregate('COUNT') * 1);
		chartData.filter.Backlog.push(filter);
	}
	return chartData;
}

Since I need the filter value for multiple purposes, I first assigned that to a variable, and then used that variable wherever it was needed. Inside of Highcharts, my only reference to the series was the category name, which is why there is an odd capitalized property key for the chartData.filter properties (the category names are labels on the chart, so they are capitalized). These changes gave me the data to work with so that I could modify the onclick function to look like this:

plotOptions: {
	series: {
		cursor: 'pointer',
		point: {
			events: {
				click: function (evt) {
					var s = {id: 'snh_list', table: chartData.table, filter: chartData.filter[this.series.name][this.index]};
					var newURL = chartData.location.search(s);
					spAriaFocusManager.navigateToLink(newURL.url());
				}
			}
		}
	}
},

In order for that to work, I need to pass the $location object to Highcharts as well, so my client side GenericChart widget code ended up looking like this:

function($scope, $rootScope, $location) {
	var c = this;
	if (c.data.chartData) {
		c.data.chartData.location = $location;
		$scope.chartOptions = genericChartUtil.getChartObject(c.data.chartData, c.data.chartType);
	}
	if (c.options.listen_for) {
		$rootScope.$on(c.options.listen_for, function (event, config) {
			if (config.chartData) {
				config.chartData.location = $location;
				$scope.chartOptions = genericChartUtil.getChartObject(config.chartData, config.chartType);
			}
		});
	}
}

To get back to the chart itself, once you clicked on a data point to see the underlying records, I added my dynamic breadcrumbs widget to the the top of the chart page and to the top of a new list page that I created for this purpose. Now it was time to test things out …

Well, as often seems to be the case with these things, there is good news and bad news. The good news is that clicking on the data points on the chart actually does bring up the list of records, which is very cool. Even though I had to do a considerable amount of restructuring of my initial concept, everything now seems to work. And I like the feature. Clicking on a bar or point on the line now takes you to a list of the records that make up the value for that data point. And when you are done, you can click on the breadcrumb for the chart and get back to the chart itself. All of that works beautifully.

Unfortunately, when you get back to the chart page, it reverts to the original default settings for all of the options. If you used any of the four selections at the top of the page to get to a specific chart configuration, and then clicked on a data point to see the underlying records, when you returned to the chart, you were no longer looking at the chart that you had selected. You were back to the original, default values for all of the selections. The page basically restarts from the beginning. That, I do not like at all.

The primary reason for that behavior is that your chart option selections are not part of the URL, so they are not preserved when you click on the breadcrumb to return. Making them part of the URL would mean another complete reconfiguration of the way in which that chart works, but something is going to have to be done. I don’t like it the way that it works now. Originally, I was going to release a new Update Set with all of these changes, but I’m not happy with the way things are working right now. I’m going to have to do a little bit more work before I’m ready to release another version. Hopefully, I can do that next time out.

Dynamic Service Portal Breadcrumbs

“Do not wait; the time will never be ‘just right.’ Start where you stand, and work with whatever tools you may have at your command, and better tools will be found as you go along.”
George Herbert

I’ve had this idea for a while to attempt a different approach to Service Portal breadcrumbs, and I finally quit tinkering with my Data Table clones and Configurable Content Selector long enough to actually throw something together. My issue with the out-of-the-box breadcrumb widget is that you have to tell it what the breadcrumbs are on every page rather than the system keeping track of where you are and how you got there. It seemed to me that it would not only be easier to set up, but it would also be more accurate, since there are often times more than one path to get to a specific page.

To keep track of the current page stack for the breadcrumbs, I decided to leverage the existing User Preferences infrastructure. User Preferences are accessible in the Service Portal via built-in GlideSystem functions, and provide a convenient means to keep track of a user’s path through the various screens in the portal. To fetch a User Preference, you use the gs.getPreference(key) method, and to update a User Preference, the script is gs.getUser().setPreference(key, value).

To begin, I pulled up the existing breadcrumb widget and created a clone that I called SNH Breadcrumbs. I did not want to change the way the breadcrumbs were displayed, so I left the HTML portion of the widget intact. I did not want to set the value of the breadcrumbs via widget option anymore, though, so I removed the option. Then I modified the server-side script to create a label for the current page and pull the current breadcrumbs out of the User Preferences. I also provided the means to update the breadcrumbs when an update was invoked on the client side. The complete server-side script now looks like this:

(function() {
	if (input) {
		if (input.breadcrumbs) {
			gs.getUser().setPreference('snhbc', JSON.stringify(input.breadcrumbs));
		}
	} else {
		data.table = $sp.getParameter('table');
		data.sys_id = $sp.getParameter('sys_id');
		if (data.table) {
			var rec = new GlideRecord(data.table);
			if (data.sys_id) {
				rec.get(data.sys_id);
				data.page = rec.getDisplayValue('number');
				if (!data.page) {
					data.page = rec.getDisplayValue('name');
				}
				if (!data.page) {
					data.page = rec.getDisplayValue('short_description');
				}
				if (!data.page) {
					data.page = rec.getLabel();
				}
			} else {
				data.page = rec.getPlural();
			}
		}
		data.breadcrumbs = [];
		var snhbc = gs.getPreference('snhbc');
		if (snhbc) {
			data.breadcrumbs = JSON.parse(snhbc);
		}
	}
})();

The page label is based on the URL parameters table and sys_id. If both are present, I go ahead and grab the record and attempt to obtain a label from the data. If only the table parameter is present, then I assume that we are talking about multiple records, so I grab the Plural label for the table itself. If there is no table parameter, then I let the client-side script handle the label for the page. On the client side, I build a breadcrumb entry for the current page, and then loop through the existing breadcrumbs to see if this page is already in the list. If it is, then that’s where we will stop; otherwise, we will just tack the new current page entry on to the end of the existing stack of pages. Here is the complete client-side script:

function($scope, $rootScope, $location, spUtil) {
	var c = this;
	c.expanded = !spUtil.isMobile();
	c.expand = function() {
		c.expanded = true;
	};
	c.breadcrumbs = [];
	var thisPage = {url: $location.url(), id: $location.search()['id'], label: c.data.page || document.title};
	
	if (thisPage.id != $rootScope.portal.homepage_dv) {
		var pageFound = false;
		for (var i=0;i<c.data.breadcrumbs.length && !pageFound; i++) {
			if (c.data.breadcrumbs[i].id == thisPage.id) {
				c.breadcrumbs.push(thisPage);
				pageFound = true;
			} else {
				c.breadcrumbs.push(c.data.breadcrumbs[i]);
			}
		}
		if (!pageFound) {
			c.breadcrumbs.push(thisPage);
		}
	}
	c.data.breadcrumbs = c.breadcrumbs;
	c.server.update();
}

That’s really all there is to it. Here’s one example of how it looks in practice:

Dynamic breadcrumbs example

In the example above, the URL for the page contains a table parameter, but no sys_id parameter. This generates a page label from the table’s getPlural() method. If we select a different perspective, which uses a different table, we will still be on the same page, but the page label will reflect the current table in use for that perspective.

Breadcrumbs example using a different perspective/table

Now, if you click on one of the items in the table, you will see that the breadcrumb list grows, and this time the URL has both a table and a sys_id parameter, but the record in question (sysapproval_approver) has no number, name, or short_description fields, so the label is defaulted to the generic label for the record.

Approval record breadcrumb example

Clicking on the Approvals breadcrumb will take you back to the original screen, removing the single Approval record from the breadcrumb array.

Using the breadcrumb to return to the initial screen

Now, if you click on the Change record instead of the Approval record, the Change record actually does have a number field, so the label for that page is the actual number of the record.

Breadcrumbs example with numbered record

And finally, if you click on the Opened by column, which is configured to take you to the User Profile page, there is no number, but there is a name, so that becomes the label.

Breadcrumbs example from the Opened by column

The reason that you can find the record to fetch the name in the above example is because the Data Table widget arbitrarily passes both the table and sys_id parameters to the User Profile page, even though table is not needed (the table sys_user is assumed by the User Profile page — you don’t have to pass it). When you pull down the User menu and select Profile, no table name is passed in the URL, so the label defaults to the name of the page.

Breadcrumbs example from the User Profile selection

One thing to keep in mind is that the trail will only build as you pass through pages that contain the widget. Any pages that you pass through that do not contain the widget will not get added to the running list of pages, as there will be no code running that pushes the page onto the stack. Other than that, it seems to work in most other cases. If you want to try it out for yourself, here’s an Update Set that contains the custom widget.

Update: There is a better (working!) version here.