Visualize Your Jira Query Language Filters

Overview

We use JQL (Jira Query Language) frequently to query Jira for a specific set of issues based on dates, status, keywords, and the like. In addition to getting the results in Jira, we thought it would be nice if you could also visualize your results.

Unlike the other scripts included in VisualScript that hide the underlying query mechanisms, this script relies on the user to enter a fully formed query. When the issues are returned, they are depicted in an interlinked hierarchy as epics, stories, issues and sub-tasks. However, only the issues that match the filter are displayed. For example, if a subtask is selected by the filter, but its parent task or epic are not, then the subtask is shown as a child of the filter query itself.

Jira filter visualization

The user interface is built to handle the full JQL query to select the issues we want to include in our visualization. This gives total freedom to anyone already familiar with JQL to create a custom visual quickly.

The query below selects any issue whose summary field contains the string "market".

Jira filter summary field

How I Implemented It

Obtaining the Data

In this particular script, when we query Jira for data, we're going to pass in the parameter string "as is" from the value entered by the user into the UI.

function VisualScript(localhost, params, callback) { var epics = {}, stories = {}, subTasks = {}; VSCallFunctionForJiraQuery(gSDJSLocalHost, params, handleEpics);

Building the Model

The next step is to build a model using the information returned by Jira. I scan the list of returned issues and build a list of epics, stories and sub-tasks.

// Build the model. Scan through the issues, bulding the list of epics, stories and subTasks. Create VisualScript shapes for the epics now. issuesLen = issues.length; for (i = 0; i < issuesLen; i++) { switch (issues[i].fields.issuetype.name) { case "Epic": epics[issues[i].key] = { issue: issues[i] }; break; case "Story": stories[issues[i].key] = { issue: issues[i] }; break; case "Sub-task": subTasks[issues[i].key] = { issue: issues[i] }; } }

Next, I need some extra information that's not available from the query. I need to know the name of the custom field that this Jira installation is using for "epic link". Unlike keyname and summary information that I use that is a "standard" part of the Jira API results set for an issue, the epic is implemented as a custom field. I need to issue an API call on a single issue while passing it the "expand=names" parameter. This will cause Jira to add additional information to the results about the use of custom fields. To implement this, I find the first issue in the returned issues list, then issue the API request.

// Use the first issueto find the custom field that's used for epic links, then process the stories and subtasks using that information VSCallFunctionForJiraIssue(gSDJSLocalHost, issues[0].key "?expand=names", buildVisualScript);

The information is passed as a parameter to the function buildVisualScript.

Building VisualScript

Now that I have all the information I need in my model, I can create my VSON markup. After setting up my document, I set up the root shape, which naturally contains the query parameters. Beneath the main shape, I add a connector to connect all the shapes that matched the query. Epics are linked to stories and subtasks as long as they matched the query. Any orphan results are linked directly to the query parameters without their corresponding epics.

vs = new VS.Document(); vs.SetTemplate(VS.Templates.Orgchart); rootShape = vs.GetTheShape() .SetLabel("Filter:\n" + params); rootConnector = rootShape.AddShapeConnector(); // Add a shape for each epic for (var epicKey in epics) { epics[epicKey].shape = rootConnector.AddShape() .SetLabel(epics[epicKey].issue.fields.summary) .SetFillColor(colors.epic) .SetLineThickness(0) .SetHyperlink(browseUrl + epics[epicKey].issue.key); } // Now add shapes for all the stories. for (var storyKey in stories) { target = rootConnector; // Default to the root connector // Add to the epic (if there) or to the root shape if there is no epic if (epicLinkField && stories[storyKey].issue.fields[epicLinkField]) { targetEpicKey = stories[storyKey].issue.fields[epicLinkField]; if (epics[targetEpicKey]) { if (!epics[targetEpicKey].shapeConnector) { // Shape connector is not there.... add it epics[targetEpicKey].shapeConnector = epics[targetEpicKey].shape.AddShapeConnector(); } target = epics[targetEpicKey].shapeConnector; } } // Add a new shape for this story to the target's first/only shape connector stories[storyKey].shape = target.AddShape() .SetLabel(stories[storyKey].issue.fields.summary) .SetShapeType(VS.ShapeTypes.Rect) .SetFillColor(colors.story) .SetTextTruncate(60) .SetLineThickness(0) .SetHyperlink(browseUrl + storyKey); } // Now add the subtasks for (var subTaskKey in subTasks) { var parentStoryID = subTasks[subTaskKey].issue.fields.parent.key; if (stories[parentStoryID]) { if (!stories[parentStoryID].shapeConnector) { stories[parentStoryID].shapeConnector = stories[parentStoryID].shape.AddShapeConnector(); } target = stories[parentStoryID].shapeConnector; } else { target = rootConnector; } if (target) { subTasks[subTaskKey].shape = target.AddShape() .SetLabel(subTasks[subTaskKey].issue.fields.summary) .SetShapeType("Rect") .SetFillColor(colors.subtask) .SetTextTruncate(60) .SetLineThickness(0) .SetHyperlink(browseUrl + subTaskKey); } }

Rendering the VisualScript

The last step is to generate the visual using by passing the VisualScript object to the rendering engine using callback.

// Render the VisualScript
callback(vs.toJSON());

Creating the UI

After the script is complete, I set up the UI I mentioned in the beginning, so that the user could provide the query for the visualization. In the top section of the VisualScript edit page, you'll see a field labeled "Parameter Template". The specification string for the UI Builder's "Parameter Template" is:

Parameter template

This sets up a single field that will be passed to the script.

When I enter the above string is entered into the UI Builder's Parameter Template, this form is displayed:

New string

I then override the "Displayed Name" to be a little more of a friendly string and add a brief description:

New string

More Information

This is just one example of the useful visuals that can be produced using VisualScript with a minimum of development effort.

The full source for this script can be seen here. To make the code easier to understand, I highlighted the part used for querying the data in green, the part for building the model in orange, and the part that creates VisualScript from the data in blue.

function VisualScript(localhost, params, callback) { var epics = {}, stories = {}, subTasks = {}; // Shows a display of all epics, stories and subtasks for a query. Please refer to https://www.visualscript.com/learn/visualscript-sdk/ for information on use of VisualScript VSCallFunctionForJiraQuery(gSDJSLocalHost, params, handleEpics); function handleEpics(issues) { var i, issuesLen; debugger; if (!issues || issues.length == 0) { alert((!params || params.length == 0) ? "Please enter a query" : "No issues found for query: " + params); callback(null); return; }
// Build the model. Scan through the issues, building the list of epics, stories and subTasks. Create VisualScript shapes for the epics now. issuesLen = issues.length; for (i = 0; i < issuesLen; i++) { switch (issues[i].fields.issuetype.name) { case "Epic": epics[issues[i].key] = { issue: issues[i] }; break; case "Story": stories[issues[i].key] = { issue: issues[i] }; break; case "Sub-task": subTasks[issues[i].key] = { issue: issues[i] }; } } // Use the first issue to find the custom field that's used for epic links, then process the stories and subtasks using that information VSCallFunctionForJiraIssue(gSDJSLocalHost, issues[0].key + "?expand=names", buildVisualScript); }
// Build VisualScript from the model function buildVisualScript(firstStoryInfo) { var vs = {}, rootShape, rootConnector, targetEpicKey; var colors = { epic: "#ACE2FC", story: "#91AECF", subtask: "#FADE9E" }; var browseUrl = localhost + "/browse/"; var epicLinkField = getEpicLinkField(firstStoryInfo); vs = new VS.Document(); vs.SetTemplate(VS.Templates.Orgchart); rootShape = vs.GetTheShape() .SetLabel("Filter:\n" + params); rootConnector = rootShape.AddShapeConnector(); // Add a shape for each epic for (var epicKey in epics) { epics[epicKey].shape = rootConnector.AddShape() .SetLabel(epics[epicKey].issue.fields.summary) .SetFillColor(colors.epic) .SetLineThickness(0) .SetHyperlink(browseUrl + epics[epicKey].issue.key); } // Now add shapes for all the stories. for (var storyKey in stories) { target = rootConnector; // Default to the root connector // Add to the epic (if there) or to the root shape if there is no epic if (epicLinkField && stories[storyKey].issue.fields[epicLinkField]) { targetEpicKey = stories[storyKey].issue.fields[epicLinkField]; if (epics[targetEpicKey]) { if (!epics[targetEpicKey].shapeConnector) { // Shape connector is not there.... add it epics[targetEpicKey].shapeConnector = epics[targetEpicKey].shape.AddShapeConnector(); } target = epics[targetEpicKey].shapeConnector; } } // Add a new shape for this story to the target's first/only shape connector stories[storyKey].shape = target.AddShape() .SetLabel(stories[storyKey].issue.fields.summary) .SetShapeType(VS.ShapeTypes.Rect) .SetFillColor(colors.story) .SetTextTruncate(60) .SetLineThickness(0) .SetHyperlink(browseUrl + storyKey); } // Now add the subtasks for (var subTaskKey in subTasks) { var parentStoryID = subTasks[subTaskKey].issue.fields.parent.key; if (stories[parentStoryID]) { if (!stories[parentStoryID].shapeConnector) { stories[parentStoryID].shapeConnector = stories[parentStoryID].shape.AddShapeConnector(); } target = stories[parentStoryID].shapeConnector; } else { target = rootConnector; } if (target) { subTasks[subTaskKey].shape = target.AddShape() .SetLabel(subTasks[subTaskKey].issue.fields.summary) .SetShapeType("Rect") .SetFillColor(colors.subtask) .SetTextTruncate(60) .SetLineThickness(0) .SetHyperlink(browseUrl + subTaskKey); } } // Render the VisualScript callback(vs.toJSON()); } // Get the field name for the epic of a story function getEpicLinkField(firstStoryInfo) { // Find the custom field used to contain the epic link for (var field in firstStoryInfo.names) { if (firstStoryInfo.names[field] == "Epic Link") { return field; } } } }