Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Overview

Read our step-by-step guide to learn more about Report Builder's capabilities and how to create your scripted report from scratch.

Table of Contents

...

Introduction

Report Builder helps organizations with any Jira reporting need. It improves project management and helps to provide better project, team, and goal vision. The app has built-in personal and time reports templates, which cover most time reporting needs. With Scripted Reports, you will also build reports from scratch by implementing any calculation algorithm, parameter, and visualization library. Build your report with HTML and JavaScript basics.

...

Info

A comprehensive guide to Scripted Report API (aka SR API) is provided in this guide.

To manage the data provided, you can use JavaScript in the General tab. The code will run in the top-level await scope with the defined SR object. This object is constructed to help with tasks like interacting with Jira API, quickly getting data from the input fields of the General Tab, and passing the final data to the template.

Let's go over some of these methods:

  • SR.fields.getValueByFieldName(fieldName:String)  – returns a promise with the input field value based on fieldName string parameter. Please note that some pickers could be a multi-select field that returns an array of values.

  • SR.jira.getIssuesByJQL(jql:String, <fieldsString, <fields:String>, <expand:String>) – returns a promise with an array of Jira issues based on JQL query and the list of comma-separated fields. If the fields parameter is not set, then Jira Navigable fields will be returned

  • SR.render(templateValues:Object, <callback:Function>) – passes the values from the JavaScript to the template defined in the Template tab. It is possible to set some after rendering logic with the callback parameter

...

To get the data from the input fields with the help of SR.fields.getValueByFieldName() you need to pass the field name value that was set in the General tab.

Code Block
const jqlField = await SR.fields.getValueByFieldName('jql-field');
const { start, end } = await SR.fields.getValueByFieldName('range');

As the SR.jira.getIssuesByJQL() uses the string type for both parameters, you convert the selected fields to the comma-separated string. As the request to the Jira API is an asynchronous operation, you should be using top-level await to wait for a response.

Code Block
const jql = `created >= "${start}" and created <= "${end}"${jqlField && ` AND ${jqlField}`}`;
const foundIssues = await SR.jira.getIssuesByJQL(jql, ObjectFIELDS.valuesjoin(FIELDS','));

Now, fetch the Jira issues and control the final data object which is possible with the JavaScript map() method: 

Code Block
const issues = foundIssues.map(({ key, fields }) => {
    /* Get properties:
        key - issue key
        timespent - Time Spent
        timeoriginalestimate - Original estimate
        timeestimate - Remaining
    */
    const {
        timeestimate,
        timeoriginalestimate,
        timespent,
    } = fields;

    return {
        key,
        timespent: millisecondsToHours(timespent),
        timeestimate: millisecondsToHours(timeoriginalestimate),
        remaining: millisecondsToHours(timeestimate),
    };
});

...

Code Block
const millisecondsToHours = (milliseconds) => {
    if (isNaN(milliseconds)) {
        return 0;
    }

    return (Math.round(((milliseconds / 3600) + Number.EPSILON) * 100) / 100);
};

...

Here you can see the complete code for the Script tab:

Code Block
  /* 1. Define constants */
 
const FIELDS = {[
      timespent: 'timespent',
    
 timeoriginalestimate: 'timeoriginalestimate',
      timeestimate: 'timeestimate'
  };
  
 ];
/* 2. Define helper functions */

 const millisecondsToHours = (milliseconds) => {
      if (isNaN(milliseconds)) {
 
        return 0;
   
  }
       return (Math.round(((milliseconds / 3600) + Number.EPSILON) * 100) / 100);
 
};

 
/* 3. Getting parameters for the JQL request */
 
const jqlField = await SR.fields.getValueByFieldName('jql-field');
 
const { start, end } = await SR.fields.getValueByFieldName('range');

  /* 4. Requesting issues by JQL string and fields list */
  const jql = `created >= "${start}" and created <= "${end}"${jqlField && ` AND ${jqlField}`}`;
 
const foundIssues = await SR.jira.getIssuesByJQL(jql, Object.values(FIELDS));
  
  console.log.join('foundIssues,', foundIssues));

  /* 5. Parsing data from the response and creating the final object */

 const issues = foundIssues.map(({ key, fields }) => {
      /* 5.1 Get properties:
      key - issue key
key   - issue key projectKey - project key
      timespent - Time Spent
   
      timeoriginalestimate - Original estimate
  
       timeestimate - Remaining
      */
      const {
          timeestimate,
   */
  const {
    timeoriginalestimatetimeestimate,
    timeoriginalestimate,
     timespent,
      } = fields;

 
    /* 5.2 Issue work time vs original estimate logic  */
   
  return {
     
    key,
     
    timespent: millisecondsToHours(timespent),
          timeestimate: millisecondsToHours(timeoriginalestimate),

         remaining: millisecondsToHours(timeestimate),
  
   };
  });

 
/* 6. Passing values to the handlebars template engine */
 
SR.render({ issues });

Permission tab

...

You already know how to get the spent time in comparison to the estimated time for issues. But what if you want to get a report that will calculate the work time for a whole project, so multiple issues from multiple users? This can be achieved with minor changes in the report script.

First, you need to define the object that will store each project worklog as a key-value pair.add project field to your constant FIELDS:

Code Block
const projectsFIELDS = {};

...

 [
  'timespent',
  'timeoriginalestimate',
  'timeestimate',
  'project'
];

Next, you need to simple refactoring. Replace foundIssues.map() to foundIssues.reduce(), also add result.issues.push(issue)and return result to the end. Well, define the calculation logic of projects inside the foundIssues.reduce() like issue work time logic. Now, you should get a code like this:

Code Block
const { issues, projects } = foundIssues.map((reduce((result, { key, fields }) => {
    /* 5.1 Get properties:
    key - issue key
    projectKey - project key
 
      timespent - Time Spent
   
    timeoriginalestimate - Original estimate
   
    timeestimate - Remaining
 
  */
 
  const {
   
    project: {
            key: projectKey,

       },
   
    timeestimate,

       timeoriginalestimate,
   
    timespent,
 
  } = fields;

 
  /* 5.2 ProjectsIssue work time vs original estimate logic  */
  const  if (projects[projectKey])issue = {
    key,
   /* 5.3  If project key is already exist then recalculate time */
        projects[projectKey].timeestimate += timeoriginalestimate;
        projects[projectKey].timespent += timespent;
        projects[projectKey].remaining += timeestimate;

        projects[projectKey].timeestimateHours = millisecondsToHours(projects[projectKey].timeestimate);
        projects[projectKey].timespentHours = millisecondsToHours(projects[projectKey].timespent);
        projects[projectKey].remainingHours = millisecondsToHours(projects[projectKey].remaining);
    } else {
        /* 5.4  If project key is not exist then create object with initial time  */

        projects[projectKey] = {
      timespent: millisecondsToHours(timespent),
    timeestimate: millisecondsToHours(timeoriginalestimate),
    remaining: millisecondsToHours(timeestimate),
  };
  /* 5.3 Add current issue to result  */
  result.issues.push(issue);
  /* 5.4 Find a project of the current issue in the processed project */
  const foundProject = result.projects.find(({ key }) => key === projectKey);
  if (foundProject) {
    /* 5.5  If project key is already exist then recalculate time */
    foundProject.timeestimate += timeoriginalestimate;
    foundProject.timespent += timespent;
    foundProject.remaining += timeestimate;
    foundProject.timeestimateHours = millisecondsToHours(projects[projectKey].timeestimate);
    foundProject.timespentHours = millisecondsToHours(projects[projectKey].timespent);
    foundProject.remainingHours = millisecondsToHours(projects[projectKey].remaining);
  } else {
    const project = {
      key: projectKey,
 
          timespent,
            timeestimate: timeoriginalestimate,
      remaining: timeestimate,
      remainingtimeestimateHours: timeestimate,millisecondsToHours(timeoriginalestimate),
      timespentHours: millisecondsToHours(timespent),
       timeestimateHoursremainingHours: millisecondsToHours(timeoriginalestimatetimeestimate),
    };
    /* 5.6 Add timespentHours: millisecondsToHours(timespent),
   new project to result  */
    result.projects.push(project);
  }
remainingHours: millisecondsToHours(timeestimate), return result;
}, {
  issues: [],
};     }projects: []
});

Finally, pass the "projects" object as part of the rendering object and update the Template tab to show the data.

...