Krisztian Kovacs
5 July 2018
Using Javascript in ScriptRunner for xMatters
How to use a custom dialog to build a JSON payload then convert our 'simple code' into something that can send more complex data to xMatters.
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:
- Use our custom dialog to build a JSON payload and make it interactive with various fields the user can change or complete
- 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:
1//-------------------------------------------------------------------------
2// This script can be applied as a rest-end-point that shows the dialog
3// when a user clicks on the 'send xmatters' button
4// Currently known button placement for web fragments:
5// - jira.issue.tools
6//-------------------------------------------------------------------------
7// Please change the following so the script would suit your needs:
8@Field static String myLittleSelectName = "My Little Select Field"
9@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:
1<div class="aui-dialog2-content">
2 <p>Header of the dialog, some text about warnings and whatever....</p>
3 Hi there fellow Worker,<br />
4 Allow me to present you this simple customfield, you can change to whatever before sending:<br />
5 <input id="" value="${getCustomFieldData(issueKey, costFieldName)}" ${
6 def cost = getCustomFieldData(issueKey, costFieldName) as Float
7 if (cost > 1000) return "disabled"
8 else return ""
9 }></input><br />
10 And another customfield, from a single-select input field:<br />
11 <input id="" value="${getCustomFieldData(issueKey, myLittleSelectName)}"></input><br />
12 The text surrounding the key fields won't be sent to xMatters, only the field values.
13 This is to make the communication more complex without sending any "junk".
14</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
1var payload = {
2 "subject" : "Urgent Message",
3 "body" : "",
4};
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.
There is a payload defined in the:
1. Dialog script (Rest EndPoint)
1var payload = {
2 "subject" : "Urgent Message",
3 "cost" : "",
4 "myLittleSelect" : "",
5 "comment" : ""
6};
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)
1body = """
2{
3 "properties": {
4 "subject" : "${content.subject}",
5 "cost": "${content.cost}",
6 "myLittleSelect": "${content.myLittleSelect}",
7 "comment": "${content.comment}"
8 },
9 "recipients": [
10 "kkovacs|Work Email"
11 ]
12}
13"""
3. xMatters Incoming Communication Configuration
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:
1static String errorHandling(String status) {
2 if (status == "401") return "There was a problem with the username or password. ($status)"
3 if (status == "403") return "The server has refused to fulfill the request. ($status)"
4 if (status == "404") return "The requested resource does not exist on the server. ($status)"
5 if (status == "408") return "The client failed to send a request in the time allowed by the server. ($status)"
6 if (status == "500") return "Due to a malfunctioning script, server configuration error or similar. ($status)"
7 else return "Unknown error: $status"
8}
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:
Inserting code into string
1<input id="" value="${getCustomFieldData(issueKey, costFieldName)}" ${
2 def cost = getCustomFieldData(issueKey, costFieldName) as Float
3 if (cost > 1000) return "disabled"
4 else return ""
5 }></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:
1<input id=test" value="random value"></input>
2<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).
1static String getCustomFieldData(String key, String customFieldName) {
2 Issue issue = ComponentAccessor.issueManager.getIssueByCurrentKey(key)
3 Collection<CustomField> customField = ComponentAccessor.getCustomFieldManager().getCustomFieldObjectsByName(customFieldName)
4 return issue.getCustomFieldValue(customField[0])
5}
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
1import com.atlassian.jira.component.ComponentAccessor
2import com.atlassian.jira.issue.Issue
3import com.atlassian.jira.issue.fields.CustomField
4import javax.ws.rs.core.MediaType
5import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
6import groovy.transform.BaseScript
7import javax.ws.rs.core.MultivaluedMap
8import javax.ws.rs.core.Response
9import groovy.transform.Field
10@BaseScript CustomEndpointDelegate delegate
11//-------------------------------------------------------------------------
12// This script can be applied as a rest-end-point that shows the dialog
13// when a user clicks on the 'send xmatters' button
14// Currently known button placement for web fragments:
15// - jira.issue.tools
16//-------------------------------------------------------------------------
17// Please change the following so the script would suit your needs:
18@Field static String myLittleSelectName = "My Little Select Field"
19@Field static String costFieldName = "Cost"
20//-------------------------------------------------------------------------
21static trimIssueKey(String key) {
22 key = key.replaceAll(/^\[|]$/, '')
23 return key
24}
25static getJiraBaseUrl() {
26 def baseUrl = ComponentAccessor.getApplicationProperties().getString("jira.baseurl")
27 return baseUrl
28}
29static String getCustomFieldData(String key, String customFieldName) {
30 Issue issue = ComponentAccessor.issueManager.getIssueByCurrentKey(key)
31 Collection<CustomField> customField = ComponentAccessor.getCustomFieldManager().getCustomFieldObjectsByName(customFieldName)
32 return issue.getCustomFieldValue(customField[0])
33}
34static String getDialogText(String issueKey) {
35 return """
36<script>
37 function submit() {
38 var payload = {
39 "subject" : "Urgent Message",
40 "cost" : "",
41 "myLittleSelect" : "",
42 "comment" : ""
43 };
44 payload.cost = document.getElementById("cost").value;
45 payload.myLittleSelect = document.getElementById("myLittleSelect").value;
46 var textarea = document.getElementById("sr-dialog-textarea").value;
47 var linebreak = textarea.split('\\n');
48 var length = linebreak.length;
49 var data = [];
50 for ( var i = 0 ; i<length ; i++) {
51 data = data + " " + linebreak[i];
52 }
53 payload.comment = data;
54
55 var xhttp = new XMLHttpRequest();
56 xhttp.open("POST", "${getJiraBaseUrl()}/rest/scriptrunner/latest/custom/xMattersRelay", false);
57 xhttp.setRequestHeader("Content-type", "application/json");
58 xhttp.send(JSON.stringify(payload));
59 AJS.dialog2("#sr-dialog").hide();
60 location.reload();
61 }
62 var el = document.getElementById("submit");
63 if (el.addEventListener)
64 el.addEventListener("click", submit, false);
65 else if (el.attachEvent)
66 el.attachEvent('onclick', submit);
67</script>
68<section role="dialog" id="sr-dialog"
69 class="aui-layer aui-dialog2 aui-dialog2-medium" aria-hidden="true" data-aui-remove-on-hide="true">
70<header class="aui-dialog2-header">
71 <h2 class="aui-dialog2-header-main">xMatters Message</h2>
72 <a class="aui-dialog2-header-close">
73 <span class="aui-icon aui-icon-small aui-iconfont-close-dialog">Close</span>
74 </a>
75</header>
76<div class="aui-dialog2-content">
77 <p>Header of the dialog, some text about warnings and whatever....</p>
78 Hi there fellow Worker,<br />
79 Allow me to present you this simple customfield, you can change to whatever before sending:<br />
80 <input id="cost" value="${getCustomFieldData(issueKey, costFieldName)}" ${
81 def cost = getCustomFieldData(issueKey, costFieldName) as Float
82 if (cost > 1000) return "disabled "
83 else return ""
84 }></input><br />
85 And another customfield, from a single-select input field:<br />
86 <input id="myLittleSelect" value="${getCustomFieldData(issueKey, myLittleSelectName)}"></input><br />
87 The text surrounding the key fields won't be sent to xMatters, only the field values.
88 This is to make the communication more complex without sending any "junk".<br />
89 Comment:<br />
90 <textarea id="sr-dialog-textarea" rows="15" cols="75"></textarea>
91</div>
92<footer class="aui-dialog2-footer">
93 <div class="aui-dialog2-footer-actions">
94 <button class="aui-button" id="submit">Send xMatters</button>
95 <button id="dialog-close-button" class="aui-button aui-button-link">Close</button>
96 </div>
97 <div class="aui-dialog2-footer-hint">This is a footer message</div>
98</footer>
99</section>
100"""
101}
102showXMattersDialog() {
103 MultivaluedMap queryParams ->
104 String issueKey = queryParams.get("issueKey")
105 issueKey = trimIssueKey(issueKey)
106 String dialog = getDialogText(issueKey)
107 Response.ok().type(MediaType.TEXT_HTML).entity(dialog).build()
108}
// xMattersRelay
1import groovy.transform.Field
2import com.onresolve.scriptrunner.runner.util.UserMessageUtil
3import groovy.json.JsonSlurper
4import groovyx.net.http.HTTPBuilder
5import groovyx.net.http.ContentType
6import static groovyx.net.http.Method.*
7@BaseScript CustomEndpointDelegate delegate
8import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
9import groovy.transform.BaseScript
10import javax.ws.rs.core.MultivaluedMap
11import javax.ws.rs.core.Response
12//-------------------------------------------------------------------------
13// Don't forget to change the variables
14//-------------------------------------------------------------------------
15@Field String xMattersURL =
16 "https://adaptavist-dev.xmatters.com/api/integration/1/functions/3eb43db8-e0a4-40ff-b1c8-452e89dd4691/triggers"
17@Field String xMattersUser = "SOMEUSERNAME"
18@Field String xMattersPassword = "SOMEPASSWORD"
19//-------------------------------------------------------------------------
20String issueKey = null
21static String errorHandling(String status) {
22 if (status == "401") return "There was a problem with the username or password. ($status)"
23 if (status == "403") return "The server has refused to fulfill the request. ($status)"
24 if (status == "404") return "The requested resource does not exist on the server. ($status)"
25 if (status == "408") return "The client failed to send a request in the time allowed by the server. ($status)"
26 if (status == "500") return "Due to a malfunctioning script, server configuration error or similar. ($status)"
27 else return "Unknown error: $status"
28}
29xMattersRelay(httpMethod: "POST") {
30 MultivaluedMap queryParams,
31 def payload ->
32 def jsonSlurper = new JsonSlurper()
33 def content = jsonSlurper.parseText(payload)
34 issueKey = content.key
35 def http = new HTTPBuilder(xMattersURL)
36 http.request(POST) {
37 requestContentType = ContentType.JSON
38 StatusRequestTimeout = 3
39 headers.'Authorization' =
40 "Basic ${"$xMattersUser:$xMattersPassword".bytes.encodeBase64().toString()}"
41 body = """
42 {
43 "properties": {
44 "subject" : "${content.subject}",
45 "cost": "${content.cost}",
46 "myLittleSelect": "${content.myLittleSelect}",
47 "comment": "${content.comment}"
48 },
49 "recipients": [
50 "kkovacs|Work Email"
51 ]
52 }
53 """
54 response.success = { resp, JSON ->
55 UserMessageUtil.success("xMatters sent. Weeeeee...")
56 return JSON
57 }
58 response.failure = { resp ->
59 String status = resp.status
60 UserMessageUtil.error(errorHandling(status))
61 return "Request failed with status ${resp.status}"
62 }
63 }
64 Response.ok(payload).build()
65
66}
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.
1xhttp.open("POST", "${getJiraBaseUrl()}/rest/scriptrunner/latest/custom/xMattersRelay", false);
2xhttp.setRequestHeader("Content-type", "application/json");
3xhttp.send(JSON.stringify(payload));
4alert(xhttp.responseText); <<<<<<<<<<<<<<<<<<<<<<<<<< this needs to go away.
5AJS.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.
1xhttp.open("POST", "${getJiraBaseUrl()}/rest/scriptrunner/latest/custom/xMattersRelay", false);
2xhttp.setRequestHeader("Content-type", "application/json");
3xhttp.send(JSON.stringify(payload));
4AJS.dialog2("#sr-dialog").hide();
5location.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.