Last updated Apr 19, 2024

Writing a dashboard item app

Applicable:

Jira 7.0.0 and later.

Level of experience:

This is an intermediate tutorial. You should have completed at least one beginner tutorial before working through this tutorial. See the list of tutorials in DAC.

Time estimate:

It should take you approximately 1 hour to complete this tutorial.

Overview of the tutorial

This tutorial shows you how to write an app that adds a dashboard item to gadget directory, so users can install it to customize their dashboards.

The dashboard item will have a static content rendered on server and dynamic content rendered client-side. Using this app you can track issues that are about to due.

You can add dashboard item elements to Jira by adding module definitions to the app descriptor. In this tutorial, you'll add the web section module to define the dashboard item in your Jira.

Your completed app will consist of the following components:

  1. An app descriptor to enable the app in Jira.
  2. The required plugin modules that define the dashboard item.
  3. Soy templates to render dashboard item
  4. Context provider to pass variables to Soy template.
  5. JavaScript AMD module to load content dynamically.

About these instructions

You can use any supported combination of operating system and IDE to create this app. These instructions were written using IntelliJ IDEA 2017.3 on macOS Sierra. If you use another operating system or IDE combination, you should use the equivalent operations for your specific environment.

This tutorial was last tested with Jira 7.10.0 using the Atlassian SDK 6.3.10.

Before you begin

To complete this tutorial, you need to know the following: 

  1. The basics of Java development: classes, interfaces, methods, how to use the compiler, and so on.
  2. How to create an Atlassian plugin project using the Atlassian Plugin SDK.
  3. The basics of using and administering Jira.
  4. Be familiar with Soy templates and JavaScript.

App source

We encourage you to work through this tutorial. If you want to skip ahead or check your work when you are finished, you can find the app source code on Atlassian Bitbucket.

To clone the repository, run the following command:

1
2
git clone https://bitbucket.org/atlassian_tutorial/dashboard-item-tutorial.git

Alternatively, you can download the source as a ZIP archive

Step 1. Create the app project

In this step, you'll use an atlas- command to generate stub code for your app.

  1. Set up the Atlassian Plugin SDK and build a project if you did not do that yet.

  2. Open a Terminal and navigate to the directory where you would like to keep the project code.

  3. To create an app skeleton, run the following command:

    1
    2
    atlas-create-jira-plugin
    
  4. To identify your app, enter the following information.

    group-id

    com.example.plugins.tutorial

    artifact-id

    dashboard-item-tutorial

    version

    1.0-SNAPSHOT

    package

    com.example.plugins.tutorial

  5. Confirm your entries when prompted. The SDK generates the initial app project files in a directory named dashboard-item-tutorial.

  6. Navigate to the directory created in the previous step.

  7. Delete the test directories.

    Setting up testing for your app isn't part of this tutorial. To delete the generated test skeleton, run the following commands:

    1
    2
    rm -rf ./src/test/java
    rm -rf ./src/test/resources/
    
  8. Delete the unneeded Java class files.

    1
    2
    rm -rf ./src/main/java/com/example/plugins/tutorial/*
    
  9. Import project in your favorite IDE.

Step 2. Review and tweak the stub code

It is a good idea to familiarize yourself with the project configuration file known as the POM (that is, Project Object Model definition file).

In this step, you'll review and tweak the pom.xml file.

The POM is located at the root of your project and declares the project dependencies and other information.

  1. Navigate to the root folder of your app and open the pom.xml file.

  2. Add your company or organization name and your website URL to the organization element (the following code blocks show how it looks in plain text):

    1
    2
    <organization>
        <name>Example Company</name>
        <url>http://www.example.com/</url>
    </organization>
    
  3. To add a meaningful description for your app, update the project description element. For example:

    1
    2
    <description>This plugin adds company links to a new menu in the JIRA header.</description>
    
  4. Save the file.

  5. Your stub code contains an app descriptor file called atlassian-plugin.xml. This is an XML file that identifies the app to the host application (that is, Jira) and defines the required app functionality.

    Open the atlassian-plugin.xml file.

    You should see something like this (comments removed):  

1
2
<?xml version="1.0" encoding="UTF-8"?>
<atlassian-plugin key="${atlassian.plugin.key}" name="${project.name}" plugins-version="2">
    <plugin-info>
        <description>${project.description}</description>
        <version>${project.version}</version>
        <vendor name="${project.organization.name}" url="${project.organization.url}"/>
        <param name="plugin-icon">images/pluginIcon.png</param>
        <param name="plugin-logo">images/pluginLogo.png</param>
    </plugin-info>
    <resource type="i18n" name="i18n" location="dashboard-item-tutorial"/>
    <web-resource key="dcd ashboard-item-tutorial-resources" name="dashboard-item-tutorial Web Resources">
        <dependency>com.atlassian.auiplugin:ajs</dependency>
        <resource type="download" name="dashboard-item-tutorial.css" location="/css/dashboard-item-tutorial.css"/>
        <resource type="download" name="dashboard-item-tutorial.js" location="/js/dashboard-item-tutorial.js"/>
        <resource type="download" name="images/" location="/images"/>
        <context>dashboard-item-tutorial</context>
    </web-resource>
</atlassian-plugin>

Step 3. Create a dashboard item with server-side rendering

In this step, you'll create a dashboard item that will use context provider to pass value to Soy template.

  1. Define a new dashboard item in the app descriptor (that is, atlassian-plugin.xml) file:

    1
    2
    <dashboard-item key="new-dashboard-item"
            i18n-name-key="com.example.plugins.tutorial.dashboard.item.title">
        <definition>
            <title key="com.example.plugins.tutorial.dashboard.item.title"/>
            <categories>
                <category>Jira</category>
            </categories>
            <author>
                <name>Author's name</name>
            </author>
            <thumbnail location="/download/resources/${atlassian.plugin.key}:dashboard-item-tutorial-resources/images/pluginLogo.png"/>
        </definition>
        <description key="com.example.plugins.tutorial.dashboard.item.description"/>
        <resource name="view" type="soy" location=":dashboard-item-tutorial-resources/Dashboard.Item.Tutorial.Templates.Static"/>
        <context-provider class="com.example.plugins.tutorial.DashboardItemContextProvider"/>
    </dashboard-item>
    
    • A definition element is used to display a dashboard item in gadget directory (that is, where users search for gadgets).
    • You can reference pluginLogo.png as a thumbnail location, because SDK included it in your project and declared it in web-resource element.
    • dashboard-item-tutorial-resources is web resource's module key.
  2. To use Soy template to render the dashboard item, update web-resources element with the following:

    1
    2
    <web-resource key="dashboard-item-tutorial-resources" name="dashboard-item-tutorial Web Resources">
        <dependency>com.atlassian.auiplugin:ajs</dependency>
        <transformation extension="soy">
            <transformer key="soyTransformer"/>
        </transformation>
        <resource type="download" name="dashboard-item-tutorial.css" location="/css/dashboard-item-tutorial.css"/>
        <resource type="download" name="dashboard-item-tutorial.js" location="/js/dashboard-item-tutorial.js"/>
        <resource type="download" name="images/" location="/images"/>
        <resource type="download" location="soy/dashboard-item.soy" name="soy/dashboard-item.soy.js"/>
        <resource type="soy" location="/soy/dashboard-item.soy" name="Dashboard.Item.Tutorial.Templates.Static"/>
        <context>atl.dashboard</context>
        <context>dashboard-item-tutorial</context>
    </web-resource>
    

    Notice that we declared atl.dashboard, so resources will be included on dashboard page.

  3. Save the file.

Step 4. Create a Soy template

  1. Navigate to src/main/resources and create a soy directory.

  2. Create a dashboard-item.soy file.

  3. Add the following code to the Soy template:

    1
    2
    {namespace Dashboard.Item.Tutorial.Templates}
    
    /**
    * @param pluginName
    * @param version
    */
    {template .Static}
     <div>Welcome to <span class="aui-lozenge aui-lozenge-success">{$pluginName}</span> v{$version}!</div>
    {/template}
    
  4. Save the file.

Step 5. Add a context provider

In this step, to make the new dashboard item work, you will create a context provider to inject pluginName and version variables in Soy template.

  1. Navigate to src/main/java/com/example/plugins/tutorial and create a new Java class DashboardItemContextProvider.

  2. Add the following code to it:

    1
    2
    package com.example.plugins.tutorial;
    
    import com.atlassian.plugin.Plugin;
    import com.atlassian.plugin.PluginAccessor;
    import com.atlassian.plugin.PluginInformation;
    import com.atlassian.plugin.PluginParseException;
    import com.atlassian.plugin.spring.scanner.annotation.component.Scanned;
    import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
    import com.atlassian.plugin.web.ContextProvider;
    import com.google.common.collect.Maps;
    
    import java.util.Map;
    
    @Scanned
    public class DashboardItemContextProvider implements ContextProvider {
        private final PluginAccessor pluginAccessor;
    
        public DashboardItemContextProvider(
                @ComponentImport PluginAccessor pluginAccessor) {
            this.pluginAccessor = pluginAccessor;
        }
    
        @Override
        public void init(final Map<String, String> params) throws PluginParseException {
        }
    
        @Override
        public Map<String, Object> getContextMap(final Map<String, Object> context) {
            final Map<String, Object> newContext = Maps.newHashMap(context);
            Plugin plugin = pluginAccessor.getEnabledPlugin("com.example.plugins.tutorial.dashboard-item-tutorial");
            newContext.put("version", plugin.getPluginInformation().getVersion());
            newContext.put("pluginName", plugin.getName());
            return newContext;
        }
    }
    

    We used Atlassian Spring Scanner to inject PluginAccessor from host application.

  3. Since we declared i18n keys in app descriptor, it's time to update dashboard-item-tutorial.properties element in the atlassian-plugin.xml file.

    1
    2
    com.example.plugins.tutorial.dashboard.item.title=Dashboard tutorial
    com.example.plugins.tutorial.dashboard.item.description=Writing a dashboard item app tutorial
    

Step 6. Test your dashboard item

  1. In Terminal window, run the following the SDK command:

    1
    2
    atlas-run
    

    This command starts a Jira instance and loads your app. In the output, look for a line that looks something like this:

    1
    2
    [INFO] jira started successfully in 134s at http://localhost:2990/jira
    

    It tells you that Jira instance has been started and shows you the Jira home page URL.

  2. In a browser, go to Jira home page that is indicated in the Terminal output. 

  3. Log in using the default admin/admin username and password combination.

  4. Create a new project as prompted. Jira prompts you to create a new project only when you start a new instance.

  5. At the top right of the page, click Dashboard > Manage dashboards

  6. Create a new dashboard and click Add gadget button. Here is an example of how your gadget should look like.

  7. After you add dashboard item to dashboard you will see it in work.

The app works, but so far it doesn't do much. You will enhance it in the next steps.

Leave Jira running in browser for now. 

Step 7. Load content dynamically

So far you have created a simple app that renders a dashboard item server-side.

Dashboard item API provides a way to include JavaScript using AMD modules.

In this step, you will use JavaScript to make REST call and search for issues that are about to due.

  1. Open the atlassian-plugin.xml file and add the following inside dashboard-item element:

    1
    2
    <amd-module>dashboard-items/tutorial</amd-module>
    
  2. Navigate to src/main/resources/soy and open the dashboard-item.soy file.

  3. Create a div element for dynamic content and add templates — first to render issues, second to handle empty list.

    1
    2
    {namespace Dashboard.Item.Tutorial.Templates}
    
    /**
    * @param pluginName
    * @param version
    */
    {template .Static}
     <div id="dynamic-content"/>
     <div>Welcome to <span class="aui-lozenge aui-lozenge-success">{$pluginName}</span> v{$version}!</div>
    {/template}
    
    /**
    * @param issues
    */
    {template .IssueList}
       <table class="aui">
           <thead>
               <tr>
                   <th id="basic-icon"></th>
                   <th id="basic-fname">Issue key</th>
                   <th id="basic-lname">Due date</th>
               </tr>
           </thead>
                  <tbody>
           {foreach $issue in $issues}
                        <tr>
                            <td headers="basic-number"><img src="{$issue.fields.issuetype.iconUrl}"/></td>
                            <td headers="basic-fname">{$issue.key}</td>
                            <td headers="basic-lname">{$issue.fields.duedate}</td>
                        </tr>
           {/foreach}
           </tbody>
       </table>
       <div class="buttons-container">
           <div class="buttons">
               <input class="button submit" type="submit" value="Refresh">
           </div>
       </div>
    {/template}
    /**
    *   Use this template if there is no issues in response
    */
    {template .Empty}
       No issues yet.
       <div class="buttons-container">
              <div class="buttons">
                  <input class="button submit" type="submit" value="Refresh">
              </div>
          </div>
    {/template}
    
  4. Navigate to src/main/resources/js/ and open the dashboard-item-tutorial.js file.

  5. To define AMD module, add the following JavaScript code to the file:

    1
    2
    define('dashboard-items/tutorial', ['underscore', 'jquery', 'wrm/context-path'], function (_, $, contextPath) {
        var DashboardItem = function (API) {
            this.API = API;
            this.issues = [];
        };
        /**
        * Called to render the view for a fully configured dashboard item.
        *
        * @param context The surrounding <div/> context that this items should render into.
        * @param preferences The user preferences saved for this dashboard item (e.g. filter id, number of results...)
        */
        DashboardItem.prototype.render = function (context, preferences) {
            this.API.showLoadingBar();
            var $element = this.$element = $(context).find("#dynamic-content");
            var self = this;
            this.requestData().done(function (data) {
                self.API.hideLoadingBar();
                self.issues = data.issues;
                if (self.issues === undefined || self.issues.length  === 0) {
                    $element.empty().html(Dashboard.Item.Tutorial.Templates.Empty());
                }
                else {
                    $element.empty().html(Dashboard.Item.Tutorial.Templates.IssueList({issues: self.issues}));
                }
                self.API.resize();
                $element.find(".submit").click(function (event) {
                    event.preventDefault();
                    self.render(element, preferences);
                });
            });
    
            this.API.once("afterRender", this.API.resize);
        };
    
        DashboardItem.prototype.requestData = function () {
            return $.ajax({
                method: "GET",
                url: contextPath() + "/rest/api/2/search?jql=due<=3d"
            });
        };
    
        return DashboardItem;
    });
    

    In this snippet, we make a REST call to get issues that are due in 3 days (this is hardcoded for now) and render different templates depending on response.

  6. Save the changes.

Step 8. Test dynamic loading

  1. To test updates, rebuild your app using QuickReload:

    1
    2
    atlas-package
    
  2. Go to the local Jira instance that you left running before.

  3. Disable caching in browser and refresh dashboard page. Your dashboard item should look like this.

  4. Create few issues with due date in less than 3 days and click Refresh.

Step 9. Add a configuration screen and test the app

In this step, you will create a configuration screen for dashboard item that will be used to pass due date field in REST call.

  1. Open the atlassian-plugin.xml file, find dashboard-item definition, and add configurable attribute:

    1
    2
    <dashboard-item key="new-dashboard-item"
               i18n-name-key="com.example.plugins.tutorial.dashboard.item.title"
               configurable="true">
    
  2. Open the dashboard-item.soy file. To create another Soy template to render configuration form, add following to the file:

    1
    2
    /**
    * configuration
    */
    {template .Configuration}
       <form class="aui">
           <div class="field-group">
               <label for="due-date-input">Due Date
                   <span class="aui-icon icon-required">(required)</span></label>
               <input class="text medium-field" type="text"
                      id="due-date-input" name="due-date-input" value="3d">
               <div class="description">Syntax: 3(d|w|m) </div>
           </div>
           <div class="buttons-container">
               <div class="buttons">
                   <input class="button submit" type="submit" value="Save" id="comment-save-button">
                   <a class="cancel" href="#">Cancel</a>
               </div>
           </div>
       </form>
    {/template}
    
  3. Open the dashboard-item-tutorial.js file. Because we added configurable attribute, dashboard calls renderEdit method if dashboard item is not configured yet.

    Add the following renderEdit method to the file:

    1
    2
    DashboardItem.prototype.renderEdit = function (context, preferences) {
            var $element = this.$element = $(context).find("#dynamic-content");
            $element.empty().html(Dashboard.Item.Tutorial.Templates.Configuration());
            this.API.once("afterRender", this.API.resize);
            var $form = $("form", $element);
            $(".cancel", $form).click(_.bind(function() {
                if(preferences['due-date-input'])
                    this.API.closeEdit();
            }, this));
    
            $form.submit(_.bind(function(event) {
                event.preventDefault();
    
                var preferences = getPreferencesFromForm($form);
                var regexp = /^\d+([dwm])$/;
                if(regexp.test(preferences['due-date-input'])) {
                    this.API.savePreferences(preferences);
                    this.API.showLoadingBar();
                }
            }, this));
        };
    
        function getPreferencesFromForm($form) {
            var preferencesArray = $form.serializeArray();
            var preferencesObject = {};
    
            preferencesArray.forEach(function(element) {
                preferencesObject[element.name] = element.value;
            });
    
            return preferencesObject;
        }
    
  4. Update render and requestData methods to work with preferences:

    1
    2
    DashboardItem.prototype.render = function (context, preferences) {
         this.API.showLoadingBar();
         var $element = this.$element = $(context).find("#dynamic-content");
         var self = this;
         this.requestData(preferences).done(function (data) {
             self.API.hideLoadingBar();
             self.issues = data.issues;
             if (self.issues === undefined || self.issues.length  === 0) {
                 $element.empty().html(Dashboard.Item.Tutorial.Templates.Empty());
             }
             else {
                 $element.empty().html(Dashboard.Item.Tutorial.Templates.IssueList({issues: self.issues}));
             }
             self.API.resize();
             $element.find(".submit").click(function (event) {
                 event.preventDefault();
                 self.render(context, preferences);
             });
         });
    
         this.API.once("afterRender", this.API.resize);
     };
    
    DashboardItem.prototype.requestData = function (preferences) {
        return $.ajax({
            method: "GET",
            url: contextPath() + "/rest/api/2/search?maxResults=10&jql=due<=" + preferences['due-date-input']
        });
    };
    
  5. Save the changes.

  6. Rebuild your app and test changes, your configuration screen should look like this:

Congratulations, that's it!

Have a treat!

Next steps

Your app JAR file is located at target\dashboard-item-tutorial-1.0-SNAPSHOT.jar.

You can install the app using the Universal Plugin Manager

Rate this page: