Developer
News and Updates
Get Support
Sign in
Get Support
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in
Last updated Aug 7, 2026

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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
```
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
```
<?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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
```
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
```
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
    3
    4
    5
    6
    <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
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    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
    3
    4
    5
    6
    7
    8
    <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
    3
    4
    5
    6
    <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
3
4
5
6
7
8
9
10
11
12
13
<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: