The CreatorCon Call for Content is officially open! Get started here.

Fabian Kunzke
Kilo Sage
Kilo Sage

This blog will extend the pevious blog about simple AjaxCalls by introducing a more complex usecase: custom reports.

First, let's start with the attachments:
You will find two updateset within this blog. One contains a very light application - a very simple chart framework -, the other one contains a demo report to utilize this framework. After both is installed, you can open and view the report by using the following url:

your_instance.service-now.com/x_145100_charts_Chart%20Render.do?sysparm_names=Incident

What ist this application for?

Have you ever tried to use reports for your service portal? Tried to tie in a report into a form? Did you ever need a dashboard for reports? This is what this framework is for. It allows you to open a ui page with an added url parameter selecting a custom report you defined within your instance. Don't worry if that sounds a bit too complicated, it's not. I will explain this in more detail in a second. First, let's answer one important question: Why can't i just use the oob reports? And the answer is: you can. But, i have run into limits with them. Here are just some of them:

- Layering multiple different reports can be tough if they don't share the same table (e.g. a report which compares the open incidents over time with the open changes and problems over time)
- oob reports have limited interactions and animations
- some reports cannot be visualized well (e.g. the worldmap reports are kind of disapointing)
- running reports on huge amount of data just takes ages (30s +)

Now the last reason is the one i had to create this framework. It is (at the moment) restricted to displaying line charts (honestly because i was too lazy to extend it, but that would not be an issue), but it is not restricted as to where those lines come from. Want a line chart displaying a report over 2 million incidents? No problem. Want to also display your 1 million changes in the same report? Feel free to do so. Want to display this within 3-5 seconds? No worries.
Now this sounds like something straight out of the future, but it has a simple reason as to why this works so much faster than oob reports: GlideAggregate. The important piece of information is: the oob reporting actually loads all records within a report. If you have a line chart with all incidents over those last 2 years, all those incidents are retrieved from the server. This way, if you click on the report, a list of those reports can be shown. From my experience though: this is very rarely needed. And thus, a lightweight report which just counts records instead of loading all of them (essentially a GlideAggregate) is just so much more efficient.

Now Fabian, hear me out. I have come here to learn something about GlideAjax, not this reporting stuff. Why should i care? Well, simply put: Know when to use oob parts. If you can use an oob report, do so. Compared to the last (and very entry level) blog this more advanced blog must also provide you with the reason of doing this. Otherwise, why would you use any scripting at all? But, i understand your point. Let's get going as to how this all works. Installed the update sets on your personal developer instance? Sweet. Let's dive into it!

Charts

First things first: this uses chart.js. This is a javascript bib which allows you to display charts from (imo) a very clean javascript based object. Nice.

How does this whole thing work?

Mainly, you got one ui page which will display the chart object and you have one script include which will build said object. And guess what: there is some GlideAjax in between. First, lets check our ui pages client script:

function getChartData()
{
	var ajaxCall = new GlideAjax('x_145100_charts.Chart_Generator');
	ajaxCall.addParam('sysparm_name', 'getChartData');
	var chartDataNames = getParameterValue('sysparm_names');
	ajaxCall.addParam('sysparm_data_name', chartDataNames);
	ajaxCall.getXMLAnswer(returnChartData);
}

function returnChartData(answer)
{
	// get data array from the response
	var chartJSON = answer;
	var chart = JSON.parse(chartJSON);
	drawChart(chart);
}

function drawChart(chart)
{
	var canvas = document.getElementById("myChart").getContext('2d');
	var myChart = new Chart(canvas, chart);
}

function getParameterValue(name) {
	
	var url = document.URL.parseQuery();
	if (url[name])
		return decodeURI(url[name]);
	
	return;
}

Doesn't look too weird, does it. Now what happens here becomes more obvious as we take a look at the html "code":

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
	<script></script>
	<body onload="getChartData()">
		<canvas id="myChart" width="400" height="400"></canvas>
	</body>
</j:jelly>

Ok, i am not joking, this is it. This is the ui page which will display any chart we throw at it (well, at least any chart which is in the chart.js format (hint: get familiar with it)). Within our xml we defined a blank canvas (this is esentially our html tag on which we will draw stuff on). Within our client script a function is defined which does an ajax call (wuhuuuuu) grabbing a chart object from the backend. And then all we do is add this chart to the canvas (this is the "drawChart" function). And thats it. Note: the "getParameterValue" function retrieves the name of the chart we are looking for from the URL. Now how does the Ajax call work? Let's take a closer look:

The ajax call

An ajax call generally follows 2 steps: First, define what you want to call and call it, second, do something with the response. Nothing else here:

function getChartData()
{
	var ajaxCall = new GlideAjax('x_145100_charts.Chart_Generator');
	ajaxCall.addParam('sysparm_name', 'getChartData');
	var chartDataNames = getParameterValue('sysparm_names');
	ajaxCall.addParam('sysparm_data_name', chartDataNames);
	ajaxCall.getXMLAnswer(returnChartData);
}

function returnChartData(answer)
{
	// get data array from the response
	var chartJSON = answer;
	var chart = JSON.parse(chartJSON);
	drawChart(chart);
}

We call our script include (the server side piece of code). In this case it is the Chart_Generator. As you can see this script include is within an application (x_145100_charts) scope. This is super helpful to keep your code architecture tidy and thus making maintaining code a lot easier. We essentially say: "This piece of code is for this use only. Don't use it anywhere else, because we can only guarantee, that it works here!". Next, we define the function we want to call (here its "getChartData" which, as you may guess, gets the data for a chart). Lastly, we define what exactly we are looking for by handing over additional parameters (in this case the chart data names). Note: You can also hand over objects here, just make sure to use a JSON.stringify() first!).

Then we wait (.getXMLAnswer).

Done waiting? Great, we got a response from the server. In this case it's the answer with a JSON string. Now why do we need a JSON string here? The communication between client and server is restricted to string based objects. In our case we are communicating a chart object (remember: this is the stuff we will draw within our canvas). This object is more than just a string. Therefore, we need to convert it to a string and then parse it back to a javascript object. Remember: Always use JSON based string-encoding when using GlideAjax for objects that are not strings.

All thats left to do now is hand this chart object over to the drawChart() function and we are done. Awesome. Now, let's look at the server side stuff:

getChartData: function()
	{
		var dataName = this.getParameter('sysparm_data_name');
		var chartRawData = calculateChartData(dataName);
		var singleDataset = {
			label: chartRawData.name,
			data: chartRawData.points
		};
		
		var datasets = [singleDataset];
		var labels = chartRawData.steps;
		var chart = createChartObject('line', datasets, labels);
		return JSON.stringify(chart);
	},

This is where we recieve the ajax call. First of all we take the information of the specified report name. Small note: If we would have a JSON encoded object as a parameter, we would use .parse() to get the javascript object (If you want to take a deeper look into parsing strategies with JSON look no further: boom). Next step is some coding magic. Actually it's just calling this function here:

function calculateChartData(dataName)
{
	var evaluator = new GlideScopedEvaluator();
	var dataScript = new GlideRecord('x_145100_charts_chart_data');
	dataScript.addQuery('name', 'IN', dataName);
	dataScript.query();
	if(dataScript.next())
		{
			var data = evaluator.evaluateScript(dataScript, 'script', null);
			var points = data.points;
			var steps = data.steps;
			var name = dataScript.getValue('name');
			
			return {points: points,
					steps: steps,
					name: name};
		}	
}

function createChartObject(chartType, datasets, labels)
{
	var data = {
		labels: labels,
		datasets: datasets
	};
	var scales = {
		yAxes: [{
			ticks: {beginAtZero: true}
		}]
	};
	var options = {
		scales: scales
	};
	var chart =
		{
			type: chartType,
			data: data,
			options: options
		};
	
	return chart;
}

This function checks a table (the chart data table) for our specified chart. Note: Within that table any script which returns a data object can be used. Within the demo data update set you will find a chart as an example.
Then, based on the specified chart data, this function builds a standard chart.js object (if you are using another chart library, adjust the code accordingly). Then, all we do is return the full chart object to the "getChartData" function. There it is then stringified into a JSON string and send back to our ui page which in return displays the chart within the canvas. And we are done (almost).

Let's take a short look at the example chart record:

(function toExecute() {
	var points = [];
	var steps = [];
	
	var incidentCount = new GlideAggregate('incident');
	incidentCount.addQuery('active', false);
	incidentCount.addAggregate('COUNT');
	incidentCount.addTrend('closed_at','Week');
	incidentCount.setGroup(false);
	incidentCount.query();
	
	while(incidentCount.next())
		{
			var countPerWeek = incidentCount.getAggregate('COUNT');
			var week = incidentCount.getValue('timeref');
			points.push(countPerWeek);
			steps.push(week);
		}
	
	var data = {points: points, steps: steps};
	return data;
})();

This is the script to generate the "data" for our chart. All it is is a simple GlideAggregate counting all inactive incidents  closed per week. Thats it. We then return this as steps (these are the markers for the week) and the value for each week (aka. the points). Aaaaand now we are done.

The takeaway

Why is GlideAjax so useful here? With the use of GlideAjax, we are able to create a simple framework which can be administered and maintained within the backend. Need a new custom report? Just add another record and your good to go. Esentially we can take all the responibility for logic and data gathering away from the client into the realm under our control: The backend. Here we can use stuff that is not even possible on the client side, like GlideAggregates and GlideEvaluators (this lets us execute javascript code from a script field). Not only does this allow us to have HUGE reusability, but we no longer depend on the client. We now have complete freedom over accessability, scripting and object access as we are on the server side. And if weneed more information from the client side, we can just hand that over via the GlideAjax parameters.

But let's be real: This is not used, is it? It is! I did build another UI Page which just contains a few iFrames. What do those iFrames do? They load the ui page with the report. All i have to do is extend the url of those iframes with the report i want to load. Want to know something awesome? I can load more than one report within one iframe by just comma seperating them. And here is the best part of it: Want to build a UI Page that hands over a query? No worries, add an ajax parameter. Want to load more complex charts such as a highcharts worldmap? No worries, just another library to add and then another chart record. Want to embed this into a form? No worries, just add a formatter to load this ui page and you are done (i actually encountered a usecase where i had to load vendor performance reports on the vendor form).

In short: Want to get away from coding ui pages for each and every single requirement that you have and start building reusable frameworks? Get started with GlideAjax!

 

As a last note: Use the application at your own risk. I have tested it on a Madrid instance and it works fine and i am using this for some of our costumers (with some additions), but still i cannot guarantee it will work for your usecase. So please, try it out on your personal developer instance first.

Other than that, i am more than exited about some feedback. Also, feel free to share any other points/usecases you want to get some insights on when it comes to GlideAjax and the use of "frameworks" and i will see what i can do.

Regards

Fabian

ps.: This may not work for internet explorer due to the chart.js utility.

1 Comment