Last updated Mar 5, 2025

Creating custom reports for Assets

Level of experience:

Advanced. You should have completed at least one intermediate tutorial before working through this tutorial.

Time estimate:

It should take you approximately two hours to complete this tutorial.

Applicable:

Jira Service Management 5.12 and later.

This tutorial shows how to create a new report type to display payroll information. This custom report will accept parameters from the user, output clearly defined data, and generate a report based on that data.

The final report will look like this:

Final custom report in an Asset app

Before you begin

To get the most out of this tutorial, you should know the following:

After creating a sample project, implement the Assets Widget Framework in your app. This framework allows you to use the exposed report interfaces. The widget framework consists of three parts:

  • Widget parameters: These are the input fields used to generate the report.
  • Widget data: This represents the output data that will be consumed by the front-end renderers.
  • Widget module: This acts as the engine that generates the report.

Step 1: Implement the widget parameters

Widget parameters are the input fields used to generate the report.

Widget parameters in an Asset app

First, implement the WidgetParameters class.


See the WidgetParameters class
1
2
```
package com.riadalabs.jira.plugins.insight.reports.payroll;

import com.google.common.collect.Lists;
import io.riada.jira.plugins.insight.widget.api.WidgetParameters;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

/**
* This is what is sent from the frontend as expected inputs
*/
public class PayrollReportParameters implements WidgetParameters {

    private Value schema;
    private Value objectType;

    private Value numAttribute;
    private Value dateAttributeStartDate;
    private Value dateAttributeEndDate;

    private String period;
    private LocalDateTime startDate;
    private LocalDateTime endDate;

    private String iql;

    public PayrollReportParameters() {
    }

    public Value getSchema() {
        return schema;
    }

    public void setSchema(Value schema) {
        this.schema = schema;
    }

    public Value getObjectType() {
        return objectType;
    }

    public void setObjectType(Value objectType) {
        this.objectType = objectType;
    }

    public Value getNumAttribute() {
        return numAttribute;
    }

    public void setNumAttribute(Value numAttribute) {
        this.numAttribute = numAttribute;
    }

    public Value getDateAttributeStartDate() {
        return dateAttributeStartDate;
    }

    public void setDateAttributeStartDate(Value dateAttributeStartDate) {
        this.dateAttributeStartDate = dateAttributeStartDate;
    }

    public Value getDateAttributeEndDate() {
        return dateAttributeEndDate;
    }

    public void setDateAttributeEndDate(Value dateAttributeEndDate) {
        this.dateAttributeEndDate = dateAttributeEndDate;
    }

    public String getPeriod() {
        return period;
    }

    public Period period() {

        return Period.from(getPeriod());
    }

    public void setPeriod(String period) {
        this.period = period;
    }

    public LocalDateTime getStartDate() {
        return startDate;
    }

    public void setStartDate(LocalDateTime startDate) {
        this.startDate = startDate;
    }

    public LocalDateTime getEndDate() {
        return endDate;
    }

    public void setEndDate(LocalDateTime endDate) {
        this.endDate = endDate;
    }

    public String getIql() {
        return iql;
    }

    public void setIql(String iql) {
        this.iql = iql;
    }

    public LocalDate determineStartDate(Boolean doesWeekBeginOnMonday) {
        return this.period() == Period.CUSTOM ? getStartDate().toLocalDate()
                : this.period().from(LocalDate.now(), doesWeekBeginOnMonday);
    }

    public LocalDate determineEndDate(Boolean doesWeekBeginOnMonday) {
        final LocalDate today = LocalDate.now();

        return this.period() == Period.CUSTOM ? Period.findEarliest(getEndDate().toLocalDate(), today)
                : this.period().to(today, doesWeekBeginOnMonday);
    }

    public List<Value> getDateAttributes() {
        return Lists.newArrayList(getDateAttributeStartDate(), getDateAttributeEndDate());
    }

    public List<Value> getNumericAttributes() {
        return Lists.newArrayList(getNumAttribute());
    }
}
```

Next, implement the insight-widget module type parameters element in the atlassian-plugin.xml plugin descriptor. This will ensure that the parameters are displayed on-screen.


See the insight-widget module
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>

    <!-- add our i18n resource -->
    <resource type="i18n" name="i18n" location="insight-report-payroll"/>

    <!-- resources to be imported by reports iframe -->
    <web-resource key="insight-report-payroll" i18n-name-key="Payroll Report Resource">
        <resource type="download" name="insight-report-payroll.css"
                location="/css/insight-report-payroll.css"/>
        <resource type="download" name="insight-report-payroll.js"
                location="/js/insight-report-payroll.js"/>
        <resource type="download" name="chart.js"
                location="/js/lib/chart.js"/>
        <context>insight-report-payroll</context>
    </web-resource>

    <!-- implement insight-widget module type -->
    <insight-widget key="insight.example.report.payroll"
                    class="com.riadalabs.jira.plugins.insight.reports.payroll.PayrollReport"
                    category="report"
                    web-resource-key="insight-report-payroll"
                    name="insight.example.report.payroll.name"
                    description="insight.example.report.payroll.description"
                    icon="diagram"
                    background-color="#26a9ba">
        <!-- Map and display data -->
        <renderers>
            <renderer mapper="BarChart.Mapper"
                    view="BarChart.View"
                    label="insight.example.report.view.chart.bar"/>
            <renderer mapper="AreaChart.Mapper"
                    view="AreaChart.View"
                    label="insight.example.report.view.chart.area"
                    selected="true"/>
        </renderers>
        <!-- Export data to file -->
        <exporters>
            <exporter transformer="Transformer.JSON"
                    extension="json"
                    label="insight.example.report.exporter.json"/>
        </exporters>
        <!-- Parameters to show up in report form -->
        <parameters>
            <parameter key="period"
                    label="insight.example.report.period"
                    type="switch"
                    required="true"
                    default="CURRENT_WEEK">
                <configuration>
                    <options>
                        <option label="insight.example.report.period.current.week" value="CURRENT_WEEK"/>
                        <option label="insight.example.report.period.last.week" value="LAST_WEEK"/>
                        <option label="insight.example.report.period.current.month" value="CURRENT_MONTH"/>
                        <option label="insight.example.report.period.last.month" value="LAST_MONTH"/>
                        <option label="insight.example.report.period.current.year" value="CURRENT_YEAR"/>
                        <option label="insight.example.report.period.last.year" value="LAST_YEAR"/>
                        <option label="insight.example.report.period.custom" value="CUSTOM"/>
                    </options>
                </configuration>
            </parameter>
            <parameter key="startDate"
                    type="datepicker"
                    label="insight.example.report.period.custom.start"
                    required="true">
                <configuration>
                    <dependency key="period">
                        <value>CUSTOM</value>
                    </dependency>
                </configuration>
            </parameter>
            <parameter key="endDate"
                    type="datepicker"
                    label="insight.example.report.period.custom.end"
                    required="true">
                <configuration>
                    <dependency key="period">
                        <value>CUSTOM</value>
                    </dependency>
                </configuration>
            </parameter>
            <parameter key="schema"
                    type="schemapicker"
                    label="insight.example.report.schema"
                    required="true">
            </parameter>
            <parameter key="objectType"
                    type="simpleobjecttypepicker"
                    label="insight.example.report.objecttype"
                    required="true">
                <configuration>
                    <dependency key="schema"/>
                </configuration>
            </parameter>
            <parameter key="numAttribute"
                    type="objecttypeattributepicker"
                    label="insight.example.report.attribute.numeric"
                    required="true">
                <configuration>
                    <dependency key="objectType"/>
                    <filters>
                        <value>INTEGER</value>
                        <value>DOUBLE</value>
                    </filters>
                </configuration>
            </parameter>
            <parameter key="dateAttributeStartDate"
                    type="objecttypeattributepicker"
                    label="insight.example.report.attribute.date.start"
                    required="true">
                <configuration>
                    <dependency key="objectType"/>
                    <filters>
                        <value>DATE</value>
                        <value>DATE_TIME</value>
                    </filters>
                </configuration>
            </parameter>
            <parameter key="dateAttributeEndDate"
                    type="objecttypeattributepicker"
                    label="insight.example.report.attribute.date.end"
                    required="true">
                <configuration>
                    <dependency key="objectType"/>
                    <filters>
                        <value>DATE</value>
                        <value>DATE_TIME</value>
                    </filters>
                </configuration>
            </parameter>
            <parameter key="iql"
                    type="iql"
                    label="insight.example.report.iql">
                <configuration>
                    <dependency key="schema"/>
                </configuration>
            </parameter>
        </parameters>
    </insight-widget>
</atlassian-plugin>
```

Step 2: Implement the widget data

The widget data defines how the report will be used by the front-end renderers. To implement the widget data, use the WidgetData class.


See the WidgetData class
1
2
```
package com.riadalabs.jira.plugins.insight.reports.payroll;

import com.fasterxml.jackson.annotation.JsonInclude;
import io.riada.jira.plugins.insight.widget.api.WidgetData;
import io.riada.jira.plugins.insight.widget.api.WidgetMetadata;

import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY;

/**
* This is what is sent to the frontend as wrapper for the generated reports output data
*/
public class PayrollReportData implements WidgetData {

    private final Map<LocalDate, List<Expenditure>> expendituresByDay;
    private final boolean hasData;
    private final boolean useIso8601FirstDayOfWeek;

    public static PayrollReportData empty() {
        return new PayrollReportData(Collections.EMPTY_MAP, false, false);
    }

    public PayrollReportData(Map<LocalDate, List<Expenditure>> expendituresByDay,
            boolean hasData,
            boolean useIso8601FirstDayOfWeek) {
        this.expendituresByDay = expendituresByDay;
        this.hasData = hasData;
        this.useIso8601FirstDayOfWeek = useIso8601FirstDayOfWeek;
    }

    @Override
    public boolean hasData() {
        return this.hasData;
    }

    @Override
    public WidgetMetadata getMetadata() {

        WidgetMetadata metadata = new WidgetMetadata(hasData(), getNotice());
        metadata.addOption("useIso8601FirstDayOfWeek", useIso8601FirstDayOfWeek);

        return metadata;
    }

    @JsonInclude (NON_EMPTY)
    public Map<LocalDate, List<Expenditure>> getExpendituresByDay() {
        return expendituresByDay;
    }
}
```

Step 3: Implement the widget module

The widget module generates the report. To implement the widget module, use the WidgetModule and GeneratingDataByIQLCapability classes.


See the WidgetModule class
1
2
```
package com.riadalabs.jira.plugins.insight.reports.payroll;

import com.atlassian.jira.config.properties.APKeys;
import com.atlassian.jira.config.properties.ApplicationProperties;
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
import com.google.common.collect.Maps;
import com.riadalabs.jira.plugins.insight.channel.external.api.facade.ObjectSchemaFacade;
import com.riadalabs.jira.plugins.insight.channel.external.api.facade.ObjectTypeAttributeFacade;
import com.riadalabs.jira.plugins.insight.channel.external.api.facade.ObjectTypeFacade;
import com.riadalabs.jira.plugins.insight.reports.payroll.builder.IQLBuilder;
import com.riadalabs.jira.plugins.insight.reports.payroll.builder.ReportDataBuilder;
import com.riadalabs.jira.plugins.insight.reports.payroll.validator.PayrollReportValidator;
import com.riadalabs.jira.plugins.insight.services.model.ObjectAttributeBean;
import com.riadalabs.jira.plugins.insight.services.model.ObjectBean;
import com.riadalabs.jira.plugins.insight.services.model.ObjectTypeAttributeBean;
import com.riadalabs.jira.plugins.insight.services.model.ObjectTypeBean;
import com.riadalabs.jira.plugins.insight.services.progress.model.ProgressId;
import io.riada.core.service.model.ServiceError;
import io.riada.core.service.Reason;
import io.riada.core.service.ServiceException;
import io.riada.jira.plugins.insight.widget.api.WidgetModule;
import io.riada.jira.plugins.insight.widget.api.capability.GeneratingDataByIQLCapability;
import org.jetbrains.annotations.NotNull;

import javax.annotation.Nonnull;
import javax.inject.Inject;
import javax.inject.Named;
import java.time.LocalDate;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;

@Named
public class PayrollReport implements WidgetModule<PayrollReportParameters>,
        GeneratingDataByIQLCapability<PayrollReportParameters, PayrollReportData> {

    private final ObjectSchemaFacade objectSchemaFacade;
    private final ObjectTypeFacade objectTypeFacade;
    private final ObjectTypeAttributeFacade objectTypeAttributeFacade;
    private final ApplicationProperties applicationProperties;

    @Inject
    public PayrollReport(@ComponentImport final ObjectSchemaFacade objectSchemaFacade,
                        @ComponentImport final ObjectTypeFacade objectTypeFacade,
                        @ComponentImport final ObjectTypeAttributeFacade objectTypeAttributeFacade,
                        @ComponentImport final ApplicationProperties applicationProperties) {
        this.objectSchemaFacade = objectSchemaFacade;
        this.objectTypeFacade = objectTypeFacade;
        this.objectTypeAttributeFacade = objectTypeAttributeFacade;
        this.applicationProperties = applicationProperties;
    }

    @Override
    public void validate(@NotNull PayrollReportParameters parameters) throws Exception {
        Set<ServiceError> validationErrors = PayrollReportValidator.validate(parameters, objectSchemaFacade,
                objectTypeAttributeFacade);

        if (!validationErrors.isEmpty()) {
            throw new ServiceException(validationErrors, Reason.VALIDATION_FAILED);
        }
    }

    @NotNull
    @Override
    public String buildIQL(@Nonnull PayrollReportParameters parameters) throws Exception {
        final IQLBuilder iqlBuilder = new IQLBuilder(objectTypeAttributeFacade);

        final Integer objectTypeId = parameters.getObjectType().getValue();
        final ObjectTypeBean objectTypeBean = objectTypeFacade.loadObjectTypeBean(objectTypeId);

        iqlBuilder.addObjectType(objectTypeBean)
                .addDateAttributes(parameters.getDateAttributes(), parameters)
                .addNumericAttributes(parameters.getNumericAttributes())
                .addCustomIQL(parameters.getIql());

        return iqlBuilder.build();
    }

    @NotNull
    @Override
    public PayrollReportData generate(@NotNull PayrollReportParameters parameters, List<ObjectBean> objects,
            @NotNull ProgressId progressId) {

        if (objects.isEmpty()) {
            return PayrollReportData.empty();
        }

        final boolean doesWeekBeginOnMonday = applicationProperties.getOption(APKeys.JIRA_DATE_TIME_PICKER_USE_ISO8601);

        final LocalDate startDate = parameters.determineStartDate(doesWeekBeginOnMonday);
        final LocalDate endDate = parameters.determineEndDate(doesWeekBeginOnMonday);

        final Map<Integer, String> numericAttributeNames = createAttributeIdToNameMap(parameters.getNumericAttributes());
        final LinkedHashMap<Integer, ObjectAttributeBean> dateAttributesMap = createEmptyObjectTypeAttributeIdMap(parameters.getDateAttributes());

        final ReportDataBuilder reportDataBuilder =
                new ReportDataBuilder(startDate, endDate, doesWeekBeginOnMonday, numericAttributeNames,
                        dateAttributesMap);

        return reportDataBuilder.fillData(objects)
                .build();
    }

    private Map<Integer, String> createAttributeIdToNameMap(List<Value> attributes) {
        return attributes.stream()
                .map(Value::getValue)
                .map(id -> uncheckCall(() -> objectTypeAttributeFacade.loadObjectTypeAttributeBean(id)))
                .collect(Collectors.toMap(ObjectTypeAttributeBean::getId, ObjectTypeAttributeBean::getName));
    }

    private LinkedHashMap<Integer, ObjectAttributeBean> createEmptyObjectTypeAttributeIdMap(List<Value> attributes) {
        final LinkedHashMap emptyValuedMap = Maps.newLinkedHashMap();

        attributes.stream()
                .map(Value::getValue)
                .forEach(id -> emptyValuedMap.put(id, null));

        return emptyValuedMap;
    }

    private <T> T uncheckCall(Callable<T> callable) {
        try {
            return callable.call();
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
```

The following are the currently exposed components used for interfacing with Assets:

  • ObjectSchemaFacade
  • ObjectTypeFacade
  • ObjectTypeAttributeFacade
  • ObjectFacade
  • ConfigureFacade
  • IqlFacade
  • ObjectAttributeBeanFactory
  • ImportSourceConfigurationFacade
  • InsightPermissionFacade
  • InsightGroovyFacade
  • ProgressFacade

Step 4: Customize the widget framework

If you look at the example above, you'll notice three areas where customization is possible within the widget framework:

  • Validation
  • Query building
  • Report generation

Let's explore each of these areas in detail.

Validating the parameters

Ensure that the widget parameters are correctly created. In the following example, you can verify that the object attribute types match the actual object attributes. In this case, if an Employee has a salary attribute expected to be numeric but is now textual, an exception will be thrown.

1
2
public void validate(@NotNull WidgetParameters parameters) throws Exception

Building the AQL query

You can also build an query using the widget parameters. This query will be used to fetch the objects. For example:

1
2
public String buildIQL(@NotNull WidgetParameters parameters) throws Exception

Generating the report

Finally, you can generate the widget data from the returned objects. In this example, the ProgressId corresponds to the progress of the current report job. Use the ProgressFacade to extract any pertinent information.

1
2
public WidgetData generate(@NotNull WidgetParameters parameters, List<ObjectBean> objects, @NotNull ProgressId progressId)

Step 5: Add the widget module to the descriptor

Now that you’ve implemented classes for WidgetParameters, WidgetData, and WidgetModule, it's time to modify the descriptor to register your custom report widget as an app within Assets.

You need to modify the ModuleType, define renderers and exporters, and specify mapper and view functions to create a fully functioning report. Ensure that all label names are unique.

  1. Specify the ModuleType. This will point to the WidgetModule.

    1
    2
    <insight-widget class="com.riadalabs.jira.plugins.insight.reports.payroll.PayrollReport"
    
  2. Define renderers. The graphical display is rendered within an iFrame.

    1
    2
    <renderers>
        <renderer mapper="BarChart.Mapper"
                  view="BarChart.View"
                  label="Bar Chart"/>
    </renderers>
    

    The Bar Chart includes JavaScript components that transform and display the data generated by the backend. Here's an example:


    See the JavaScript example
    1
    2
    var BarChart = {};
    
    BarChart.Mapper = function (data, parameters, baseUrl) {
    
        var mapper = new PayrollMapper(data, parameters);
    
        var expenditureIn = {
            label: "IN",
            data: [],
            backgroundColor: 'rgba(255,57,57,0.5)',
            borderColor: 'rgb(255,57,57)',
            borderWidth: 1
        };
    
        var expenditureOut = {
            label: "OUT",
            data: [],
            backgroundColor: 'rgba(45,218,181,0.5)',
            borderColor: 'rgb(45,218,181)',
            borderWidth: 1
        };
    
        var expenditureTotal = {
            label: "TOTAL",
            data: [],
            backgroundColor: 'rgba(111,158,255,0.5)',
            borderColor: 'rgb(111,158,255)',
            borderWidth: 1
        };
    
        return mapper.asTimeSeries(expenditureIn, expenditureOut, expenditureTotal);
    
    };
    
    BarChart.View = function (mappedData) {
    
        var containingElement = document.querySelector('.js-riada-widget');
        if (!containingElement) return;
    
        var canvas = new Canvas("myChart");
    
        var canvasElement = canvas.appendTo(containingElement);
    
        var myChart = new Chart(canvasElement, {
            type: 'bar',
            data: mappedData,
            options: {
                scales: {
                    xAxes: [{
                        ticks: {
                            beginAtZero: true
                        },
                        stacked: true
                    }]
                },
                animation: {
                    duration: 0
                },
                responsive: true,
                maintainAspectRatio: false
            }
        });
    
        return containingElement;
    };
    
    var AreaChart = {};
    
    AreaChart.Mapper = function (data, parameters, baseUrl) {
    
        var mapper = new PayrollMapper(data, parameters);
    
        var expenditureIn = {
            label: "IN",
            data: [],
            backgroundColor: 'rgba(255,57,57,0.5)',
            steppedLine: true,
            pointRadius: 2,
        };
    
        var expenditureOut = {
            label: "OUT",
            data: [],
            backgroundColor: 'rgba(45,218,181,0.5)',
            steppedLine: true,
            pointRadius: 2
        };
    
        var expenditureTotal = {
            label: "TOTAL",
            data: [],
            backgroundColor: 'rgba(111,158,255, 0.5)',
            steppedLine: true,
            pointRadius: 2
        };
    
        return mapper.asTimeSeries(expenditureIn, expenditureOut, expenditureTotal);
    
    };
    
    AreaChart.View = function (mappedData) {
    
        var containingElement = document.querySelector('.js-riada-widget');
        if (!containingElement) return;
    
        var canvas = new Canvas("myChart");
    
        var canvasElement = canvas.appendTo(containingElement);
    
        var myChart = new Chart(canvasElement, {
            type: 'line',
            data: mappedData,
            options: {
                scales: {
                    yAxes: [{
                        ticks: {
                            beginAtZero: true
                        }
                    }]
                },
                elements: {
                    line: {
                        tension: 0
                    }
                },
                animation: {
                    duration: 0
                },
                responsive: true,
                maintainAspectRatio: false
            }
        });
    
        return containingElement;
    };
    
    var Canvas = function (id) {
        this.id = id;
    
        this.appendTo = function (containingElement) {
    
            clearOldIfExists(containingElement);
    
            var canvasElement = document.createElement("canvas");
            canvasElement.id = this.id;
    
            containingElement.appendChild(canvasElement);
    
            return canvasElement;
        };
    
        function clearOldIfExists(containingElement) {
            var oldCanvas = containingElement.querySelector('#myChart');
            if (oldCanvas) oldCanvas.remove();
        }
    };
    
    var PayrollMapper = function (data, parameters) {
        this.data = data;
        this.parameters = parameters;
    
        var EXPENDITURE_IN = "IN";
        var EXPENDITURE_OUT = "OUT";
        var EXPENDITURE_TOTAL = "TOTAL";
    
        this.asTimeSeries = function (dataIn, dataOut, dataTotal) {
            var mappedData = {};
    
            if (!this.data.metadata.hasData || this.parameters.numAttribute == null) {
                return mappedData;
            }
    
            mappedData.labels = Object.keys(data.expendituresByDay);
            mappedData.datasets = [];
    
            var attributeMap = createAttributeMap(this.parameters, dataIn, dataOut, dataTotal);
    
            Object.entries(data.expendituresByDay).forEach(function (entry, index) {
    
                var expenditures = entry[1];
    
                if (expenditures === undefined || expenditures.length === 0) {
    
                    Object.entries(attributeMap).forEach(function (entry) {
                        var expenditure = entry[1];
    
                        fillData(expenditure, EXPENDITURE_IN, 0.0);
                        fillData(expenditure, EXPENDITURE_OUT, 0.0);
    
                        var previousTotal = index === 0 ? 0.0 : expenditure[EXPENDITURE_TOTAL].data[index - 1];
                        fillData(expenditure, EXPENDITURE_TOTAL, previousTotal);
                    });
    
                }
    
                expenditures.forEach(function (expenditure) {
                    if (attributeMap.hasOwnProperty(expenditure.name)) {
                        fillData(attributeMap[expenditure.name], EXPENDITURE_IN, expenditure.typeValueMap[EXPENDITURE_IN]);
                        fillData(attributeMap[expenditure.name], EXPENDITURE_OUT, 0.0 - expenditure.typeValueMap[EXPENDITURE_OUT]);
    
                        var currentTotal = expenditure.typeValueMap[EXPENDITURE_IN] - expenditure.typeValueMap[EXPENDITURE_OUT];
                        var previousTotal = index === 0 ? 0.0 : attributeMap[expenditure.name][EXPENDITURE_TOTAL].data[index - 1];
                        fillData(attributeMap[expenditure.name], EXPENDITURE_TOTAL, currentTotal + previousTotal)
                    }
                });
    
            });
    
            mappedData.datasets = flatMap(attributeMap);
    
            return mappedData;
    
        };
    
        var createAttributeMap = function (parameters, dataIn, dataOut, dataTotal) {
            var map = {};
    
            map[parameters.numAttribute.label] = {};
    
            dataIn.label = parameters.numAttribute.label + "-" + dataIn.label;
            dataOut.label = parameters.numAttribute.label + "-" + dataOut.label;
            dataTotal.label = parameters.numAttribute.label + "-" + dataTotal.label;
    
            map[parameters.numAttribute.label][EXPENDITURE_IN] = dataIn;
            map[parameters.numAttribute.label][EXPENDITURE_OUT] = dataOut;
            map[parameters.numAttribute.label][EXPENDITURE_TOTAL] = dataTotal;
    
            return map;
        };
    
        function fillData(expenditure, dataType, value) {
            expenditure[dataType].data.push(value);
        }
    
        function flatMap(attributeMap) {
            var flattened = [];
            Object.values(attributeMap).forEach(function (valuesByAttribute) {
                Object.values(valuesByAttribute).forEach(function (value) {
                    flattened.push(value);
                });
            });
    
            return flattened;
        }
    
    };
    
    var Transformer = {};
    
    Transformer.JSON = function (mappedData) {
        if(!mappedData) return null;
    
        mappedData.datasets.forEach(function(dataset){
        ignoringKeys(['_meta'], dataset);
        });
    
        return JSON.stringify(mappedData);
    };
    
    function ignoringKeys(keys, data){
        keys.forEach(function(key){
            delete data[key];
        })
    }
    

  3. Specify mapper functions. Ensure they follow these signatures:

    • data: Represents the widget data.
    • parameters: Refers to the widget parameters.
    • baseUrl: The base URL for the widget.
    • return: Transformed data.
    1
    2
    Mapper = function (data, parameters, baseUrl) { ... }
    
  4. Specify view functions. Ensure they follow these signatures:

    • mappedData: The output from the Mapper function.
    • containerElementSelector: Should be set to jsRiadaWidget.
    • return: Void.
    1
    2
    View = function (mappedData, params, containerElementSelector) { ... }
    
  5. In the view function, append elements to the Document Object Model (DOM), ensuring the parent element is:

    1
    2
    <div id="riada" class="js-riada-widget">
    
  6. In the view function, append elements to the Document Object Model (DOM), ensuring the parent element is:

    1
    2
    <web-resource key="insight-report-payroll" i18n-name-key="Payroll Report Resource">
        <resource type="download" name="insight-report-payroll.css"
                  location="/css/insight-report-payroll.css"/>
        <resource type="download" name="insight-report-payroll.js"
                  location="/js/insight-report-payroll.js"/>
        <context>insight-report-payroll</context>
    </web-resource>
    
  7. Define any exporters for your data using the following structure.

    1
    2
    <exporters>
       <exporter transformer="Transformer.JSON"
                 extension="json"
                 label="insight.example.report.exporter.json"/>
    </exporters>
    

    The exported data will be the output generated by the Mapper:

    • mappedData: The Mapper output.

    • return: The data is transformed to the specified extension type.

      1
      2
      Transformer.JSON = function (mappedData) { ... }
      

Exporters are displayed in the created report but not in the preview.

Exporters view in an Asset custom report

Parameters

The following options are displayed in the report parameters form. The key corresponds to the widget parameters field name.

1
2
<parameter key="numAttribute"
            type="objecttypeattributepicker"
            label="insight.example.report.attribute.numeric"
            required="true">
    <configuration>
        <dependency key="objectType"/>
        <filters>
            <value>INTEGER</value>
            <value>DOUBLE</value>
        </filters>
    </configuration>
</parameter>

The current parameter type options are:

  • Checkbox
  • Datepicker
  • Datetimepicker
  • IQL
  • JQL
  • Number
  • Objectpicker
  • Objectschemapicker
  • Objectsearchfilterpicker
  • Objecttypeattributepicker
  • Objecttypepicker
  • Projectpicker
  • Radiobutton
  • Schemapicker
  • Select
  • Simpleobjecttypepicker
  • Switch
  • Text
  • Timepicker
  • Userpicker

Dependencies are relative to other parameters and filters on what types are returnable.

Congratulations!

You've built your own custom report.

Next steps

You can create almost any type of custom report in Assets by developing a Jira app with the Assets Widget Framework. ​ As demonstrated in this tutorial, the framework offers input through WidgetParameters, output via WidgetData, and a reporting engine using the WidgetModule. You can adapt this setup to provide reporting or exporting capabilities to meet nearly any requirement.

You might also want to explore other tutorials for Assets app developments:

Rate this page: