Skip to content
snhackery

Adventures in mangling the ServiceNow platform

  snhackery
  • Home
  • About
  • Disclaimer
  • Update Sets

Month: March 2023

Service Account Management, Part XIX

Posted on March 31, 2023 | by snhackery

“There are no big problems, there are just a lot of little problems.”
— Henry Ford

Last time, we wrapped up the initial table configuration for our Service Account dashboard and tested everything out to make sure that it all worked as intended. We also identified the fact that we need to add a second table to the configuration so that we can see the pending requests that have not yet created a record in the Service Account table.Before we do that, though, I decided that it would be useful to add a link to the original request on the Service Account record so that you could easily pull up the request from the account record.

New Original Request field on the Service Account table

To populate the field during the creation of the Service Account record, I pulled the Service Account Request Fulfillment flow up in the App Engine Studio and added an entry to drag in the data pill from the original request in the trigger.

Populating the new Original Request field during record creation

With that out of the way, we can return our attention to adding the new table to the dashboard configuration. To do that, we go back to the Content Selector Configuration Editor that we recently updated to correct a few issues related to Scoped Applications. Before we do that, though, let’s pull up the list of Requested Items and build ourselves a filter that we can use to show all of the open items for Service Accounts requested by the current operator.

List filter for open Service Account items requested by the current operator

We are looking for active items requesting the Service Account catalog item requested by the currently logged on user. The filter can be found in the URL under the sysparm_query parameter.

active%253Dtrue%255Ecat_item%253D55cc38eb970711100362bfb6f053afa8%255Erequest.requested_forDYNAMIC90d1921e5f510100a9ad2572f2b477fe

Of course, we have to do a few change alls to get rid of all of the double encoding present in this version, but once we do that we will have a workable filter for the pending state on our newly added table.

active=true^cat_item=55cc38eb970711100362bfb6f053afa8^request.requested_forDYNAMIC90d1921e5f510100a9ad2572f2b477fe

Now let’s jump into the editor and add our new table.

Adding the sc_req_item table to the configuration

For now, let’s assume that we don’t want anything to appear in the Active and Retired states, and we can use the same technique that we used on the original table when we didn’t want anything to appear for that table in the Pending state. We’ll set the field list to simply number, and set the filter to number=0.

Setting the fields and filter for the Active state

For the Pending state, we add a few more relevant fields and use the filter we snagged from the list URL earlier.

Setting the fields and filter for the Pending state

We can do a little more with this, but let’s save what we have for now and take it out for a spin, just to make sure that everything is still in working order. Saving the changes should take us to the generated script, which now looks like this.

var ServiceAccountDashboardConfig = Class.create();
ServiceAccountDashboardConfig.prototype = Object.extendsObject(global.ContentSelectorConfig, {
	initialize: function() {
	},

	perspective: [{
		name: 'requester',
		label: 'Requester',
		roles: ''
	},{
		name: 'fulfiller',
		label: 'Fulfiller',
		roles: 'itil'
	}],

	state: [{
		name: 'active',
		label: 'Active'
	},{
		name: 'retired',
		label: 'Retired'
	},{
		name: 'pending',
		label: 'Pending'
	}],

	table: {
		requester: [{
			name: 'x_660634_service_0_service_account',
			displayName: 'Service Account',
			active: {
				filter: 'active=true^ownerDYNAMIC90d1921e5f510100a9ad2572f2b477fe^ORowning_groupDYNAMICd6435e965f510100a9ad2572f2b47744',
				fields: 'number,type,user_id,owner,owning_group',
				svcarray: [],
				aggarray: [],
				btnarray: [],
				refmap: {
					sys_user: 'user_profile'
				},
				actarray: []
			},
			retired: {
				filter: 'active=false^ownerDYNAMIC90d1921e5f510100a9ad2572f2b477fe^ORowning_groupDYNAMICd6435e965f510100a9ad2572f2b47744',
				fields: 'number,type,user_id,owner,owning_group',
				svcarray: [],
				aggarray: [],
				btnarray: [],
				refmap: {
					sys_user: 'user_profile'
				},
				actarray: []
			},
			pending: {
				filter: 'number=0',
				fields: 'number',
				svcarray: [],
				aggarray: [],
				btnarray: [],
				refmap: {},
				actarray: []
			}
		},{
			name: 'sc_req_item',
			displayName: 'Requested Item',
			active: {
				filter: 'number=0',
				fields: 'number',
				svcarray: [],
				aggarray: [],
				btnarray: [],
				refmap: {},
				actarray: []
			},
			retired: {
				filter: 'number=0',
				fields: 'number',
				svcarray: [],
				aggarray: [],
				btnarray: [],
				refmap: {},
				actarray: []
			},
			pending: {
				filter: 'active=true^request.requested_forDYNAMIC90d1921e5f510100a9ad2572f2b477fe^cat_item=55cc38eb970711100362bfb6f053afa8',
				fields: 'number,opened,request.requested_for,stage',
				svcarray: [],
				aggarray: [],
				btnarray: [],
				refmap: {
					sys_user: 'user_profile'
				},
				actarray: []
			}
		}],
		fulfiller: [{
			name: 'x_660634_service_0_service_account',
			displayName: 'Service Account',
			active: {
				filter: 'active=true^type.fulfillment_groupDYNAMICd6435e965f510100a9ad2572f2b47744',
				fields: 'number,type,user_id,owner,owning_group',
				svcarray: [],
				aggarray: [],
				btnarray: [],
				refmap: {
					sys_user: 'user_profile'
				},
				actarray: []
			},
			retired: {
				filter: 'active=false^type.fulfillment_groupDYNAMICd6435e965f510100a9ad2572f2b47744',
				fields: 'number,type,user_id,owner,owning_group',
				svcarray: [],
				aggarray: [],
				btnarray: [],
				refmap: {
					sys_user: 'user_profile'
				},
				actarray: []
			},
			pending: {
				filter: 'number=0',
				fields: 'number',
				svcarray: [],
				aggarray: [],
				btnarray: [],
				refmap: {},
				actarray: []
			}
		}]
	},

	type: 'ServiceAccountDashboardConfig'
});

Now all we need to do is to pull up the dashboard with the modified configuration, click on the Pending state, and take a quick peek.

First test of the new configuration

Well, that’s not too bad. Looks like we screwed up on the field name for the open date, but other than that, things look pretty good. I want to add a few more columns from the catalog item variables anyway, which we can do by configuring some Scripted Value Columns, so let’s fix our little error and deal with those new fields in our next installment.

Posted in Projects | Tagged App Engine Studio, Catalog Requests, Content Selector, Data Table Widget, Scoped Application, Script Include, Service Account

Content Selector Configuration Editor, Corrected (again)

Posted on March 21, 2023 | by snhackery

“You’ve got to think about big things while you’re doing small things, so that all the small things go in the right direction.”
— Alvin Toffler

Recently I was playing around with the Content Selector Configuration Editor to create a dashboard for my Service Account Management app, which is a Scoped Application, and realized that the last fix that I put in to make things work with a Scoped Application did not quite go far enough. Looking things over it is quite clear that the original design had only considered support for global scripts, and my first attempt to rectify that oversight did not resolve all of the issues for configuration scripts that were not in the global scope. Today it is time to finish that up and correct all of the other shortcomings in that tool when working outside of the global scope.

For starters, the pick list for available scripts in the current version includes all of the configuration scripts for all scopes. What we really need is to limit that selection list to just those scripts in the current scope. Otherwise, you could potentially be editing a script in one scope while you are working in another scope, which will not end well if it works at all. To limit the list to just the scripts in the current scope, we need to add something like this to the filter:

^sys_scope=javascript:gs.getCurrentApplicationId()

That will turn this attribute in the record picker:

default-query="'active=true^scriptLIKEObject.extendsObject(ContentSelectorConfig^ORscriptLIKEObject.extendsObject(global.ContentSelectorConfig'"

… to this:

default-query="'active=true^sys_scope=javascript:gs.getCurrentApplicationId()^scriptLIKEObject.extendsObject(ContentSelectorConfig^ORscriptLIKEObject.extendsObject(global.ContentSelectorConfig'"

It would also be good to add a little more information to the help text for that field, so the entire snh-form-field tag now looks like this:

<snh-form-field
  snh-label="Content Selector Configuration"
  snh-model="c.data.script"
  snh-name="script"
  snh-type="reference"
  snh-help="Select the Content Selector Configuration from the current Scope that you would like to edit."
  snh-change="scriptSelected();"
  placeholder="Choose a Content Selector Configuration"
  table="'sys_script_include'"
  default-query="'active=true^sys_scope=javascript:gs.getCurrentApplicationId()^scriptLIKEObject.extendsObject(ContentSelectorConfig^ORscriptLIKEObject.extendsObject(global.ContentSelectorConfig'"
  display-field="'name'"
  search-fields="'name'"
  value-field="'api_name'"/>

That solves one problem, but there are others. When building the new script from the user’s input in the save() function of the widget’s server script, this conditional only reduces the API Name to the root name for global scripts:

if (data.scriptInclude.startsWith('global.')) {
	data.scriptInclude = data.scriptInclude.split('.')[1];
}

This needs to be done for scripts in any scope, so the entire conditional should just go away and simply be reduced to this:

data.scriptInclude = data.scriptInclude.split('.')[1];

Further down in that same function, this line again assumes that you are working in the global scope:

scriptGR.api_name = 'global.' + name;

The API Name is actually set for you whenever you save a new script, so this line can actually just be removed entirely and things will work just fine.

With all of these changes, the new save() function now looks like this:

function save() {
		if (input.script && input.script.value) {
			data.scriptInclude = input.script.value;
			data.scriptInclude = data.scriptInclude.split('.')[1];
		}
		var name = data.scriptInclude;
		if (input.newRecord) {
			name = input.newScriptName;
		}
		var script = "var ";
		script += name;
		script += " = Class.create();\n";
		script += name;
		script += ".prototype = Object.extendsObject(";
		if (gs.getCurrentApplicationScope() != 'global') {
			script += "global.";			
		}
		script += "ContentSelectorConfig, {\n";
		script += "	initialize: function() {\n";
		script += "	},\n";
		script += "\n";
		script += "	perspective: [";
		var separator = '';
		for (var p=0; p<input.config.perspective.length; p++) {
			var thisPerspective = input.config.perspective[p];
			script += separator;
			script += "{\n		name: '";
			script += thisPerspective.name;
			script += "',\n		label: '";
			script += thisPerspective.label;
			script += "',\n		roles: '";
			script += thisPerspective.roles || '';
			script += "'\n	}";
			separator = ",";
		}
		script += "],\n\n";
		script += "	state: [";
		separator = '';
		for (var s=0; s<input.config.state.length; s++) {
			var thisState = input.config.state[s];
			script += separator;
			script += "{\n		name: '";
			script += thisState.name;
			script += "',\n		label: '";
			script += thisState.label;
			script += "'\n	}";
			separator = ",";
		}
		script += "],\n\n";
		script += "	table: {";
		separator = '';
		for (var tp=0; tp<input.config.perspective.length; tp++) {
			var tablePerspective = input.config.perspective[tp];
			script += separator;
			script += "\n		";
			script += tablePerspective.name;
			script += ": [";
			var tableSeparator = '';
			for (var tt=0; tt<input.config.table[tablePerspective.name].length; tt++) {
				var tableTable = input.config.table[tablePerspective.name][tt];
				script += tableSeparator;
				script += "{\n			name: '";
				script += tableTable.name;
				script += "',\n			displayName: '";
				script += tableTable.displayName;
				script += "'";
				for (var ts=0; ts<input.config.state.length; ts++) {
					var tableState = input.config.state[ts];
					script += ",\n			";
					script += tableState.name;
					script += ": {\n				filter: '";
					script += tableTable[tableState.name].filter;
					script += "',\n				fields: '";
					script += tableTable[tableState.name].fields;
					script += "',\n				svcarray: [";
					var lastSeparator = '';
					for (var v=0; v<tableTable[tableState.name].svcarray.length; v++) {
						var thisScriptedValue = tableTable[tableState.name].svcarray[v];
						script += lastSeparator;
						script += "{\n					name: '";
						script += thisScriptedValue.name;
						script += "',\n					label: '";
						script += thisScriptedValue.label;
						script += "',\n					heading: '";
						script += thisScriptedValue.heading;
						script += "',\n					script: '";
						script += thisScriptedValue.script;
						script += "'\n				}";
						lastSeparator = ",";
					}
					script += "]";
					script += ",\n				aggarray: [";
					lastSeparator = '';
					for (var g=0; g<tableTable[tableState.name].aggarray.length; g++) {
						var thisAggregate = tableTable[tableState.name].aggarray[g];
						script += lastSeparator;
						script += "{\n					name: '";
						script += thisAggregate.name;
						script += "',\n					label: '";
						script += thisAggregate.label;
						script += "',\n					heading: '";
						script += thisAggregate.heading;
						script += "',\n					table: '";
						script += thisAggregate.table;
						script += "',\n					field: '";
						script += thisAggregate.field;
						script += "',\n					filter: '";
						script += thisAggregate.filter;
						script += "',\n					source: '";
						script += thisAggregate.source;
						script += "',\n					hint: '";
						script += thisAggregate.hint;
						script += "',\n					page_id: '";
						script += thisAggregate.page_id;
						script += "'\n				}";
						lastSeparator = ",";
					}
					script += "]";
					script += ",\n				btnarray: [";
					lastSeparator = '';
					for (var b=0; b<tableTable[tableState.name].btnarray.length; b++) {
						var thisButton = tableTable[tableState.name].btnarray[b];
						script += lastSeparator;
						script += "{\n					name: '";
						script += thisButton.name;
						script += "',\n					label: '";
						script += thisButton.label;
						script += "',\n					heading: '";
						script += thisButton.heading;
						script += "',\n					icon: '";
						script += thisButton.icon;
						script += "',\n					color: '";
						script += thisButton.color;
						script += "',\n					hint: '";
						script += thisButton.hint;
						script += "',\n					page_id: '";
						script += thisButton.page_id;
						script += "',\n					condition: '";
						if (thisButton.condition) {
							var condition = thisButton.condition;
							if (condition.indexOf("'") != -1) {
								condition = condition.replace(/'/g, "\\'");
							}
							script += condition;
						}
						script += "'\n				}";
						lastSeparator = ",";
					}
					script += "]";
					script += ",\n				refmap: {";
					lastSeparator = '';
					var indent = '';
					for (var key  in tableTable[tableState.name].refmap) {
						script += lastSeparator;
						script += "\n					";
						script += key;
						script += ": '";
						script += tableTable[tableState.name].refmap[key];
						script += "'";
						lastSeparator = ",";
						indent = '\n				';
					}
					script += indent + "}";
					script += ",\n				actarray: [";
					lastSeparator = '';
					for (var a=0; a<tableTable[tableState.name].actarray.length; a++) {
						var thisAction = tableTable[tableState.name].actarray[a];
						script += lastSeparator;
						script += "{\n					name: '";
						script += thisAction.name;
						script += "',\n					label: '";
						script += thisAction.label;
						script += "'\n				}";
						lastSeparator = ",";
					}
					script += "]";
					script += "\n			}";
				}
				script += "\n		}";
				tableSeparator = ",";
			}
			script += "]";
			separator = ",";
		}
		script += "\n	},\n\n";
		script += "	type: '";
		script += name;
		script += "'\n});";
		var scriptGR = new GlideRecord('sys_script_include');
		if (input.newRecord) {
			scriptGR.initialize();
			scriptGR.name = name;
			scriptGR.description = name;
			scriptGR.access = 'public';
			scriptGR.insert();
		} else {
			scriptGR.get('name', name);
		}
		scriptGR.setValue('script', script);
		scriptGR.update();
		data.sys_id = scriptGR.getUniqueValue();
}

All in all, not a huge number of changes, but just enough to make things work. I bundled all of the relevant parts into another Update Set that includes these various changes, which you can find here. This component is also a part of the larger SNH Data Table Widget collection, so eventually I will need to publish a new version of that collection out on Share as well.

Posted in Cool Stuff | Tagged Content Selector, Form Fields, Scoped Application, Service Account, Update Set, Widget

Service Account Management, Part XVIII

Posted on March 7, 2023 | by snhackery

“On your darkest days do not try to see the end of the tunnel by looking far ahead. Focus only on where you are right now. Then carefully take one step at a time, by placing just one foot in front of the other. Before you know it, you will turn that corner.”
— Anthon St. Maarten

Last time, we threw together the beginnings of a configuration script for Service Account dashboard using the Content Selector Configuration Editor. Now that we have a viable script, we need to create Service Portal Page that will utilize that configuration. To begin, we will pull up the list of Portal Pages and click on the New button to create a new page.

New Service Portal Page

We will call our new page Service Account Dashboard and give it an ID of service_account_dashboard. Once we submit the form we can pull it back up and use the link down at the bottom of the form to bring it up in Service Portal Designer. Onto the blank canvas we will drag a 12-wide container, and beneath that one, we will drag in a 3/9 container. Into the upper 12-wide container, we will drag in the Dynamic Service Portal Breadcrumbs widget, and into the 3 portion of the 3/9 container, we will drag in the Content Selector widget. In the 9 portion of the 3/9 container, we will pull in the SNH Data Table from URL Definition widget. Now that we have placed all of the widgets, we will need to edit them, starting with the Content Selector.

Configuring the Content Selector widget

Here is where we enter the full name of the configuration script that we created last time. Since this is a Scoped application, we need to include the scope with the name so that it can be successfully located. That’s all there is to configuring that widget, as most of the configuration information is contained in the referenced script. Configuring the Data Table widget is a little more involved.

Configuring the Data Table widget

Here we give it a title of Service Accounts and select an appropriate Glyph image. We check the Use Instance Title checkbox to get our title to show up, and we leave all of the rest of them unchecked. Once we save that and save the page, we should be ready to try it out, which we can do easily enough with the View page in new tab button up in the upper right-hand corner.

Active Service Accounts

So far, so good. The default selection is active Service Accounts from the requester’s perspective, and you can see all of the account records from our failed and successful test submissions. I went ahead and retired one of them so that we could test the Retired state. Let’s click on the Retired button and see how that comes out.

Retired Service Accounts

That looks good as well. Now let’s try the Pending state, which should come up empty for the Service Account table, as pending requests have not gotten far enough along in the process to have created the record in that table yet.

Pending Service Accounts

Well, that’s not right! But you knew things were going too well at this point and it was about time for something to go horribly wrong. This is just a problem with our Filter, though, and should be easily remedied. We used the filter 1=0, which obviously did not work, so let’s try using an actual field from the table and do something like this in our config file:

filter: 'number=0',

Before we add that to all of the pending configurations, let’s pull up the dashboard again and see how that looks.

Pending Service Accounts, corrected

That’s better. Of course, to actually see the pending Service Accounts, we will need to add another table to our configuration. We can go back into the Content Selector Configuration Editor to do that, and then go back to the dashboard and check it out. That sounds like a good exercise for our next installment.

Posted in Projects | Tagged Breadcrumbs, Content Selector, Data Table Widget, Portal Page, Service Account, Service Portal, Service Portal Designer

Recent Posts

  • Periodic Review, Part XI
  • Periodic Review, Part X
  • Periodic Review, Part IX
  • Periodic Review, Part VIII
  • Periodic Review, Part VII

Recent Comments

  • snhackery on Service Account Management, Part XVI
  • Jennifer Schoenhoeft on Service Account Management, Part XVI
  • snhackery on Service Portal Form Fields, Broken
  • Joe Blogs on Service Portal Form Fields, Broken
  • Joe Blogs on Service Portal Form Fields, Broken

Archives

  • February 2024
  • September 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • April 2023
  • March 2023
  • February 2023
  • January 2023
  • December 2022
  • November 2022
  • October 2022
  • September 2022
  • August 2022
  • July 2022
  • June 2022
  • May 2022
  • April 2022
  • March 2022
  • February 2022
  • January 2022
  • December 2021
  • November 2021
  • October 2021
  • September 2021
  • August 2021
  • July 2021
  • June 2021
  • May 2021
  • April 2021
  • March 2021
  • February 2021
  • January 2021
  • December 2020
  • November 2020
  • October 2020
  • September 2020
  • August 2020
  • July 2020
  • June 2020
  • May 2020
  • April 2020
  • March 2020
  • February 2020
  • January 2020
  • December 2019
  • November 2019
  • October 2019
  • September 2019
  • August 2019
  • July 2019
  • June 2019
  • May 2019
  • April 2019
  • March 2019
  • February 2019
  • January 2019
  • December 2018

Categories

  • Cool Stuff
  • Discoveries
  • General
  • Hackery
  • Projects

Meta

  • Log in
  • Entries feed
  • Comments feed
  • WordPress.org

Subscribe to snhackery via Email

Enter your email address to subscribe to snhackery and receive notifications of new posts by email.

Useful ServiceNow links:
Main web site: https://servicenow.com
Developer site: https://developer.servicenow.com
Annual Conference:   https://knowledge.servicenow.com