Skip navigation

"Deep linking consists of using a uniform resource identifier (URI) that links to a specific location within an app rather than simply launching the app."

Wikipedia

 

 

 

 

DISCLAIMER This functionality isn't officially supported by service portal. Use at your own risk.

 

In this post we will continue from where we left off on Single page app on Service Portal  Part 1

 

Ui Router supports url history out of the box by appending hashes to the url but unfortunately it isn't possible within service portal.

 

For this demo we will implement our own workaround to accomplish the unique url functionality in our application by using get parameters (?) instead of hash (#).

 

Let's get to it!  All the functionality will live inside the client script of helloworld-uiview widget defined on the blog post part 1, lets see where we left off.

 

 

function ($state,$location) {

      if ($location.path().indexOf('editor') != -1) {

        return;

    }

    $state.go('homepage');

}

 

 

 

Let's start by loading all the parameters from the url, then removing $state.go('homepage') as we want to read from the url what state to load. Your code should look like this:

 

function ($state,$location) {

  if ($location.path().indexOf('editor') != -1) {

    return;

  }

  // returns an object with the url parameters

  var urlParams = $location.search();

 

 

  // Check if we have a variable called state from the url, if there is we load that state passing all the parameters

  if (urlParams.state) {

    $state.go(urlParams.state, urlParams);

    // If state isn't present in the parameters send user to default state

  } else {

    $state.go('homepage');

  }

}

 

 

Lets save our widget and go to /helloworld?state=detail and you should see the detail view

pasted image 0.png

 

But if you click on Go To Homepage View you are going to notice the url doesn't change. That's because we are not done yet. Let's go back to our uiview and add that functionality.

 

Next we are going to listen to a UI Router event called $stateChangeSuccess fired once the state transition is complete, every time it happens we are going to check it new state is different than old state (this can be improved to support same state with different parameters) if its different we are going to append state to parameters, in case the state wasnt specified in the url, and we manipulate  the url with angular $location service

 

 

function ($state,$location,$rootScope) {

  if ($location.path().indexOf('editor') != -1) {

    return;

  }

  // returns an object with the url parameters

  var urlParams = $location.search();

 

  // Check if we have a variable called state from the url, if there is we load that state passing all the parameters

  if (urlParams.state) {

    $state.go(urlParams.state, urlParams);

    // If state isn't present in the parameters send user to default state

  } else {

    $state.go('homepage');

  }

 

    $rootScope.$on('$stateChangeSuccess',

        function (event, toState, toParams, fromState, fromParams) {

            if (toState.name != fromState.name) {

                toParams.state = toState.name;

                $location.search(toParams);

            }

        }

    );

 

}

 

IMPORTANT: Please be careful when choosing your state parameters as they can conflict with service portal . e.g. Never use ID as SP relies on to define what page to load 

 

Save, test your portal and Voilà!

 

gif-generated.gif

 

 

NOTE: Your widget will reload every time the url changes (not 100% why other than $rootScope.$apply() being called somewhere else). There is a work around that, but this is topic for another time.

 

Thank you

 

Arthur Oliveira

Today at 8:00AM the CreatorCon Challenge Entry Period begins and the Entry website is now open for business. Here's a rundown of the requirements for an eligible entry, and the three-step entry process. Note: this blog post is not a substitute for the official Terms and Conditions that you must read and accept as part of the entry requirements. This blog post hits only selected highlights, sort of like a Cliffs Notes, and also contains tips and guidance.

 

ServiceNow_CreatorCon_linkedin_red_v1.png

 

Eligible Entity Requirements

 

  1. Open only to independent legal entities (if in USA, an LLC, S Corp, or C Corp). The name and website of the independent legal entity must be specified in the Personal/Company Profile (see below) by the entry deadline of 2/15/17 at 8:00PM Pacific Time. You can enter now if you're an independent developer, but if you're serious about advancing in the Challenge, you'll need to create a company and submit the name and website in the Personal/Company Profile by 2/15/17 or your entry is not eligible.
  2. Have not received an entity valuation as a result of equity financing (very early stage startup)
  3. Have received total cash funding of less than $3M USD
  4. Entity's principal business must be developing and selling commercial pre-packaged software-as-a-service. Companies that are today primarily Services/Implementation companies must spin off an ISV independent legal entity to be an eligible entity.
  5. Entity cannot be managed by current ServiceNow employees, or former ServiceNow employees as of 9/28/15 or later
  6. Must create (or use an existing) a single designated ServiceNow developer account to enter and develop the app on the developer instance associated with that designated account.
  7. Read, accept, and comply with the official Terms and Conditions

 

Eligible App Requirements

 

  1. One app per entity.
  2. App cannot have been submitted to ServiceNow for technical certification prior to August 1, 2016
  3. Entity agrees to publish a winning app on the ServiceNow Store by August 15th, 2017
  4. Apps must be developed on the ServiceNow platform Helsinki or Istanbul release by the entry deadline. Shortlisted and finalist apps must be on Istanbul by CreatorCon (May 9th-11th 2017)
  5. The principal value must be delivered by the app running on the ServiceNow platform. We are not seeking out integrations where the primary value is delivered on another platform. In support of this, part of the app evaluation criteria is the extent (depth and breadth) of your app's utilization of ServiceNow platform services. You are of course welcome and encouraged to use the ServiceNow platform's extensive REST API and integration tools platform services in your app and to integrate with whatever you want to, but be mindful that the principal value of your app must be delivered on the ServiceNow platform.

 

Entry Process

 

Step 0. If you don't already have one, you need to create and be logged into a single designated ServiceNow developer account.

Step 1. Complete and submit the Personal/Company Profile Entry form at the Entry website, or click on Enter from the CreatorCon Challenge landing page. Again, if you are currently an indie developer, you must submit your Company Name and Website by the entry deadline (2/15/17) for an eligible entry.

Step 2. Complete and submit the App Profile. If you do not already have a developer instance at the time of submitting your App Profile, click on the Get One Now link right below the Developer Instance Name field and follow the instructions to obtain your developer instance. When you return to the App Profile entry form, you may need to refresh the page for your developer instance to populate. Your developer instance is where you must create your app and where ServiceNow will go starting on 2/15/17 to evaluate apps for technical viability and match to your app demo video. Developer instance name cannot be changed.

 

App Profile.PNG

 

Notice the three circles in the Entry nav. They are white with three dots while incomplete,  and toggle to red with a white checkmark when completed. You can click on any of the dots to go immediately to that form to make changes and updates (App Materials Submission opens on Dec 9th).

 

Step 3. (Opens December 9th) Submit Application Materials

 

Mandatory Application Materials

 

1. Your app, resident on the developer instance associated with your designated ServiceNow developer account (the developer instance in your App Profile).

2. A 2-minute investor pitch video

3. An up to 5-minute on-screen app demo video from both the admin and end user perspective.

 

Optional Application Materials

 

Supporting content such as a business plan, investor presentation, whitepaper, explainer video etc. are welcome but not required.

 

Updating Your Entry

 

After your initial submission of the Personal/Company Profile form, you can come back at any time as often as you like until the entry deadline to make changes (except your name, email address, and developer instance - these are fixed by your designated ServiceNow developer account, and cannot be changed) and add/update new information.

 

You can get to the Entry website directly from the Developer Dashboard - click on Manage->CreatorCon Challenge Entry.

 

manage creatorconchallenge entry.png

 

 


With $500,000 in cash investments and approximately $325,000 worth of non-investment (marketing/publicity and business development) prizes on offer, this is a golden opportunity for your startup  to partner with ServiceNow and take your company to the next level faster. We are looking forward to seeing what you build.

Martin Barclay
Director, Product Marketing
App Store and ISVs
ServiceNow
Santa Clara, CA

"Single-Page Applications are Web apps that load a single HTML page and dynamically update that page as the user interacts with the app. "

Microsoft Magazine

 

 

 

 

For this example we will be using ui-router to manage state and what is displayed to the user but with this approach you could use any state manager.

 

Let's call our project Hello World

 

Some information about our project

Portal:

Title: Hello World

Url suffix: helloworld

Homepage: helloworld_homepage (an empty page that we creating the portal)

 

 

 

 

Step 1

First let's download the library (https://github.com/angular-ui/ui-router) and create a UI Script with its content . The minified version is recommended.

 

 

 

Step 2

Let's create a UI Script that our application will rely on. That UI Script can be used to create factories, services, filter, etc. Basically any angular 1 component.

I'll name our UI Script helloworld-uiscript. I find that appending uiscript to the name makes it easier to identify it later on when we start working with widgets as well.

 

In our UIScript I'll start creating an angular module called helloworld and adding ui.router as dependency. This is pure angular code and it should look  very familiar to angular developers

 

(function (angular) {

    angular.module('helloworld', ['ui.router'])

})(angular);

 

 

Next let's define our states. For this demo we are creating 2 states. I'll try to keep it simple naming it homepage and detail but for your application make sure your state name reflect the state of your application.

 

IMPORTANT Please note some states are already being used by service portal and it will break if you try to create a state with the same name, that's why I avoided using state1 and state2 and used homepage and detail instead

 

 

Lets add a configuration block to our helloworld application. We will also inject $stateProvider which is provided by uiRouter and define our states.

 

(function(angular) {

    angular.module('helloworld', ['ui.router'])

        .config(function($stateProvider) {

            $stateProvider

                .state('homepage', {

                })

                .state('detail', {

                })

        })

}])(angular);

 

 

Next we need to define what content (template) we will display for each state. We have couple options here.

 

Option 1:

We can define our template inline using the template property. It would look somewhat like this:

 

.state('homepage', {

template: "<h1>Hello World Title - Homepage</h1>"

})

 

This template property could become massive depending on what our state would display, so let's take a look at some other options

 

 

Option 2:

UI Pages! As alternative to option 1, we could use the property templateUrl to point each state to a UI Page. It would look like this:

 

.state('homepage', {

templateUrl: "homepage.do"

})

 

 

Option 3:

TemplateCache! We can also use templateCache to point to a template ID that we will define later on. IT would look like this

.state('homepage', {

templateUrl: "homepage"

})

 

Since option 1 and 2 is pretty straightforward I'll implement our helloworld demo using Option 3.

 

Your UI Script should look like this at the end

 

(function (angular) {

    angular.module('helloworld', ['ui.router'])

        .config(function ($stateProvider) {

 

            $stateProvider

                .state('homepage', {

                    templateUrl: "homepage"

                })

                .state('detail', {

                    templateUrl: "detail"

                })

        })

})(angular);

 

UPDATE:  SP ng-template might be a better solution for a production application but the concept is the same

 

Step 3:

Now lets create our custom widget that will wrap our application, add the proper dependencies and add it to our homepage.

 

You can create the widget using the widget editor view or servicenow form. I'll use the widget editor by going to /sp_config?id=widget_editor but we will need to switch to the form view to add the dependencies

 

For this widget I'm going to name it helloworld-uiview

 

HTML

Let's start creating our cached templates defined in the uiscript (state1 and state2) then we will add a ui-view directive provided by ui router. Your code should look like this

 

 

<script type="text/ng-template" id="homepage">

  <h1>Homepage View</h1>

  <a ui-sref="detail" class="btn btn-primary">Go To Detail View</a>

</script>

 

<script type="text/ng-template" id="detail">

  <h1>Detail View</h1>

  <a ui-sref="homepage" class="btn btn-primary">Go To Homepage View</a>

</script>

 

<ui-view></ui-view>

 

 

 

CLIENT SCRIPT

In the client script we need to define our default state AKA home state. For this we need to inject $state, provided by ui router

 

IMPORTANT: Later in the process we are going to access a page called $spd.do . This page relies on $state to work, so we need to write some work around to stop $state manipulation  for when accessing this page

 

Your code should look like this:

 

function ($state,$location) {

  if ($location.path().indexOf('editor') != -1) {

    return;

  }

  $state.go('homepage');

}

 

 

 

No Server side script, SCSS and link function are needed for this example, now let's switch to the platform view by clicking on the hamburger menu on the top right

 

 

 

Setting up Dependencies

Scroll down to Dependencies and let's create a new dependency (sp_dependency)

Name: helloworld Dependency

Include on page load: yes

Portals for pageload: helloworld

Angular module name: helloworld (defined in our ui script)

 

Save instead of submitting so we stay on the same page and we can add the files dependency

 

Now lets create 2 new JS Includes. One for our helloworld-uiscript and another for ui router. It's important that the Order field for UI router is smaller than helloworld-uiscript since our uiscript injects uirouter as dependency

 

Display Name: UI Router

Source: UI Script

UI script: ui router

 

Display Name: UI Router

Source: UI Script

UI script: helloworld uiscript

 

 

 

Step 4

The alst step is adding our helloworld-uiview to the homepage.

 

We can do this by going to /$spd.do

 

 

Final Result

 

gif-generated.gif

You can view a live demo here:

https://empaoliveira.service-now.com/helloworld

 

I hope it helped!

 

Soon I'll post a part 2 where I'll explain how to develop single page app with deep linking so you can use the history tab to go back or send a user to a specific state.

 

Thank you

GlideFilter is used to determine if a GlideRecord meets the conditions specified in an encoded query string. For instance, it's used in the business rule condition builder to determine under which conditions the business rule must run. It's not well documented and its usage is slightly different between global and scoped apps, so there seems to be some confusion, especially on how it's used with regular expressions. I'll share what I know here and update this based on your feedback/additions/corrections.

 

The only official documentation available is Scoped GlideFilter API Reference. However, its variation is available in the global scope as well. Let's take a look at how they're used and how they're different (as of the Helsinki release).

 

SCOPED GlideFilter

 

Scoped GlideFilter is an object that is used without having to instantiate. It has only one method:

 

boolean checkRecord(GlideRecord gr, string filter, boolean matchAll)  // returns true if gr meets the conditions specified in filter

 

gr: a single GlideRecord

filter: an encoded query string

matchAll: (Optional) if true (default), all conditions in filter must match to return true; if false, any single true condition returns true (see below)

 

Here's an example:

 

var gr = new GlideRecord('incident');
gr.query();
var filter = 'active=true^state=2';  // active && state == 'In Progress'
while (gr.next()) {  // iterate through records
  if (GlideFilter.checkRecord(gr, filter)) gs.info(gr.number);  // test each record for the filter conditions
}

 

This returns all incident records that are active and the state is 'In Progress'. Some points to note are:

 

  1. GlideFilter is not instantiated (no need for new GlideFilter()).
  2. GlideFiter.checkRecord() is applied to each record after gr.query() is executed. In other words, GlideFilter is not used by gr.query().

 

USING MATCH_RGX FOR REGULAR EXPRESSION MATCH

 

GlideFilter also allows the use of regular expressions in the filter using the MATCH_RGX operator. Here's an example, modified from the previous one:

 

var gr = new GlideRecord('incident');
gr.query();
var filter = 'active=true^state=2^numberMATCH_RGXINC.*';  // active && state == 'In Progress' && /^INC.*$/m.test(number)
while (gr.next()) {  // iterate through records
  if (GlideFilter.checkRecord(gr, filter)) gs.info(gr.number);  // test each record for the filter conditions
}

 

This returns all incident records that are active, the state is 'In Progress' and the number starts with 'INC' (since all incident numbers start with 'INC', this doesn't really do anything). What's not well known and often a source of confusion is that MATCH_RGX already includes /^ and $/m, the start and end of string in multiline mode; the regular expression filter condition specified, therefore, is what comes between /^ and $/m. So

 

fieldMATCH_RGXcondition

 

is equivalent to, in JavaScript,

 

/^condition$/m.test(field)

 

Also, the test is case sensitive (we'll see below that there's a way to make this case insensitive in the global scope but not in scoped GlideFilter). Some examples and their JavaScript equivalents are

 

fieldMATCH_RGXabc === /^abc$/m.test(field)  // field exactly matches "abc"
fieldMATCH_RGXabc.* === /^abc.*$/m.test(field)  // field starts with "abc"
fieldMATCH_RGX.*abc === /^.*abc$/m.test(field)  // field ends with "abc"
fieldMATCH_RGX.*abc.* === /^.*abc.*$/m.test(field)  // field contains "abc"

 

It's worthwhile noting that MATCH_RGX is not available in GlideRecord's .addEncodedQuery() method (I wish it was); this indicates there are different flavors of encoded queries in ServiceNow.

 

GLOBAL GlideFilter

 

The examples used above for Scoped GlideFilter also work in the global scope. In addition, Global GlideFilter provides the following features:

 

new GlideFilter(string filter, string title)

 

filter: an encoded query string

title: title of the filter

 

boolean match(GlideRecord gr, boolean matchAll)  // returns true if gr meets the conditions specified in filter from GlideFilter instance

 

gr: a single GlideRecord

matchAll: if true, all conditions in filter must match to return true; if false, any single true condition returns true (see below). This is not optional, unlike in .checkRecord().

 

void setCaseSensitive(boolean caseSensitivity)  // sets whether .match() is case sensitive (does not apply to .checkRecord())

 

caseSensitivity: if true, .match() is case sensitive (does not apply to .checkRecord()).

 

Here are some additional properties and methods:

 

boolean caseSensitive  // true if .match() is case sensitive
string filter  // encoded query string of the filter
string getFilter()  // returns encoded query string of the filter
string title  // title of the filter
string getTitle()  // returns the title of the filter
void setDisplayTitle(String displayTitle)  // sets the display title of the filter
string getDisplayTitle()  // returns the display title of the filter (returns title if display title not set)
string script  // a string representing the source code of the JavaScript function for evaluating the filter conditions

 

Here's an example:

 

var gr = new GlideRecord('incident');
gr.query();
var filter = 'active=true^state=2^numberMATCH_RGXinc.*';  // active && state == 'In Progress' && /^inc.*$/i.test(number)
var gf = new GlideFilter(filter, '');
gf.setCaseSensitive(false);
while (gr.next()) {  // iterate through records
  if (gf.match(gr, true)) gs.info(gr.number);  // test each record for the filter conditions
}

 

Just like what we saw earlier, this returns all incident records that are active, the state is 'In Progress' and the number starts with 'inc' (case insensitive). .match() works similarly to .checkRecord(), but with an added benefit of evaluating case-sensitive or insensitive matches.

 

matchAll FLAG

 

The .script property, which shows the JavaScript function that evaluates the filter conditions, gives a clue on how the matchAll flag works. Using the above example:

 

matchAll = true

function trecord() {return !!(current.active == true && current.state.toString().toLowerCase() == 2 && RegExp('^inc.*$','mi').test(current.number.toString()));}trecord();

 

matchAll = false (effectively all conditions become OR conditions)

function trecord() {if (current.active == true ) return true;if (current.state == 2 ) return true;if (RegExp('^inc.*$','m').test(current.number.toString()) ) return true; return false;}trecord();

 

SUMMARY

 

Here's a summary of how GlideFilter is used in both global and scoped apps:

 

  1. GlideFilter is used to test encoded query filter conditions one record at a time; it can't be used to filter a bulk record set.
  2. GlideFilter supports regular expression matches using the MATCH_RGX operator: fieldMATCH_RGXcondition is equivalent to /^condition$/m.test(field) in JavaScript, with the /^ and $/m automatically added.
  3. In scoped apps,
    • GlideFilter is an object that is used without having to instantiate.
    • GlideFilter has only one method: .checkRecord().
  4. In global apps,
    • GlideFilter can be used the same way as in scoped apps.
    • GlideFilter can be instantiated, which gives two main methods: .match(), .setCaseSensitive().
  5. Encoded queries in GlideFilter support MATCH_RGX but those used in GlideRecord's .addEncodedQuery() don't.

 

 

 

UPDATES

 

2016-11-21 corrected matchAll parameter and added .script example output; corrected title parameter; added multiline flag to regular expressions; added additional properties and methods for GlideFilter().

 

Please feel free to connect, follow, post feedback / questions / comments, share, like, bookmark, endorse.

John Chun, PhD PMP see John's LinkedIn profile

visit snowaid

ServiceNow Advocate
Jim Coyne

"Developer Toolbox" Tool

Posted by Jim Coyne Nov 9, 2016

Not really a tool in itself, but a collection of modules with links to some tables and pages within the platform.  Some are not used very often, but are useful nonetheless.  Most of them are for tables that do not have an existing module and I can never remember the table name, or keep misspelling it 

 

I've attached an XML export of my current Update Set.  It contains records for:

  • the "Developer Toolbox" Application Menu, which is restricted to the "admin" role and normally is the first Application shown in the Navigator
  • the "Developer Toolbox" Menu Category which adds a red highlight to the Application menu (UI14 and below)
  • the "glide.ui14.navigator.use_border_color" System Property, set to "true", which shows the red highlight on the Application Menu (see Fuji screenshot below)
  • an Image, "u_CMDBHardwareHierarchy.gif", used to show a subset of the Hardware CMDB class hierarchy from the "Hardware Hierarchy Map" Module (I find it useful to show new clients how classes work)
  • numerous Modules (currently 46) with links to different tables and pages

 

The Modules are grouped into three different sections:

  • Useful Links
  • Useful Pages
  • Verify Post-Clone Tasks

 

Screenshot from Geneva:

 

Screenshot from Fuji:

 

 

Here's the CMDB hierarchy image:

u_CMDBHardwareHierarchy.gif

 

It helps when explaining the class/subclass inheritance model to clients.

 

I usually install the Update Set in every instance that I work in, hoping to add a little bit of productivity to my day.  The Post-Clone section is handy for verifying common things after you perform a clone.  I keep adding new modules as I come across new tables, pages or tasks that I need to perform.

 

Please share any links you may have created in the past or might be inspired to add in the future.

*** Please Like and/or tag responses as being Correct.
And don't be shy about tagging reponses as Helpful if they were, even if it was not a response to one of your own questions ***

There's a pretty common use case in ServiceNow that goes something like this: A new user is onboarded and some sort of automation kicks off an onboarding process in ServiceNow. As part of the process ServiceNow sends an email to the hiring manager with a url that points to an order guide or catalog item. In that URL you might have included some url parameters that will populate some of the variables to help the hiring manager out and also to reduce the possibility or errors. In order to accomplish this you would use an onLoad catalog client script on the order guide or catalog item you're loading. The script commonly looks something like this:

 

function onLoad() {
    //Use the 'getParameterValue' function below to get the parameter values from the URL  
    var user = getParameterValue('sysparm_user');
    if (user) {
        g_form.setValue('user_variable', user);
    }
}

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

 

This works really well and is documented in a few places including one of my blog posts. However, this script will not work when your order guide or catalog item is rendered within Service Portal. The reason is that Service Portal blocks a number of javascript global objects from running within client scripts including document and prototype which are both used in line 11.

 

So what do you do if you want to use url parameters to populate values into catalog artifacts in the Service Portal? I recently discovered that the top object is not blocked, so I tried modifying this script a bit and it seems to work. I will admit that this method is more of a workaround/hack, but it's the only way I've been able to populate a variable from the url through the Service Portal so far.

 

function onLoad() {
    //Use the 'getParameterValue' function below to get the parameter values from the URL  
    var user = getParameterValue('sysparm_user');
    if (user) {
        g_form.setValue('user_variable', user);
    }
}

function getParameterValue(name) {
    name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
    var regexS = "[\\?&]" + name + "=([^&#]*)";
    var regex = new RegExp(regexS);
    var results = regex.exec(top.location);
    if (results == null) {
        return "";
    } else {
        return unescape(results[1]);
    }
}

 

So in this case if your url included &sysparm_user=Brad at the end then the value 'Brad' would be populated into the user_variable variable. Something to note is that this code snippet should be used within a client script or catalog client script on a form you're rendering through the Service Portal. If you're working within a widget and want to use a url parameter you would want to use angular's $location service.

 

Feel free to leave any feedback, and if anyone has a better way to do this please let us know in the comments.

It’s that time of year again!  Time to submit proposals for breakouts and workshops for the next CreatorCon.  We’re making CreatorCon bigger and better this year by running for three days in parallel with Knowledge.  Which means there’s even more chances for you to share all the cool stuff you’re doing on the ServiceNow platform with your fellow developers and architects.

 

Have you built a cool integration or built an app that solved a problem in a unique and different way?  How about a super-engaging user experience with Service Portal?  Do you have an architectural model that works really well for when to use ServiceNow as a system of record vs system of engagement or to orchestrate complex processes across people and systems?  Come demo how you made it happen for your peers.  Want to take your sharing to the next level?  Design a hands-on workshop so your fellow developers can not only see how you built your solution, but get to experience building it themselves.

 

If your session is selected, you’ll get a complimentary pass to Knowledge and CreatorCon.  Submit your proposals here by Dec 5th

If you're looking to decrease time to resolution and improve your SLA success rates, you should check out this webinar from ServiceNow Technology Partner Ushur on how to build ServiceBots for ServiceNow.

 

ServiceNow users can put Ushur ServiceBots to work right now by downloading bots for mobile approvals, resolutions, and messaging from the ServiceNow Store.

 

And the cool part is they recently launched the Ushur ServiceNow SDK which enables developers to link ServiceNow tables to ServiceBots, and enables mobile self-service without leaving the ServiceNow platform.

ushur_servicebots_webinar.png

 

Boost your SLAs with Ushur ServiceBots today!

Martin Barclay
Director, Product Marketing
App Store and ISVs
ServiceNow
Santa Clara, CA

In this post, I am going to outline how I created an interactive scatter plot in a Service Portal widget by leveraging D3.js. For this example, we are going to create the scatter plot using project records that are stored in our ServiceNow instance. What I would really like to highlight in this post is how you can use different data channels to tell different stories.

 

This particular scatter plot uses the following channels (how a particular shape can visually display data) to demonstrate different project attributes:

  1. X-axis displays the planned duration of the project
  2. Y-axis displays the planned cost of the project
  3. The size of each circle displays the planned effort of the project relative to the other projects displayed
  4. The color of each circle displays the priority of the project
  5. The line of best fit displays where the project sits in relation to the other projects in terms of planned duration vs. planned cost

 

Here is a full screenshot of this scatter plot to give a better idea of how these channels are being used:

Post 6 Scatter Plot.png

 

Since there are previous posts giving a more in-depth introduction to using D3 and Service Portal together, I won't go into much detail with the code. Here is the pasted code for my HTML, CSS, Client Script, and Server Script:

 

HTML

 

<div class="centered-chart row">

  <div id="chart"></div>

</div>

 

CSS

 

.axis path,

.axis line {

  fill: none;

  stroke: #000;

  shape-rendering: crispEdges;

}

 

.dot {

  stroke: gray;

}

 

.dot:hover {

  stroke: black;

}

 

.centered-chart {

  text-align: center;

  font: 10px sans-serif;

}

 

.p1 {

  fill: #FF6347;

}

 

.p2 {

  fill: #FFA500;

}

 

.p3 {

  fill: #FFD700;

}

 

.p4 {

  fill: #87CEFA;

}

 

.p5 {

  fill: #90EE90;

}

 

Client Script

 

function() {

            /* widget controller */

            var c = this;

         

            var data = c.data.projects;

         

            var margin = {top: 20, right: 75, bottom: 75, left: 75},

            width = 1170 - margin.left - margin.right,

            height = 500 - margin.top - margin.bottom,

            minCircle = 2, maxCircle = 20;

         

            var maxDuration = d3.max(data, function(d) { return d.duration; });

            var minDuration = d3.min(data, function(d) { return d.duration; });

         

            // Set the colors used for each priority

            var priorities = [{"priority": 1, "color": "#FF6347"},

            {"priority": 2, "color": "#FFA500"},

            {"priority": 3, "color": "#FFD700"},

            {"priority": 4, "color": "#87CEFA"},

            {"priority": 5, "color": "#90EE90"}];

         

            // Set the scale for the x-axis

            var x = d3.scaleLinear()

            .range([0, width])

            .domain([d3.min(data, function(d) { return d.duration; }) - 2, d3.max(data, function(d) { return d.duration; }) + 2]);

         

            // Set the scale for the y-axis

            var y = d3.scaleLinear()

            .range([height, 0])

            .domain([d3.min(data, function(d) { return d.cost; }) - 10000, d3.max(data, function(d) { return d.cost; }) + 10000]);

         

            // Set the scale for the circle size

            var r = d3.scaleLinear()

            .domain([ 0, d3.max(data, function(d) {return d.effort;}) ]).range([ minCircle, maxCircle ]);

         

            var xAxis = d3.axisBottom().scale(x);

            var yAxis = d3.axisLeft().scale(y);

         

            // Create an svg canvas for our chart

            var svg = d3.select("#chart").append("svg")

            .attr("id", "chart-container")

            .attr("width", width + margin.left + margin.right)

            .attr("height", height + margin.top + margin.bottom)

            .append("g")

            .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

         

            // Create a circle for each of the project records

            function generateCircles() {

                        svg.selectAll(".dot")

                        .data(data)

                        .enter().append("circle")

                        .attr("class", function(d) { return "dot p" + d.priority; })

                        .attr("r", function(d) { return r(d.effort); })

                        .attr("cx", function(d) { return x(d.duration); })

                        .attr("cy", function(d) { return y(d.cost); })

                        .on("mouseover",function(d,i){

                                    this.parentNode.appendChild(this);

                                    d3.select("#tooltip").attr("transform", "translate("+(x(d.duration)+20)+","+(y(d.cost)+20)+")");

                                    d3.select("#duration").text("Duration: " + d.duration);

                                    d3.select("#cost").text("Cost: $" + d.cost);

                                    d3.select("#effort").text("Effort: " + d.effort + ' hours');

                        })

                        .on("mouseout",function(d,i){

                                    d3.selectAll(".tooltip-attribute").text("");

                        });

            }

         

            // Create the priority legend

            function generatePriorityLegend() {

                        var legend = svg.selectAll(".legend")

                        .data(priorities)

                        .enter().append("g")

                        .attr("class", "legend")

                        .attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });

                     

                        legend.append("rect")

                        .attr("x", width + 20)

                        .attr("width", 18)

                        .attr("height", 18)

                        .style("fill", function(d) { return d.color; });

                     

                        legend.append("text")

                        .attr("x", width + 14)

                        .attr("y", 9)

                        .attr("dy", ".35em")

                        .style("text-anchor", "end")

                        .text(function(d) { return d.priority; });

                     

                        var priorityLabel = d3.select("#chart-container")

                        .append("g")

                        .attr("transform", "translate(1130,55)")

                        .append("text")

                        .attr("transform", "rotate(-90)")

                        .attr("y", 6)

                        .attr("dy", ".71em")

                        .style("text-anchor", "end")

                        .text("Priority");

            }

         

            // Create initial tooltip

            function generateTooltip() {

                        var tooltip = svg.append("g")

                        .append("g")

                        .attr("id", "tooltip");

                     

                        tooltip.append("text")

                        .attr("id", "duration")

                        .attr("class", "tooltip-attribute");

                     

                        tooltip.append("text")

                        .attr("id", "cost")

                        .attr("y", 15)

                        .attr("class", "tooltip-attribute");

                     

                        tooltip.append("text")

                        .attr("id", "effort")

                        .attr("y", 30)

                        .attr("class", "tooltip-attribute");

            }

         

            // Add the x and y axes and their labels

            function generateAxes() {

                        svg.append("g")

                        .attr("class", "x axis")

                        .attr("transform", "translate(0," + height + ")")

                        .call(xAxis);

                     

                        svg.append("text")

                        .attr("x", width)

                        .attr("y", height - 6)

                        .style("text-anchor", "end")

                        .text("Duration (Days)");

                     

                        svg.append("g")

                        .attr("class", "y axis")

                        .call(yAxis);

                     

                        svg.append("text")

                        .attr("transform", "rotate(-90)")

                        .attr("y", 6)

                        .attr("dy", ".71em")

                        .style("text-anchor", "end")

                        .text("Cost ($)");

            }

         

            // Create the line of best fit

            function generateLine() {

                        var linReg =   linearRegression(data);

                     

                        var myLine = svg.append("svg:line")

                        .attr("x1", x(minDuration))

                        .attr("y1", y(minDuration*linReg.slope + linReg.intercept))

                        .attr("x2", x(maxDuration))

                        .attr("y2", y( (maxDuration*linReg.slope) + linReg.intercept ))

                        .style("stroke", "#A9A9A9");

                     

                        function linearRegression(projects){

                                    var lr = {};

                                    var n = projects.length;

                                    var sum_x = 0;

                                    var sum_y = 0;

                                    var sum_xy = 0;

                                    var sum_xx = 0;

                                    var sum_yy = 0;

                                 

                                    for (var i = 0; i < projects.length; i++) {

                                                sum_x += projects[i].duration;

                                                sum_y += projects[i].cost;

                                                sum_xy += (projects[i].duration*projects[i].cost);

                                                sum_xx += (projects[i].duration*projects[i].duration);

                                                sum_yy += (projects[i].cost*projects[i].cost);

                                    }

                                 

                                    lr.slope = (n * sum_xy - sum_x * sum_y) / (n*sum_xx - sum_x * sum_x);

                                    lr.intercept = (sum_y - lr.slope * sum_x)/n;

                                    lr.r2 = Math.pow((n*sum_xy - sum_x*sum_y)/

                                    Math.sqrt((n*sum_xx-sum_x*sum_x)*(n*sum_yy-sum_y*sum_y)),2);

                                 

                                    return lr;

                        }

            }

         

            generateCircles();

            generatePriorityLegend();

            generateTooltip();

            generateAxes();

            generateLine();

}

 

Server Script

 

(function() {

            var projData = [];

         

            // Grab projects in the IT porfolio

            var projGR = new GlideRecord('pm_project');

            projGR.addQuery('primary_portfolio', '30e14b3beb131100b749215df106fe0f');

            projGR.addEncodedQuery("resource_planned_cost>javascript:getCurrencyFilter('pm_project','resource_planned_cost', 'USD;0')^effort>javascript:gs.getDurationDate('0 0:0:0')");

            projGR.orderByDesc('start_date');

            projGR.query();

         

            // Push each project record into an array to pass to the client script

            while (projGR.next()) {

                        var dc = new DurationCalculator();

                        var hours = dc.calcScheduleDuration('1970-01-01 00:00:00', projGR.effort)/60/60;

                        var duration = dc.calcScheduleDuration('1970-01-01 00:00:00', projGR.duration)/60/60/24;

                        projData.push({

                                    "name": projGR.short_description+'',

                                    "cost": +projGR.cost,

                                    "duration": +duration,

                                    "effort": +hours,

                                    "priority": projGR.priority+''

                        });

            }

         

            data.projects = projData;

         

})();

 

Sources

 

For a full collection of my posts, visit my Community Blogs or http://mitchstutler.com/blog.

 

NOTE: MY POSTINGS REFLECT MY OWN VIEWS AND DO NOT NECESSARILY REPRESENT THE VIEWS OF MY EMPLOYER, ACCENTURE.

Filter Blog

By date: By tag: