JAVASCRIPT
July 05, 2018

Using Javascript in ScriptRunner (Gotta Script'em All 5/6)

KK
Krisztian Kovacs 14 minute read

Welcome back to the fifth post in the Gotta Script-em All series where we explore the capabilities of Jira through a step-by-step guide to creating seamless integration links between Jira and other tools.  Make sure you check out the previous four posts in this series if you haven't yet.

In part 1 we explored how to create a custom button and rest endpoint on your Jira user interface.  In part 2 we go back to basics with a guide to writing clean code. In part 3 we resume our quest for achieving custom user interface (UI) goodness using ScriptRunner and get deeper into the nitty-gritty of manipulating custom fields in Jira. In part 4 we used the newly created dialog box  to send data to XMatters.

In this post (part 5) we aim to do two things:

  1. Use our custom dialog to build a JSON payload and make it interactive with various fields the user can change or complete
  2. Then we convert our 'simple code' into something that can send more complex data to xMatters

While this all may seem complicated, after following our previous four blogs, hopefully, you have now built up enough knowledge and confidence with ScriptRunner and coding that it will make it easier to follow what's going on.

When you have a working script that is tested and delivered, there is only one question to ask: why change it? Why would anyone change the behaviour of a script? Is it for improvement? Is it because you can? Is it because times change? The simple answer is all of the above.

There is no such thing as a 'finished' script. There is always room for improvement. They are always released before they can become perfect. Actually, that's the lie we tell ourselves: there was no time to make it perfect.

We have the opportunity to 'upgrade' our xMatters script, break it (yes you read that correctly!) and then build it right back up again.

The problem with modifying scripts is that you will undoubtedly run into the dreaded "change one line, break everything" problem. So we are going to take a different approach and instead we will present multiple input fields instead of one big 'body' field. Doing it this way is going to be brilliant for various reasons:

  • It's more readable
  • It sends more details to xMatters (not just one big message)
  • It can be modified easily
  • Individual fields can be disabled
    • if "cost" is too high: disable
    • if "cost" is below 1000, let the user change it

However there are some scary disadvantages to consider:

  • We have to rewrite multiple parts of the script
  • We need a more complex rest API submit method
  • We will also have to modify the relay script 
  • And we also need to do some work with xMatters on the receiving end

Luckily, I've already gone through this nightmare (in reality it's not that bad). The most challenging part was designing the methods. And I guess I also had to spend a lot of time testing, rewriting, retesting, redesigning, rewriting, retesting... nevermind. Here are the results.

Maintaining your script

This script is harder than usual to maintain. Normally I'd put everything that needs to be changed in the beginning of the script where the instructions are:

// Code box

copy

Copied!

 

//-------------------------------------------------------------------------
// This script can be applied as a rest-end-point that shows the dialog
// when a user clicks on the 'send xmatters' button
// Currently known button placement for web fragments:
// - jira.issue.tools
//-------------------------------------------------------------------------
// Please change the following so the script would suit your needs:
@Field static String myLittleSelectName = "My Little Select Field"
@Field static String costFieldName = "Cost"

However this time that's not enough. The person maintaining the script needs to go into the belly of the beast, into the middle of the 'getDialogText' to manipulate the text.

Unfortunately this is the case with most complex scripts. There comes a point beyond which we can't put every single configuration item at the beginning of the script. Let me show you why:

// Code box

copy

Copied!

 

<div class="aui-dialog2-content">
    <p>Header of the dialog, some text about warnings and whatever....</p>
        Hi there fellow Worker,<br />
        Allow me to present you this simple customfield, you can change to whatever before sending:<br />
        <input id="" value="${getCustomFieldData(issueKey, costFieldName)}" ${
            def cost = getCustomFieldData(issueKey, costFieldName) as Float
            if (cost > 1000) return "disabled"
            else return ""
        }></input><br />
        And another customfield, from a single-select input field:<br />
        <input id="" value="${getCustomFieldData(issueKey, myLittleSelectName)}"></input><br />
        The text surrounding the key fields won't be sent to xMatters, only the field values.
        This is to make the communication more complex without sending any "junk".
</div>

In the above example, there is a whole scripted part in the middle. You could outsource to a method, but that would complicate things a lot more. The script design makes it easy for the client to add more and more custom fields to their dialog.

Therefore maintenance instructions are as follows:

  • Add new custom field to the beginning of script
  • Add extra input field to the dialog
  • Add extra field to the submit function
  • Add extra field to the relay script

Changing the payload

// Code box

copy

Copied!

 

var payload = {
    "subject" : "Urgent Message",
    "body" : "",
};

We now have a payload (information or message in the transmitted data), and we have to change it.  However, you have to remember that when you change the payload (increase the number of items sent over), you have to replace these items in two other places: the relay script and in the xMatters configuration.

Screenshot 2018 06 27 14.32.27

There is a payload defined in the:

  1. Dialog script (Rest EndPoint)

    // Code box

    copy

    Copied!

     

    var payload = {
        "subject" : "Urgent Message",
        "cost" : "",
        "myLittleSelect" : "",
        "comment" : ""
    };

Pro Tip: don't ever forget to add extra commas when you add new lines (items) to the payload. It's always hard to find these pesky little errors.

2.  Relay Script (Rest EndPoint)

// Code box

copy

Copied!

 

body = """
{
  "properties": {
    "subject" : "${content.subject}",
    "cost": "${content.cost}",
    "myLittleSelect": "${content.myLittleSelect}",
    "comment": "${content.comment}"
  },
  "recipients": [
    "kkovacs|Work Email"
  ]
}
"""

3.  xMatters Incoming Communication Configuration

image2018 5 29 13 19 15

Don't forget, the new payload must be changed in all three places.

Handling errors

When we send the payload to xMatters there are several responses we can get that come as standard with RestAPI. I created a method to convert these "numbers" to something more humanly readable:

// Code box

copy

Copied!

 

static String errorHandling(String status) {
    if (status == "401") return "There was a problem with the username or password. ($status)"
    if (status == "403") return "The server has refused to fulfill the request. ($status)"
    if (status == "404") return "The requested resource does not exist on the server. ($status)"
    if (status == "408") return "The client failed to send a request in the time allowed by the server. ($status)"
    if (status == "500") return "Due to a malfunctioning script, server configuration error or similar. ($status)"
    else return "Unknown error: $status"
}

Now, when the user sends that fateful xMatters message, they can get some real errors out of it, for instance if the password is wrong in the script:

image2018 5 29 14 43 11

Inserting code into string

// Code box

copy

Copied!

 

<input id="" value="${getCustomFieldData(issueKey, costFieldName)}" ${
            def cost = getCustomFieldData(issueKey, costFieldName) as Float
            if (cost > 1000) return "disabled"
            else return ""
        }></input>

There are benefits to inserting simple code into a very long string. It should be simple though, nothing fancy. Here I have to work with a long HTML code, and I need an 'if statement,' which is a bit more than a simple variable insert but a lot less than a whole complex method.

Furthermore, when the client takes over and wishes to add more fields to the screen, it's a lot simpler to 'clone' this 'if statement.' Disabling the input field is simple on the front end:

// Code box

copy

Copied!

 

<input id=test" value="random value"></input>
<input id=test" value="random value" disabled></input>

All you have to do is put 'disabled' inside the tag, and that's exactly what the fancy code above does: it asks for the custom field value, puts it the field then asks for it again to examine whether it's above 1000 or not. If it's above 1000 the script writes 'disabled' inside the tag. If it's not the case then just return with a "" (nothing).

Data from custom fields

There are two ways to identify a custom field before getting the value from it.

  • Using the custom field ID
  • Using the name of the custom field

From experience I always prefer using the custom field ID. It's a bit harder to find it because you have to look in the URL, like this:

https://MYJIRA/secure/admin/ConfigureCustomField!default.jspa?customFieldId=10401

There it is, the ID is 10401.

This time I chose the other method as it's a bit easier for the client (I imagine it was requested by the them, otherwise I'd definitely go with the ID).

// Code box

copy

Copied!

 

static String getCustomFieldData(String key, String customFieldName) {
    Issue issue = ComponentAccessor.issueManager.getIssueByCurrentKey(key)
    Collection<CustomField> customField = ComponentAccessor.getCustomFieldManager().getCustomFieldObjectsByName(customFieldName)
    return issue.getCustomFieldValue(customField[0])
}

Custom fields in JIRA must have a unique ID. If you use that to find the custom field, you will only get one object back: the custom field itself. However custom fields can have the same name (though I wouldn't recommend it), and nothing stops you from renaming them every day.

When you look up a custom field by name, JIRA gives you a list of all the custom fields found (even if it's only one). I chose to deal with the first custom field with the particular name "customField[0]" in the collection.

In my experience, using the custom field by name can make the script less "foolproof," and this is the advice I give any client who prefers to use names instead of IDs in scripts.

The code

Now all the puzzle pieces are together. Admittedly I could and would love to discuss what's going on in this piece of code for ages but all blog posts must come to an end at some point. Luckily the next part of this series is coming soon. Until then, please enjoy the code.

Mind you, there is a tiny change I made that I haven't talked about yet, so don't be afraid to scroll down to the 'bugfix' section to feast your eyes on a little problem solving exercise.

// showXMattersdialog 

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.fields.CustomField
import javax.ws.rs.core.MediaType
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.transform.BaseScript
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response
import groovy.transform.Field
@BaseScript CustomEndpointDelegate delegate
//-------------------------------------------------------------------------
// This script can be applied as a rest-end-point that shows the dialog
// when a user clicks on the 'send xmatters' button
// Currently known button placement for web fragments:
// - jira.issue.tools
//-------------------------------------------------------------------------
// Please change the following so the script would suit your needs:
@Field static String myLittleSelectName = "My Little Select Field"
@Field static String costFieldName = "Cost"
//-------------------------------------------------------------------------
static trimIssueKey(String key) {
    key = key.replaceAll(/^\[|]$/, '')
    return key
}
static getJiraBaseUrl() {
    def baseUrl = ComponentAccessor.getApplicationProperties().getString("jira.baseurl")
    return baseUrl
}
static String getCustomFieldData(String key, String customFieldName) {
    Issue issue = ComponentAccessor.issueManager.getIssueByCurrentKey(key)
    Collection<CustomField> customField = ComponentAccessor.getCustomFieldManager().getCustomFieldObjectsByName(customFieldName)
    return issue.getCustomFieldValue(customField[0])
}
static String getDialogText(String issueKey) {
    return """
<script>
    function submit() {
        var payload = {
            "subject" : "Urgent Message",
            "cost" : "",
            "myLittleSelect" : "",
            "comment" : ""
        };
        payload.cost = document.getElementById("cost").value;
        payload.myLittleSelect = document.getElementById("myLittleSelect").value;
        var textarea = document.getElementById("sr-dialog-textarea").value;
        var linebreak = textarea.split('\\n');
        var length = linebreak.length;
        var data = [];
        for ( var i = 0 ; i<length ; i++) {
            data = data + " " + linebreak[i];
        }
        payload.comment = data;
  
        var xhttp = new XMLHttpRequest();
        xhttp.open("POST", "${getJiraBaseUrl()}/rest/scriptrunner/latest/custom/xMattersRelay", false);
        xhttp.setRequestHeader("Content-type", "application/json");
        xhttp.send(JSON.stringify(payload));
        AJS.dialog2("#sr-dialog").hide();
        location.reload();
    }
    var el = document.getElementById("submit");
    if (el.addEventListener)
        el.addEventListener("click", submit, false);
    else if (el.attachEvent)
        el.attachEvent('onclick', submit);
</script>
<section role="dialog" id="sr-dialog"
    class="aui-layer aui-dialog2 aui-dialog2-medium" aria-hidden="true" data-aui-remove-on-hide="true">
<header class="aui-dialog2-header">
    <h2 class="aui-dialog2-header-main">xMatters Message</h2>
    <a class="aui-dialog2-header-close">
        <span class="aui-icon aui-icon-small aui-iconfont-close-dialog">Close</span>
    </a>
</header>
<div class="aui-dialog2-content">
    <p>Header of the dialog, some text about warnings and whatever....</p>
        Hi there fellow Worker,<br />
        Allow me to present you this simple customfield, you can change to whatever before sending:<br />
        <input id="cost" value="${getCustomFieldData(issueKey, costFieldName)}" ${
            def cost = getCustomFieldData(issueKey, costFieldName) as Float
            if (cost > 1000) return "disabled "
            else return ""
        }></input><br />
        And another customfield, from a single-select input field:<br />
        <input id="myLittleSelect" value="${getCustomFieldData(issueKey, myLittleSelectName)}"></input><br />
        The text surrounding the key fields won't be sent to xMatters, only the field values.
        This is to make the communication more complex without sending any "junk".<br />
        Comment:<br />
        <textarea id="sr-dialog-textarea" rows="15" cols="75"></textarea>
</div>
<footer class="aui-dialog2-footer">
    <div class="aui-dialog2-footer-actions">
        <button class="aui-button" id="submit">Send xMatters</button>
        <button id="dialog-close-button" class="aui-button aui-button-link">Close</button>
    </div>
    <div class="aui-dialog2-footer-hint">This is a footer message</div>
</footer>
</section>
"""
}
showXMattersDialog() {
    MultivaluedMap queryParams ->
        String issueKey = queryParams.get("issueKey")
        issueKey = trimIssueKey(issueKey)
        String dialog = getDialogText(issueKey)
        Response.ok().type(MediaType.TEXT_HTML).entity(dialog).build()
}

// xMattersRelay 

import groovy.transform.Field
import com.onresolve.scriptrunner.runner.util.UserMessageUtil
import groovy.json.JsonSlurper
import groovyx.net.http.HTTPBuilder
import groovyx.net.http.ContentType
import static groovyx.net.http.Method.*
@BaseScript CustomEndpointDelegate delegate
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.transform.BaseScript
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response
//-------------------------------------------------------------------------
// Don't forget to change the variables
//-------------------------------------------------------------------------
@Field String xMattersURL =
@Field String xMattersUser = "SOMEUSERNAME"
@Field String xMattersPassword = "SOMEPASSWORD"
//-------------------------------------------------------------------------
String issueKey = null
static String errorHandling(String status) {
    if (status == "401") return "There was a problem with the username or password. ($status)"
    if (status == "403") return "The server has refused to fulfill the request. ($status)"
    if (status == "404") return "The requested resource does not exist on the server. ($status)"
    if (status == "408") return "The client failed to send a request in the time allowed by the server. ($status)"
    if (status == "500") return "Due to a malfunctioning script, server configuration error or similar. ($status)"
    else return "Unknown error: $status"
}
xMattersRelay(httpMethod: "POST") {
    MultivaluedMap queryParams,
    def payload ->
        def jsonSlurper = new JsonSlurper()
        def content = jsonSlurper.parseText(payload)
        issueKey = content.key
        def http = new HTTPBuilder(xMattersURL)
        http.request(POST) {
            requestContentType = ContentType.JSON
            StatusRequestTimeout = 3
            headers.'Authorization' =
                    "Basic ${"$xMattersUser:$xMattersPassword".bytes.encodeBase64().toString()}"
            body = """
            {
              "properties": {
                "subject" : "${content.subject}",
                "cost": "${content.cost}",
                "myLittleSelect": "${content.myLittleSelect}",
                "comment": "${content.comment}"
              },
              "recipients": [
                "kkovacs|Work Email"
              ]
            }
            """
            response.success = { resp, JSON ->
                UserMessageUtil.success("xMatters sent. Weeeeee...")
                return JSON
            }
            response.failure = { resp ->
                String status = resp.status
                UserMessageUtil.error(errorHandling(status))
                return "Request failed with status ${resp.status}"
            }
        }
        Response.ok(payload).build()
 
}

The "Bugfix"

If you want to be great at writing scripts, first you need to learn the lingo. Wait, shouldn't people first learn how to write scripts? Nah, that's for amateurs.

Well, using the right term in the proper context is crucial. Some technical terms might seem strange at first, but you soon get used to it. Every script has 'bugs.' A bug is not a syntax error (i.e. forgetting to include a quotation mark) but something that allows the script to function in the wrong way.

In our case, we have one actual bug in our script, which is the following: "When the user clicks on the submit button, there is an alert message popping up, and they have to manually refresh the page to get to the success or error message."

Let's get down to fixing this bug. The alert message was quite helpful during testing, and it's not a real bug but something that was left in there by accident.

// Code box

copy

Copied!

 

xhttp.open("POST", "${getJiraBaseUrl()}/rest/scriptrunner/latest/custom/xMattersRelay", false);
xhttp.setRequestHeader("Content-type", "application/json");
xhttp.send(JSON.stringify(payload));
alert(xhttp.responseText); <<<<<<<<<<<<<<<<<<<<<<<<<< this needs to go away.
AJS.dialog2("#sr-dialog").hide();

Removing it will not solve the manual page refresh but at least it's something.

Unfortunately we can't do anything about the refresh. What we can do is to automatically refresh the page when the user clicks on the submit button.

// Code box

copy

Copied!

 

xhttp.open("POST", "${getJiraBaseUrl()}/rest/scriptrunner/latest/custom/xMattersRelay", false);
xhttp.setRequestHeader("Content-type", "application/json");
xhttp.send(JSON.stringify(payload));
AJS.dialog2("#sr-dialog").hide();
location.reload(); <<<<<<<<<<<<<<<<<<<<< this refreshes the page automatically.

Slow down for success

We've learned a valuable lesson through this post, modifying an existing script is both easy and challenging in equal measure. Since the script already exists you don't have to create it from scratch but every single piece you modify will probably "upset the balance" and will result in you doing extensive testing and bugfixing. This is especially true if you work with someone else's scripts.

My advice is to go slowly, modify your script one tiny piece at a time and then you won't run into big problems. Also, don't forget to remember all the other places your script is connected to. In our case it's the xMatters configuration.

Join us again for part 6,  where we'll take a look at how RestAPI works and what happens when you want to connect to other 3rd party applications and not xMatters.