Configuration Item Icon Assignment Widget, Part II

Walking on water and developing software from a specification are easy if both are frozen.”
Edward V. Berard

It wasn’t entirely true that my empty shell CI Icon Assignment widget was completely devoid of all code underneath. I did have to toss in the function that launches the Retina Icon Picker in order to show that in action, but I was able to steal most of that code from the little test page that I had built to try that guy out.

$scope.selectIcon = function(inx) {
	spModal.open({
		title: 'Select Icon',
		widget: 'icon-picker',
		buttons: [
			{label: '${Cancel}', cancel: true}
		],
		size: 'sm',
	}).then(function(response) {
		c.data.itemArray[inx].icon = response.selected;
	}});
};

In my original test page, I had a second response function to handle the Cancel button, but for our purposes here, that’s not really necessary. If you hit the Cancel button on the pop-up on this widget, it will just close and nothing else will happen, which is perfectly fine. The other alteration that I had to make was to include an index argument so that when you did make a selection, it was applied to the appropriate line. Other than that, it’s pretty much what we were working with earlier.

But the icon picker is not the only element on the screen that needs a little code behind it. We’ll need to do something with the Configuration Item picker as well. Selecting a CI from the picker should result in a new row added to the table for the selected CI, and then the picker should be cleared in preparation for another selection to be made. Since we have to go look up the label for our second column, that sounds like a server side operation, so our client side code will be pretty simple:

$scope.addSelected = function() {
	c.server.update();
	$('#snrp').select2("val","");
};

The first line just kicks the process over to the server side, and then then second line clears out the value of the sn-record-picker so that it is ready for another selection. Over on the server side, we do the work to add the new CI type to the list, as long as it isn’t already there.

if (input) {
	data.itemArray = input.itemArray;
	if (input.classToAdd && input.classToAdd.value) {
		addClassToList(input.classToAdd.value);
	}
}

function addClassToList(key) {
	var foundIt = false;
	for (var i=0; !foundIt && i<data.itemArray.length; i++) {
		if (data.itemArray[i].id == key) {
			foundIt = true;
		}
	}
	if (!foundIt) {
		var thisItem = {};
		thisItem.id = key;
		thisItem.name = getItemName(key);
		data.itemArray.push(thisItem);
	}
	input.classToAdd = {};
}

function getItemName(key) {
	var ciGR = new GlideRecord(key);
	return ciGR.getLabel();
}

That takes care of adding an item to the list. Now we have to add some code to handle removing an item from the list. We put a Delete icon at the end of each line, so we just need to build the code that will run when someone clicks on that icon. Since it is a delete, we will want to pop up a quick confirm dialog before we actually remove the item from the list, just to make sure that they did that on purpose. All of that can be handled on the client side, and fairly simply.

$scope.removeItem = function(inx) {
	spModal.confirm('Remove ' + c.data.itemArray[inx].id + ' from the list?').then(function(confirmed) {
		if (confirmed) {
			c.data.itemArray.splice(inx, 1);
		}
	});
};

To test all of that out, I added a few items to the list, selected an icon for each, and then tried to remove a few. During my testing, I realized that adding enough items to the list scrolls the icon selector off of the page and out of sight, which I didn’t really like, so I did a little rearranging and put the selector on top of the list instead of underneath. With that change, the modal delete confirmation now looks like this:

Delete confirmation pop-up on reconfigured screen layout

So now we have all of the code in place to select a CI, to select an icon for the CI, and to remove a CI from the list. Now all we have left to do is to put some code under the two buttons at the bottom of the screen. Of course, coding the Save button would be much easier if I knew where I was going store this information once I am through building the list, but I haven’t really given much thought to that just yet, since we hadn’t quite gotten to that part until just this moment. Now that writing the code for the buttons is the only thing left to do, it’s time to figure that out. But that could potentially be an entire installment in and of itself, so I think we’ll stop here and save that exercise for another day.

Configuration Item Icon Assignment Widget

“You don’t have to see the whole staircase, just take the first step.”
Martin Luther King, Jr.

The main reason that I wanted to find a way to include an Icon in a pick list is because I wanted to assign different icons to various classes of Configuration Items to help visually distinguish the items based on their type. Now that I know that I can’t just add an icon to a single SELECT statement, I’m not exactly sure how I am going to that, but that’s not today’s issue. When all is said and done, I may not even be able to do what I am hoping to do, but I’m not working on that part right now. I try not to get too distracted by the things that I don’t understand or don’t know how I’m going to accomplish before it’s time. Since I can’t code everything all at once, I don’t need to solve all of the mysteries all at once, either. My theory is that I should be able to figure it out when the time comes, so there is no need to worry about it right now. To keep productive, and to maintain focus, I like to deal with things One Piece at a Time.

Today, I want to build a function that will return the appropriate icon name based on a passed configuration item class, which will give me something to call when it is time to go get the icon associated with a particular item. I want to return an icon name in all circumstances, so if the specific CI class is not mapped to an icon, then I want the function to check the parent class, and basically keep doing that until an icon name is found. If it makes it all the way to the top and there is still no icon, then there needs to be a default, because one way or another, I want to return an icon name no matter what. I will eventually stuff this function into a Script Include of some kind, but for now, I just want to code out the function. Here is what I came up with.

getIcon: function(ciClass) {
	var icon = this.map[ciClass];
	if (!icon) {
		ciClass = this.getParentClass(ciClass);
		if (ciClass) {
			icon = this.getIcon(ciClass);
		}
	}
	return icon;
},

getParentClass: function(ciClass) {
	var parentClass = '';
	var tableGR = new GlideRecord('sys_db_object');
	if (tableGR.get('name', ciClass)) {
		parentClass = tableGR.super_class.name;
	}
	return parentClass;
},

OK, it turns out that it is actually two functions, but you get the idea. This code assumes that there is a map included that associates icon names to CI classes, and that the map is keyed by CI class and returns the icon name. We don’t have to have the fully populated map right now, but to test the code, we will at least need to stub it out with a minimum of one item. That should be fairly simple to do.

map: {
	cmdb_ci: 'configuration'
},

Since the cmdb_ci class is basically the root class of all Configuration Items, defining a value for that class essentially establishes the default. As you crawl up the parentage of any specific CI class, you will eventually find your way to the cmdb_ci class, so that should satisfy my requirement that there should always be a default response from the function call.

The stubbed-out map is a good start, but I want to build up my map using some kind of tool that will allow me to select a CI class and then use my new icon picker to select an appropriate icon for the class. Something that would look like this:

CI/Icon map maintenance tool

Once you added your CI class to the list, then you could click on the little magnifying glass icon to launch our icon picker to select your icon.

Using the Icon Picker to select an icon for a CI class

What you are seeing is just a screen mock up at this point, but putting the screen together is always a good place to start. Here is the HTML that I used to produce the screen image.

<div class="panel">
<div style="width: 100%; padding: 5px 50px;">
  <h2 style="width: 100%; text-align: center;">${Configuration Item Icon Assignment}</h2>
  <table class="table table-hover table-condensed">
    <thead>
      <tr>
        <th style="text-align: center;">Item</th>
        <th style="text-align: center;">Label</th>
        <th style="text-align: center;">Icon</th>
        <th style="text-align: center;">Icon Name</th>
        <th style="text-align: center;">Delete</th>
      </tr>
    </thead>
    <tbody>
      <tr ng-repeat="item in c.data.itemArray track by item.id | orderBy: 'id'" ng-hide="item.removed">
        <td data-th="Item"><input class="form-control" ng-model="item.id" readonly="readonly"/></td>
        <td data-th="Label"><input class="form-control" ng-model="item.name" readonly="readonly"/></td>
        <td data-th="Icon" style="text-align: center;"><span style="font-size: 25px;" class="icon icon-{{item.icon}}"></span></td>
        <td data-th="Icon Name">
          <span class="input-group" style="width: 100%;">
            <input class="form-control" ng-model="item.icon" readonly="readonly"/>
            <span class="input-group-btn" ng-click="selectIcon($index)" aria-hidden="false">
              <button class="btn-ref btn btn-default">
                <span class="icon icon-search" aria-hidden="true"></span>
                <span class="sr-only ng-binding">${Select an icon}</span>
              </button>
            </span>
          </span>
        </td>
        <td data-th="Delete" style="text-align: center;"><img src="/images/delete_row.gif" ng-click="removeItem($index)" alt="Click here to remove this item from the list" title="Click here to remove this item from the list" style="cursor: pointer;"/></td>
      </tr>
    </tbody>
  </table>
  <p>To add another Configuration Item to the list, select an item from below:</p>
  <sn-record-picker
    id="snrp"
    field="data.classToAdd"
    ng-change="addSelected()"
    table="'sys_db_object'"
    default-query="'super_class=72e7251abc002300aadb875973a34b54^ORsuper_class.super_class=72e7251abc002300aadb875973a34b54^ORsuper_class.super_class.super_class=72e7251abc002300aadb875973a34b54^ORsuper_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^ORsuper_class.super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^ORsuper_class.super_class.super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^ORsuper_class.super_class.super_class.super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54'"
    display-field="'label'"
    display-fields="'name'"
    search-fields="'label'"
    value-field="'name'">
  </sn-record-picker>
  <br/>
  <p>To remove an item from the list, click on the Delete icon.</p>
</div>

<div style="width: 100%; padding: 5px 50px; text-align: center;">
  <button ng-click="save()" class="btn btn-primary ng-binding ng-scope" role="button" title="Click here to save your changes">Save</button>
   
  <button ng-click="cancel()" class="btn ng-binding ng-scope" role="button" title="Click here to cancel your changes">Cancel</button>
</div>
</div>

For the sn-record-picker, I used my old friend, the sn-record-picker Helper, but before I got that far, I had to first work out the query. I wanted any CI table (the table name is also the class name), so I was looking for any table that was based on the cmdb_ci table. There may be an easier way to do this, but this works, so I just went with it.

super_class=72e7251abc002300aadb875973a34b54^OR
super_class.super_class=72e7251abc002300aadb875973a34b54^OR
super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^OR
super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^OR
super_class.super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^OR
super_class.super_class.super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54^OR
super_class.super_class.super_class.super_class.super_class.super_class.super_class=72e7251abc002300aadb875973a34b54

That’s an ugly, brute force way of doing that, but it gets the job done, which is really all that we are after at this point. The real fun will be putting all of the code behind the screen to both build up the map and then store it somewhere. That’s pretty complicated stuff, and I’m not really sure exactly how I am going to do all of that, so we’ll save that as an exercise for another day.