Skip to content

Commit be686f9

Browse files
committed
add burndown chart widget
1 parent 4ec98a4 commit be686f9

File tree

6 files changed

+284
-15
lines changed

6 files changed

+284
-15
lines changed

src/chart/chartController.js

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
'use strict';
2+
3+
angular.module('adf.widget.redmine')
4+
.controller('ChartController', function (issues, config) {
5+
//console.log("issues length: " + issues.length);
6+
var vm = this;
7+
vm.config = config;
8+
vm.issues = issues;
9+
10+
var calculateOpenIssuesPerDay = function (from, to, issues) {
11+
// order issues by creation date
12+
var openIssues = []; // inv: ordered by "closed_on"
13+
var dates = [];// x-values
14+
var values = [];// y-values
15+
while (from.getTime() <= to.getTime()) {
16+
moveNewOpenIssues(issues, openIssues, from);
17+
removeNewClosedIssues(openIssues, from);
18+
dates.push(from.toDateString());
19+
values.push(openIssues.length)
20+
from.setDate(from.getDate() + 1); // next day
21+
}
22+
return {
23+
dates: dates,
24+
values: values
25+
}
26+
}
27+
var moveNewOpenIssues = function (allIssues, openIssues, date) {
28+
//console.log("allIssues.length "+allIssues.length);
29+
for (var i = 0; i < allIssues.length; i++) {
30+
var createDate = new Date(allIssues[i].created_on);
31+
//console.log("createDate: "+createDate.toDateString());
32+
if (createDate.getTime() <= date.getTime()) {
33+
openIssues.push(allIssues[i]);// should be still sorted
34+
allIssues.splice(i, 1);
35+
i--;
36+
} else {
37+
//break;
38+
}
39+
}
40+
}
41+
var removeNewClosedIssues = function (openIssues, date) {
42+
for (var i = 0; i < openIssues.length; i++) {
43+
if (openIssues[i].closed_on) {
44+
var closeDate = new Date(openIssues[i].closed_on);
45+
//console.log("closeDate: " + closeDate.toDateString());
46+
if (closeDate.getTime() <= date.getTime()) {
47+
openIssues.splice(i, 1);
48+
i--;
49+
} else {
50+
//break;
51+
}
52+
}
53+
}
54+
}
55+
56+
var from = new Date(vm.config.timespan.fromDateTime);
57+
var to = new Date(vm.config.timespan.toDateTime);
58+
var generatedData = calculateOpenIssuesPerDay(from, to, issues);
59+
60+
var options = {
61+
scales: {
62+
yAxes: [
63+
{
64+
id: 'y-axis-1',
65+
display: true,
66+
position: 'left',
67+
scaleLabel: {
68+
display: true,
69+
labelString: 'Open Issues'
70+
}
71+
}
72+
]
73+
},
74+
legend: {
75+
display: true,
76+
position: "bottom"
77+
}
78+
};
79+
80+
vm.chart = {
81+
labels: generatedData.dates,
82+
data: [generatedData.values],
83+
series: ["Project ..."],
84+
class: "chart-line",
85+
options: options
86+
};
87+
});

src/chart/edit/edit.html

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<form role="form">
2+
<div class="form-group">
3+
<label for="project">Project</label>
4+
<select name="project" id="project" class="form-control" ng-model="config.project">
5+
<option value="All">All</option>
6+
<option ng-repeat="project in vm.projects | orderBy: 'name'" value="{{project.id}}">{{project.name}}</option>
7+
</select>
8+
</div>
9+
<div class="form-group">
10+
<label for="assgined_to_id">Assigned To</label>
11+
<span class="glyphicon glyphicon-info-sign" uib-tooltip="Get issues which are assigned to the given user ID.
12+
<me> can be used instead an ID to fetch all issues from the logged in user. Leave empty if you want to see all issues."></span>
13+
<input name="assigned_to_id" id="assgined_to_id" class="form-control" ng-model="config.assigned_to_id">
14+
</input>
15+
</div>
16+
<div>
17+
<p class="input-group">
18+
<input class="form-control" datepicker-options="vm.dateOptions" is-open="vm.popup1.opened" ng-model="vm.config.timespan.fromDateTime"
19+
placeholder="from" show-button-bar="false" type="text" uib-datepicker-popup="{{format}}" />
20+
<span class="input-group-btn">
21+
<button class="btn btn-default" ng-click="vm.open1()" type="button">
22+
<i class="glyphicon glyphicon-calendar"></i>
23+
</button>
24+
</span>
25+
</p>
26+
<p class="input-group">
27+
<input class="form-control" datepicker-options="vm.dateOptions" is-open="vm.popup2.opened" ng-model="vm.config.timespan.toDateTime"
28+
placeholder="to" show-button-bar="false" type="text" uib-datepicker-popup="{{format}}" />
29+
<span class="input-group-btn">
30+
<button class="btn btn-default" ng-click="vm.open2()" type="button">
31+
<i class="glyphicon glyphicon-calendar"></i>
32+
</button>
33+
</span>
34+
</p>
35+
</div>
36+
</form>

src/chart/edit/editController.js

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use strict';
2+
3+
angular.module('adf.widget.redmine')
4+
.controller('editController', function (projects, config) {
5+
var vm = this;
6+
vm.config = config;
7+
8+
if (angular.equals({}, config)) {
9+
config.project = "";
10+
config.assigned_to_id = "me";
11+
config.showClosed = true;
12+
}
13+
14+
if (!vm.config.timespan) {
15+
vm.config.timespan = {};
16+
}
17+
// convert strings to date objects
18+
if (vm.config.timespan.fromDateTime) {
19+
vm.config.timespan.fromDateTime = new Date(vm.config.timespan.fromDateTime);
20+
vm.config.timespan.toDateTime = new Date(vm.config.timespan.toDateTime);
21+
}
22+
23+
vm.projects = projects;
24+
25+
26+
vm.inlineOptions = {
27+
customClass: getDayClass,
28+
minDate: new Date(),
29+
showWeeks: true
30+
};
31+
32+
function getDayClass(data) {
33+
var date = data.date,
34+
mode = data.mode;
35+
if (mode === 'day') {
36+
var dayToCheck = new Date(date).setHours(0, 0, 0, 0);
37+
38+
for (var i = 0; i < vm.events.length; i++) {
39+
var currentDay = new Date(vm.events[i].date).setHours(0, 0, 0, 0);
40+
41+
if (dayToCheck === currentDay) {
42+
return vm.events[i].status;
43+
}
44+
}
45+
}
46+
return '';
47+
}
48+
if (!vm.dateOptions) {
49+
vm.dateOptions = {
50+
formatYear: 'yy',
51+
startingDay: 1
52+
};
53+
}
54+
55+
vm.toggleMin = function () {
56+
vm.inlineOptions.minDate = vm.inlineOptions.minDate ? null : new Date();
57+
vm.dateOptions.minDate = vm.inlineOptions.minDate;
58+
};
59+
60+
vm.toggleMin();
61+
62+
vm.open1 = function () {
63+
vm.popup1.opened = true;
64+
};
65+
66+
vm.open2 = function () {
67+
vm.popup2.opened = true;
68+
};
69+
70+
vm.formats = ['yyyy-MM-dd', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate'];
71+
vm.format = vm.formats[0];
72+
vm.altInputFormats = ['M!/d!/yyyy'];
73+
74+
vm.popup1 = {
75+
opened: false
76+
};
77+
78+
vm.popup2 = {
79+
opened: false
80+
};
81+
82+
});

src/chart/view.html

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<div class="alert alert-info" ng-if="!vm.chart">
2+
Please configure the widget
3+
</div>
4+
<div ng-if="vm.chart">
5+
<canvas id="line" class="chart chart-line"
6+
chart-data="vm.chart.data" chart-labels="vm.chart.labels"
7+
chart-series="vm.chart.series" chart-options="vm.chart.options">
8+
</canvas>
9+
</div>

src/redmine.js

+33-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
'use strict';
22

3-
angular.module('adf.widget.redmine', ['adf.provider', 'smart-table'])
3+
angular.module('adf.widget.redmine', ['adf.provider', 'smart-table', 'chart.js', 'ui.bootstrap.datepicker'])
44
.constant("redmineEndpoint", "http://www.redmine.org/")
55
.config(function(dashboardProvider){
66

7-
var edit = {
7+
var editIssues = {
88
templateUrl: '{widgetsPath}/redmine/src/issues/edit/edit.html',
99
controller: 'editController',
1010
controllerAs: 'vm',
@@ -16,10 +16,22 @@ angular.module('adf.widget.redmine', ['adf.provider', 'smart-table'])
1616
}
1717
};
1818

19+
var editChart = {
20+
templateUrl: '{widgetsPath}/redmine/src/chart/edit/edit.html',
21+
controller: 'editController',
22+
controllerAs: 'vm',
23+
resolve: {
24+
/** @ngInject **/
25+
projects: function(redmineService){
26+
return redmineService.getProjects();
27+
}
28+
}
29+
};
30+
1931
dashboardProvider
20-
.widget('redmineIssues', {
32+
.widget('redmine-issues', {
2133
title: 'Redmine Issues',
22-
description: 'Show Issues of an given Redmine Instance.',
34+
description: 'Shows Issues of a given Redmine Instance',
2335
templateUrl: '{widgetsPath}/redmine/src/issues/view.html',
2436
controller: 'IssueController',
2537
controllerAs: 'vm',
@@ -29,6 +41,22 @@ angular.module('adf.widget.redmine', ['adf.provider', 'smart-table'])
2941
return redmineService.getIssues(config);
3042
}
3143
},
32-
edit: edit
44+
edit: editIssues
45+
});
46+
47+
dashboardProvider
48+
.widget('redmine-chart', {
49+
title: 'Redmine Chart',
50+
description: 'Displays a burnup or burndown chart',
51+
templateUrl: '{widgetsPath}/redmine/src/chart/view.html',
52+
controller: 'ChartController',
53+
controllerAs: 'vm',
54+
resolve: {
55+
/** @ngInject **/
56+
issues: function(redmineService, config){
57+
return redmineService.getIssues(config);
58+
}
59+
},
60+
edit: editChart
3361
});
3462
});

src/service.js

+37-10
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,60 @@
33
angular.module('adf.widget.redmine')
44
.factory('redmineService', function($http, redmineEndpoint){
55

6-
function data(response){
6+
function extractData(response){
77
return response.data;
88
}
99

1010
function request(param){
11-
return $http.get(redmineEndpoint+param).then(data);
11+
return $http.get(redmineEndpoint+param).then(extractData);
1212
}
1313

1414
function getIssues(config){
15+
var allIssues = [];
1516
var params=generateIssuesParameter(config);
16-
return request('issues.json'+params);
17+
var limit = config.limit ? config.limit : Number.MAX_SAFE_INTEGER;
18+
return collectPageIssues(params, allIssues, 0, limit);
19+
}
20+
21+
function collectPageIssues(params, allIssues, offset, limit){
22+
return request('issues.json'+params+'&offset='+offset).then(function(issues){
23+
angular.forEach(issues.issues, function(issue){
24+
allIssues.push(issue);
25+
});
26+
if(issues.total_count > allIssues.length && allIssues.length < limit) {
27+
return collectPageIssues(params, allIssues, offset+100, limit);
28+
}
29+
return allIssues;
30+
});
1731
}
1832

1933
function generateIssuesParameter(data) {
20-
var params='?';
21-
if(data.project && data.project !== "All") {
22-
params+='&project_id='+data.project;
34+
var params = '?limit=100';
35+
if (data.project && data.project !== "All") {
36+
params += '&project_id=' + data.project;
2337
}
24-
if(data.assigned_to_id) {
25-
params+='&assigned_to_id='+data.assigned_to_id;
38+
if (data.assigned_to_id) {
39+
params += '&assigned_to_id=' + data.assigned_to_id;
2640
}
27-
if(data.showClosed) {
28-
params+='&status_id=*';
41+
if (data.showClosed) {
42+
params += '&status_id=*';
43+
}
44+
if (data.timespan.fromDateTime && data.timespan.toDateTime) {
45+
var fromDate = new Date(data.timespan.fromDateTime);
46+
var toDate = new Date(data.timespan.toDateTime);
47+
48+
params += '&created_on=%3E%3C' + dateToYMD(fromDate) + '|' + dateToYMD(toDate);
2949
}
3050
return params;
3151
}
3252

53+
function dateToYMD(date) {
54+
var d = date.getDate();
55+
var m = date.getMonth() + 1;
56+
var y = date.getFullYear();
57+
return '' + y + '-' + (m <= 9 ? '0' + m : m) + '-' + (d <= 9 ? '0' + d : d);
58+
}
59+
3360
function getProjects(){
3461
return request('projects.json').then(function(data){
3562
return data.projects;

0 commit comments

Comments
 (0)