jirametrics 2.11 → 2.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/jirametrics-mcp +5 -0
- data/lib/jirametrics/aggregate_config.rb +10 -2
- data/lib/jirametrics/aging_work_bar_chart.rb +191 -133
- data/lib/jirametrics/aging_work_in_progress_chart.rb +43 -11
- data/lib/jirametrics/aging_work_table.rb +14 -17
- data/lib/jirametrics/anonymizer.rb +81 -6
- data/lib/jirametrics/atlassian_document_format.rb +160 -0
- data/lib/jirametrics/bar_chart_range.rb +17 -0
- data/lib/jirametrics/blocked_stalled_change.rb +5 -3
- data/lib/jirametrics/board.rb +34 -11
- data/lib/jirametrics/board_config.rb +5 -1
- data/lib/jirametrics/board_feature.rb +14 -0
- data/lib/jirametrics/board_movement_calculator.rb +10 -2
- data/lib/jirametrics/cfd_data_builder.rb +108 -0
- data/lib/jirametrics/change_item.rb +43 -20
- data/lib/jirametrics/chart_base.rb +143 -6
- data/lib/jirametrics/css_variable.rb +1 -1
- data/lib/jirametrics/cumulative_flow_diagram.rb +208 -0
- data/lib/jirametrics/{cycletime_config.rb → cycle_time_config.rb} +22 -5
- data/lib/jirametrics/cycletime_histogram.rb +15 -101
- data/lib/jirametrics/cycletime_scatterplot.rb +17 -83
- data/lib/jirametrics/daily_view.rb +306 -0
- data/lib/jirametrics/daily_wip_by_age_chart.rb +4 -5
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +14 -4
- data/lib/jirametrics/daily_wip_by_parent_chart.rb +4 -2
- data/lib/jirametrics/daily_wip_chart.rb +30 -8
- data/lib/jirametrics/data_quality_report.rb +43 -12
- data/lib/jirametrics/dependency_chart.rb +6 -3
- data/lib/jirametrics/download_config.rb +15 -0
- data/lib/jirametrics/downloader.rb +128 -71
- data/lib/jirametrics/downloader_for_cloud.rb +287 -0
- data/lib/jirametrics/downloader_for_data_center.rb +95 -0
- data/lib/jirametrics/estimate_accuracy_chart.rb +74 -12
- data/lib/jirametrics/estimation_configuration.rb +25 -0
- data/lib/jirametrics/examples/aggregated_project.rb +2 -2
- data/lib/jirametrics/examples/standard_project.rb +42 -27
- data/lib/jirametrics/expedited_chart.rb +3 -1
- data/lib/jirametrics/exporter.rb +26 -6
- data/lib/jirametrics/file_config.rb +10 -12
- data/lib/jirametrics/file_system.rb +59 -3
- data/lib/jirametrics/fix_version.rb +13 -0
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +5 -1
- data/lib/jirametrics/github_gateway.rb +115 -0
- data/lib/jirametrics/groupable_issue_chart.rb +11 -1
- data/lib/jirametrics/grouping_rules.rb +26 -4
- data/lib/jirametrics/html/aging_work_bar_chart.erb +5 -5
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +3 -1
- data/lib/jirametrics/html/aging_work_table.erb +7 -0
- data/lib/jirametrics/html/collapsible_issues_panel.erb +2 -2
- data/lib/jirametrics/html/cumulative_flow_diagram.erb +503 -0
- data/lib/jirametrics/html/daily_wip_chart.erb +40 -5
- data/lib/jirametrics/html/estimate_accuracy_chart.erb +4 -12
- data/lib/jirametrics/html/expedited_chart.erb +6 -14
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +4 -8
- data/lib/jirametrics/html/index.css +320 -69
- data/lib/jirametrics/html/index.erb +11 -20
- data/lib/jirametrics/html/index.js +164 -0
- data/lib/jirametrics/html/legacy_colors.css +174 -0
- data/lib/jirametrics/html/sprint_burndown.erb +17 -15
- data/lib/jirametrics/html/throughput_chart.erb +42 -11
- data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +61 -59
- data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +15 -11
- data/lib/jirametrics/html/wip_by_column_chart.erb +250 -0
- data/lib/jirametrics/html_generator.rb +32 -0
- data/lib/jirametrics/html_report_config.rb +52 -55
- data/lib/jirametrics/issue.rb +329 -106
- data/lib/jirametrics/issue_collection.rb +33 -0
- data/lib/jirametrics/issue_printer.rb +97 -0
- data/lib/jirametrics/jira_gateway.rb +81 -14
- data/lib/jirametrics/mcp_server.rb +531 -0
- data/lib/jirametrics/project_config.rb +151 -18
- data/lib/jirametrics/pull_request.rb +30 -0
- data/lib/jirametrics/pull_request_cycle_time_histogram.rb +77 -0
- data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +88 -0
- data/lib/jirametrics/pull_request_review.rb +13 -0
- data/lib/jirametrics/raw_javascript.rb +17 -0
- data/lib/jirametrics/settings.json +6 -1
- data/lib/jirametrics/sprint.rb +13 -0
- data/lib/jirametrics/sprint_burndown.rb +45 -37
- data/lib/jirametrics/sprint_issue_change_data.rb +3 -3
- data/lib/jirametrics/status.rb +1 -1
- data/lib/jirametrics/status_collection.rb +7 -0
- data/lib/jirametrics/stitcher.rb +81 -0
- data/lib/jirametrics/throughput_by_completed_resolution_chart.rb +22 -0
- data/lib/jirametrics/throughput_chart.rb +73 -23
- data/lib/jirametrics/time_based_histogram.rb +139 -0
- data/lib/jirametrics/time_based_scatterplot.rb +107 -0
- data/lib/jirametrics/user.rb +12 -0
- data/lib/jirametrics/wip_by_column_chart.rb +236 -0
- data/lib/jirametrics.rb +83 -64
- metadata +65 -6
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<%= seam_start %>
|
|
1
2
|
<div class="chart">
|
|
2
3
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
3
4
|
</div>
|
|
@@ -10,36 +11,37 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
10
11
|
options: {
|
|
11
12
|
title: {
|
|
12
13
|
display: true,
|
|
13
|
-
text: "
|
|
14
|
+
text: "<%= @header_text %>"
|
|
14
15
|
},
|
|
15
16
|
responsive: <%= canvas_responsive? %>, // If responsive is true then it fills the screen
|
|
16
17
|
scales: {
|
|
17
18
|
x: {
|
|
18
19
|
type: "time",
|
|
19
20
|
scaleLabel: {
|
|
20
|
-
display: true
|
|
21
|
-
labelString: 'Date Completed'
|
|
21
|
+
display: true
|
|
22
22
|
},
|
|
23
23
|
grid: {
|
|
24
24
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
25
25
|
},
|
|
26
|
+
<%= render_axis_title :x %>
|
|
26
27
|
min: "<%= date_range.begin.to_s %>",
|
|
27
28
|
max: "<%= (date_range.end + 1).to_s %>"
|
|
28
29
|
},
|
|
29
30
|
y: {
|
|
31
|
+
min: 0,
|
|
32
|
+
max: <%= (@highest_y_value * 1.1).ceil %>,
|
|
30
33
|
scaleLabel: {
|
|
31
|
-
display: true
|
|
32
|
-
labelString: 'Days',
|
|
33
|
-
min: 0,
|
|
34
|
-
max: <%= @highest_cycletime %>
|
|
35
|
-
},
|
|
36
|
-
title: {
|
|
37
|
-
display: true,
|
|
38
|
-
text: 'Cycle time in days'
|
|
34
|
+
display: true
|
|
39
35
|
},
|
|
36
|
+
<%= render_axis_title :y %>
|
|
40
37
|
grid: {
|
|
41
38
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
42
39
|
},
|
|
40
|
+
ticks: {
|
|
41
|
+
callback: function(value, index, ticks) {
|
|
42
|
+
return index === ticks.length - 1 ? null : value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
43
45
|
}
|
|
44
46
|
},
|
|
45
47
|
plugins: {
|
|
@@ -54,6 +56,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
54
56
|
annotation: {
|
|
55
57
|
annotations: {
|
|
56
58
|
<%= working_days_annotation %>
|
|
59
|
+
<%= date_annotation %>
|
|
57
60
|
|
|
58
61
|
<% @percentage_lines.each_with_index do |args, index| %>
|
|
59
62
|
<% percent, color = args %>
|
|
@@ -98,3 +101,4 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
98
101
|
}
|
|
99
102
|
});
|
|
100
103
|
</script>
|
|
104
|
+
<%= seam_end %>
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
<%= seam_start %>
|
|
2
|
+
<div class="chart" style="position:relative;">
|
|
3
|
+
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
4
|
+
<div id="<%= chart_id %>-tooltip" style="
|
|
5
|
+
display:none; position:absolute; pointer-events:none;
|
|
6
|
+
background:rgba(0,0,0,0.75); color:#fff; border-radius:4px;
|
|
7
|
+
padding:4px 8px; font:12px sans-serif; white-space:nowrap;
|
|
8
|
+
"></div>
|
|
9
|
+
</div>
|
|
10
|
+
<script>
|
|
11
|
+
(function() {
|
|
12
|
+
var wipData = <%= @wip_data.to_json %>;
|
|
13
|
+
var wipLimits = <%= @wip_limits.to_json %>;
|
|
14
|
+
var recommendations = <%= @recommendations.to_json %>;
|
|
15
|
+
var maxWip = <%= @max_wip %>;
|
|
16
|
+
var gridColor = <%= CssVariable['--grid-line-color'].to_json %>;
|
|
17
|
+
var barFillColor = <%= CssVariable['--wip-by-column-chart-bar-fill-color'].to_json %>;
|
|
18
|
+
var barTextColor = <%= CssVariable['--wip-by-column-chart-bar-text-color'].to_json %>;
|
|
19
|
+
var limitColor = <%= CssVariable['--wip-by-column-chart-limit-line-color'].to_json %>;
|
|
20
|
+
var recColor = <%= CssVariable['--wip-by-column-chart-recommendation-color'].to_json %>;
|
|
21
|
+
var tooltipEl = document.getElementById(<%= "#{chart_id}-tooltip".inspect %>);
|
|
22
|
+
|
|
23
|
+
var hitAreas = [];
|
|
24
|
+
|
|
25
|
+
var rectPlugin = {
|
|
26
|
+
id: 'wipRects',
|
|
27
|
+
afterDraw: function(chart) {
|
|
28
|
+
var ctx = chart.ctx;
|
|
29
|
+
var xScale = chart.scales['x'];
|
|
30
|
+
var yScale = chart.scales['y'];
|
|
31
|
+
var slotWidth = xScale.width / Math.max(xScale.ticks.length, 1);
|
|
32
|
+
|
|
33
|
+
hitAreas = [];
|
|
34
|
+
|
|
35
|
+
// 1. Draw y-axis gridlines at integer band boundaries
|
|
36
|
+
ctx.save();
|
|
37
|
+
ctx.beginPath();
|
|
38
|
+
ctx.rect(xScale.left, yScale.top, xScale.right - xScale.left, yScale.bottom - yScale.top);
|
|
39
|
+
ctx.clip();
|
|
40
|
+
ctx.strokeStyle = gridColor;
|
|
41
|
+
ctx.lineWidth = 1;
|
|
42
|
+
ctx.setLineDash([]);
|
|
43
|
+
for (var gi = 0; gi <= maxWip + 1; gi++) {
|
|
44
|
+
var gy = yScale.getPixelForValue(gi);
|
|
45
|
+
ctx.beginPath();
|
|
46
|
+
ctx.moveTo(xScale.left, gy);
|
|
47
|
+
ctx.lineTo(xScale.right, gy);
|
|
48
|
+
ctx.stroke();
|
|
49
|
+
}
|
|
50
|
+
ctx.restore();
|
|
51
|
+
|
|
52
|
+
// 2. Draw WIP limit lines (behind rectangles)
|
|
53
|
+
wipLimits.forEach(function(limits, colIndex) {
|
|
54
|
+
var xCenter = xScale.getPixelForValue(colIndex);
|
|
55
|
+
var halfSlot = slotWidth * 0.45;
|
|
56
|
+
|
|
57
|
+
[['min', 'bottom'], ['max', 'top']].forEach(function(pair) {
|
|
58
|
+
var type = pair[0];
|
|
59
|
+
var baseline = pair[1];
|
|
60
|
+
var val = limits[type];
|
|
61
|
+
if (val === null || val === undefined) return;
|
|
62
|
+
|
|
63
|
+
var y = yScale.getPixelForValue(val + 0.5);
|
|
64
|
+
|
|
65
|
+
ctx.save();
|
|
66
|
+
ctx.strokeStyle = limitColor;
|
|
67
|
+
ctx.lineWidth = 2;
|
|
68
|
+
ctx.setLineDash([5, 3]);
|
|
69
|
+
ctx.beginPath();
|
|
70
|
+
ctx.moveTo(xCenter - halfSlot, y);
|
|
71
|
+
ctx.lineTo(xCenter + halfSlot, y);
|
|
72
|
+
ctx.stroke();
|
|
73
|
+
ctx.restore();
|
|
74
|
+
|
|
75
|
+
ctx.save();
|
|
76
|
+
ctx.fillStyle = limitColor;
|
|
77
|
+
ctx.font = 'bold 10px sans-serif';
|
|
78
|
+
ctx.textAlign = 'right';
|
|
79
|
+
ctx.textBaseline = baseline;
|
|
80
|
+
ctx.fillText(type + ': ' + val, xCenter + halfSlot, baseline === 'bottom' ? y - 2 : y + 2);
|
|
81
|
+
ctx.restore();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
<% if @show_recommendations %>
|
|
86
|
+
// 3. Draw recommendation lines (behind rectangles, label on left)
|
|
87
|
+
recommendations.forEach(function(rec, colIndex) {
|
|
88
|
+
if (rec === null || rec === undefined || rec === 0) return;
|
|
89
|
+
var xCenter = xScale.getPixelForValue(colIndex);
|
|
90
|
+
var halfSlot = slotWidth * 0.45;
|
|
91
|
+
var y = yScale.getPixelForValue(rec + 0.5);
|
|
92
|
+
|
|
93
|
+
ctx.save();
|
|
94
|
+
ctx.strokeStyle = recColor;
|
|
95
|
+
ctx.lineWidth = 2;
|
|
96
|
+
ctx.setLineDash([5, 3]);
|
|
97
|
+
ctx.beginPath();
|
|
98
|
+
ctx.moveTo(xCenter - halfSlot, y);
|
|
99
|
+
ctx.lineTo(xCenter + halfSlot, y);
|
|
100
|
+
ctx.stroke();
|
|
101
|
+
ctx.restore();
|
|
102
|
+
|
|
103
|
+
ctx.save();
|
|
104
|
+
ctx.fillStyle = recColor;
|
|
105
|
+
ctx.font = 'bold 10px sans-serif';
|
|
106
|
+
ctx.textAlign = 'left';
|
|
107
|
+
ctx.textBaseline = 'top';
|
|
108
|
+
ctx.fillText('rec: ' + rec, xCenter - halfSlot, y + 2);
|
|
109
|
+
ctx.restore();
|
|
110
|
+
});
|
|
111
|
+
<% end %>
|
|
112
|
+
|
|
113
|
+
// 4. Draw WIP rectangles centered in their bands (wip + 0.5)
|
|
114
|
+
var yStep = Math.abs(yScale.getPixelForValue(0.5) - yScale.getPixelForValue(1.5));
|
|
115
|
+
|
|
116
|
+
wipData.forEach(function(colData, colIndex) {
|
|
117
|
+
var xCenter = xScale.getPixelForValue(colIndex);
|
|
118
|
+
|
|
119
|
+
colData.forEach(function(entry) {
|
|
120
|
+
var wip = entry['wip'];
|
|
121
|
+
var pct = entry['pct'];
|
|
122
|
+
var rectWidth = slotWidth * pct / 100;
|
|
123
|
+
var rectHeight = yStep * 0.8;
|
|
124
|
+
var yCenter = yScale.getPixelForValue(wip + 0.5);
|
|
125
|
+
var x1 = xCenter - rectWidth / 2;
|
|
126
|
+
var y1 = yCenter - rectHeight / 2;
|
|
127
|
+
|
|
128
|
+
ctx.save();
|
|
129
|
+
ctx.fillStyle = barFillColor;
|
|
130
|
+
ctx.strokeStyle = barFillColor;
|
|
131
|
+
ctx.lineWidth = 1;
|
|
132
|
+
ctx.fillRect(x1, y1, rectWidth, rectHeight);
|
|
133
|
+
ctx.strokeRect(x1, y1, rectWidth, rectHeight);
|
|
134
|
+
|
|
135
|
+
ctx.fillStyle = barTextColor;
|
|
136
|
+
ctx.font = '11px sans-serif';
|
|
137
|
+
ctx.textAlign = 'center';
|
|
138
|
+
ctx.textBaseline = 'middle';
|
|
139
|
+
if (rectWidth > 25) {
|
|
140
|
+
ctx.fillText(pct + '%', xCenter, yCenter);
|
|
141
|
+
}
|
|
142
|
+
ctx.restore();
|
|
143
|
+
|
|
144
|
+
var hitWidth = Math.max(rectWidth, slotWidth);
|
|
145
|
+
hitAreas.push({
|
|
146
|
+
x1: xCenter - hitWidth / 2, y1: y1,
|
|
147
|
+
x2: xCenter + hitWidth / 2, y2: y1 + rectHeight,
|
|
148
|
+
label: 'WIP ' + wip + ': ' + pct + '%'
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
var canvas = document.getElementById(<%= chart_id.inspect %>);
|
|
156
|
+
|
|
157
|
+
canvas.addEventListener('mousemove', function(e) {
|
|
158
|
+
var rect = canvas.getBoundingClientRect();
|
|
159
|
+
var mx = e.clientX - rect.left;
|
|
160
|
+
var my = e.clientY - rect.top;
|
|
161
|
+
|
|
162
|
+
var hit = null;
|
|
163
|
+
for (var i = 0; i < hitAreas.length; i++) {
|
|
164
|
+
var a = hitAreas[i];
|
|
165
|
+
if (mx >= a.x1 && mx <= a.x2 && my >= a.y1 && my <= a.y2) {
|
|
166
|
+
hit = a;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (hit) {
|
|
172
|
+
tooltipEl.textContent = hit.label;
|
|
173
|
+
tooltipEl.style.display = 'block';
|
|
174
|
+
tooltipEl.style.left = (mx + 10) + 'px';
|
|
175
|
+
tooltipEl.style.top = (my - 20) + 'px';
|
|
176
|
+
} else {
|
|
177
|
+
tooltipEl.style.display = 'none';
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
canvas.addEventListener('mouseleave', function() {
|
|
182
|
+
tooltipEl.style.display = 'none';
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
new Chart(canvas.getContext('2d'),
|
|
186
|
+
{
|
|
187
|
+
type: 'bar',
|
|
188
|
+
plugins: [rectPlugin],
|
|
189
|
+
data: {
|
|
190
|
+
labels: <%= @column_names.to_json %>,
|
|
191
|
+
datasets: [{
|
|
192
|
+
data: [],
|
|
193
|
+
backgroundColor: 'transparent'
|
|
194
|
+
}]
|
|
195
|
+
},
|
|
196
|
+
options: {
|
|
197
|
+
responsive: <%= canvas_responsive? %>,
|
|
198
|
+
scales: {
|
|
199
|
+
x: {
|
|
200
|
+
grid: {
|
|
201
|
+
color: gridColor,
|
|
202
|
+
z: 1
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
y: {
|
|
206
|
+
title: {
|
|
207
|
+
display: true,
|
|
208
|
+
text: 'WIP'
|
|
209
|
+
},
|
|
210
|
+
grid: {
|
|
211
|
+
display: false
|
|
212
|
+
},
|
|
213
|
+
min: 0,
|
|
214
|
+
max: <%= @max_wip + 1 %>,
|
|
215
|
+
afterBuildTicks: function(scale) {
|
|
216
|
+
scale.ticks = [];
|
|
217
|
+
for (var i = 0; i <= maxWip; i++) {
|
|
218
|
+
scale.ticks.push({ value: i + 0.5 });
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
ticks: {
|
|
222
|
+
callback: function(value) {
|
|
223
|
+
return Math.round(value - 0.5);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
plugins: {
|
|
229
|
+
legend: {
|
|
230
|
+
display: false
|
|
231
|
+
},
|
|
232
|
+
tooltip: {
|
|
233
|
+
enabled: false
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
})();
|
|
239
|
+
</script>
|
|
240
|
+
<%= seam_end %>
|
|
241
|
+
<% unless @recommendation_texts.empty? %>
|
|
242
|
+
<div style="margin-top: 0.5em;">
|
|
243
|
+
<strong>WIP limit recommendations</strong>
|
|
244
|
+
<ul style="margin: 0.3em 0 0 0; padding-left: 1.5em;">
|
|
245
|
+
<% @recommendation_texts.each do |text| %>
|
|
246
|
+
<li><%= text %></li>
|
|
247
|
+
<% end %>
|
|
248
|
+
</ul>
|
|
249
|
+
</div>
|
|
250
|
+
<% end %>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class HtmlGenerator
|
|
4
|
+
attr_accessor :file_system, :settings
|
|
5
|
+
|
|
6
|
+
def create_html output_filename:, settings:, project_name: ''
|
|
7
|
+
@settings = settings
|
|
8
|
+
project_name = project_name.to_s
|
|
9
|
+
html_directory = "#{Pathname.new(File.realpath(__FILE__)).dirname}/html"
|
|
10
|
+
css = load_css html_directory: html_directory
|
|
11
|
+
javascript = file_system.load(File.join(html_directory, 'index.js'))
|
|
12
|
+
erb = ERB.new file_system.load(File.join(html_directory, 'index.erb'))
|
|
13
|
+
file_system.save_file content: erb.result(binding), filename: output_filename
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def load_css html_directory:
|
|
17
|
+
base_css_filename = File.join(html_directory, 'index.css')
|
|
18
|
+
base_css = file_system.load(base_css_filename)
|
|
19
|
+
|
|
20
|
+
extra_css_filename = settings['include_css']
|
|
21
|
+
if extra_css_filename
|
|
22
|
+
if File.exist?(extra_css_filename)
|
|
23
|
+
base_css << "\n\n" << file_system.load(extra_css_filename)
|
|
24
|
+
log("Loaded CSS: #{extra_css_filename}")
|
|
25
|
+
else
|
|
26
|
+
log("Unable to find specified CSS file: #{extra_css_filename}")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
base_css
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require 'erb'
|
|
4
4
|
require 'jirametrics/self_or_issue_dispatcher'
|
|
5
5
|
|
|
6
|
-
class HtmlReportConfig
|
|
6
|
+
class HtmlReportConfig < HtmlGenerator
|
|
7
7
|
include SelfOrIssueDispatcher
|
|
8
8
|
|
|
9
9
|
attr_reader :file_config, :sections, :charts
|
|
@@ -20,37 +20,66 @@ class HtmlReportConfig
|
|
|
20
20
|
module_eval lines.join("\n"), __FILE__, __LINE__
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
define_chart name: 'aging_work_bar_chart', classname: 'AgingWorkBarChart'
|
|
24
|
-
define_chart name: 'aging_work_table', classname: 'AgingWorkTable'
|
|
25
|
-
define_chart name: 'cycletime_scatterplot', classname: 'CycletimeScatterplot'
|
|
26
|
-
define_chart name: 'daily_wip_chart', classname: 'DailyWipChart'
|
|
27
|
-
define_chart name: 'daily_wip_by_age_chart', classname: 'DailyWipByAgeChart'
|
|
28
|
-
define_chart name: 'daily_wip_by_blocked_stalled_chart', classname: 'DailyWipByBlockedStalledChart'
|
|
29
|
-
define_chart name: 'daily_wip_by_parent_chart', classname: 'DailyWipByParentChart'
|
|
30
|
-
define_chart name: 'throughput_chart', classname: 'ThroughputChart'
|
|
31
|
-
define_chart name: 'expedited_chart', classname: 'ExpeditedChart'
|
|
32
|
-
define_chart name: 'cycletime_histogram', classname: 'CycletimeHistogram'
|
|
33
|
-
define_chart name: 'estimate_accuracy_chart', classname: 'EstimateAccuracyChart'
|
|
34
|
-
define_chart name: 'hierarchy_table', classname: 'HierarchyTable'
|
|
35
|
-
define_chart name: 'flow_efficiency_scatterplot', classname: 'FlowEfficiencyScatterplot'
|
|
36
|
-
|
|
37
23
|
define_chart name: 'daily_wip_by_type', classname: 'DailyWipChart',
|
|
38
24
|
deprecated_warning: 'This is the same as daily_wip_chart. Please use that one', deprecated_date: '2024-05-23'
|
|
39
25
|
define_chart name: 'story_point_accuracy_chart', classname: 'EstimateAccuracyChart',
|
|
40
26
|
deprecated_warning: 'Renamed to estimate_accuracy_chart. Please use that one', deprecated_date: '2024-05-23'
|
|
41
27
|
|
|
42
28
|
def initialize file_config:, block:
|
|
29
|
+
super()
|
|
43
30
|
@file_config = file_config
|
|
44
31
|
@block = block
|
|
45
32
|
@sections = [] # Where we store the chunks of text that will be assembled into the HTML
|
|
46
33
|
@charts = [] # Where we store all the charts we executed so we can assert against them.
|
|
47
34
|
end
|
|
48
35
|
|
|
36
|
+
def method_missing name, *_args, board_id: nil, **_kwargs, &block
|
|
37
|
+
class_name = name.to_s.split('_').map(&:capitalize).join
|
|
38
|
+
klass = resolve_chart_class(class_name)
|
|
39
|
+
return super if klass.nil?
|
|
40
|
+
|
|
41
|
+
block ||= ->(_) {}
|
|
42
|
+
|
|
43
|
+
if klass.instance_method(:board_id=).owner == klass
|
|
44
|
+
execute_chart_per_board klass: klass, block: block, board_id: board_id
|
|
45
|
+
else
|
|
46
|
+
execute_chart klass.new(block)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def resolve_chart_class class_name
|
|
51
|
+
klass = Object.const_get(class_name)
|
|
52
|
+
klass < ChartBase ? klass : nil
|
|
53
|
+
rescue NameError
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def execute_chart_per_board klass:, block:, board_id:
|
|
58
|
+
all_boards = @file_config.project_config.all_boards
|
|
59
|
+
ids = board_id ? [board_id] : issues.collect { |i| i.board.id }.uniq
|
|
60
|
+
ids = ids.sort_by { |id| all_boards[id]&.name || '' }
|
|
61
|
+
ids.each_with_index do |id, index|
|
|
62
|
+
execute_chart(klass.new(block)) do |chart|
|
|
63
|
+
chart.board_id = id
|
|
64
|
+
# We're showing the description only on the first one in order to reduce noise on the report
|
|
65
|
+
chart.description_text nil unless index.zero?
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def respond_to_missing? name, include_private = false
|
|
71
|
+
class_name = name.to_s.split('_').map(&:capitalize).join
|
|
72
|
+
!resolve_chart_class(class_name).nil? || super
|
|
73
|
+
end
|
|
74
|
+
|
|
49
75
|
def cycletime label = nil, &block
|
|
50
76
|
@file_config.project_config.all_boards.each_value do |board|
|
|
51
77
|
raise 'Multiple cycletimes not supported' if board.cycletime
|
|
52
78
|
|
|
53
|
-
board.cycletime = CycleTimeConfig.new(
|
|
79
|
+
board.cycletime = CycleTimeConfig.new(
|
|
80
|
+
possible_statuses: file_config.project_config, label: label, block: block,
|
|
81
|
+
file_system: file_system, settings: settings
|
|
82
|
+
)
|
|
54
83
|
end
|
|
55
84
|
end
|
|
56
85
|
|
|
@@ -69,10 +98,8 @@ class HtmlReportConfig
|
|
|
69
98
|
|
|
70
99
|
html create_footer
|
|
71
100
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
erb = ERB.new file_system.load(File.join(html_directory, 'index.erb'))
|
|
75
|
-
file_system.save_file content: erb.result(binding), filename: @file_config.output_filename
|
|
101
|
+
create_html output_filename: @file_config.output_filename, settings: settings,
|
|
102
|
+
project_name: @file_config.project_config.name
|
|
76
103
|
end
|
|
77
104
|
|
|
78
105
|
def file_system
|
|
@@ -83,24 +110,6 @@ class HtmlReportConfig
|
|
|
83
110
|
file_system.log message
|
|
84
111
|
end
|
|
85
112
|
|
|
86
|
-
def load_css html_directory:
|
|
87
|
-
base_css_filename = File.join(html_directory, 'index.css')
|
|
88
|
-
base_css = file_system.load(base_css_filename)
|
|
89
|
-
log("Loaded CSS: #{base_css_filename}")
|
|
90
|
-
|
|
91
|
-
extra_css_filename = settings['include_css']
|
|
92
|
-
if extra_css_filename
|
|
93
|
-
if File.exist?(extra_css_filename)
|
|
94
|
-
base_css << "\n\n" << file_system.load(extra_css_filename)
|
|
95
|
-
log("Loaded CSS: #{extra_css_filename}")
|
|
96
|
-
else
|
|
97
|
-
log("Unable to find specified CSS file: #{extra_css_filename}")
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
base_css
|
|
102
|
-
end
|
|
103
|
-
|
|
104
113
|
def board_id id
|
|
105
114
|
@board_id = id
|
|
106
115
|
end
|
|
@@ -109,24 +118,9 @@ class HtmlReportConfig
|
|
|
109
118
|
@file_config.project_config.exporter.timezone_offset
|
|
110
119
|
end
|
|
111
120
|
|
|
112
|
-
def aging_work_in_progress_chart board_id: nil, &block
|
|
113
|
-
block ||= ->(_) {}
|
|
114
|
-
|
|
115
|
-
if board_id.nil?
|
|
116
|
-
ids = issues.collect { |i| i.board.id }.uniq.sort
|
|
117
|
-
else
|
|
118
|
-
ids = [board_id]
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
ids.each do |id|
|
|
122
|
-
execute_chart(AgingWorkInProgressChart.new(block)) do |chart|
|
|
123
|
-
chart.board_id = id
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
121
|
def random_color
|
|
129
|
-
|
|
122
|
+
@palette_index = (@palette_index || -1) + 1
|
|
123
|
+
ChartBase::OKABE_ITO_PALETTE[@palette_index % ChartBase::OKABE_ITO_PALETTE.size]
|
|
130
124
|
end
|
|
131
125
|
|
|
132
126
|
def html string, type: :body
|
|
@@ -159,10 +153,12 @@ class HtmlReportConfig
|
|
|
159
153
|
chart.time_range = project_config.time_range
|
|
160
154
|
chart.timezone_offset = timezone_offset
|
|
161
155
|
chart.settings = settings
|
|
156
|
+
chart.atlassian_document_format = project_config.atlassian_document_format
|
|
162
157
|
|
|
163
158
|
chart.all_boards = project_config.all_boards
|
|
164
159
|
chart.board_id = find_board_id
|
|
165
160
|
chart.holiday_dates = project_config.exporter.holiday_dates
|
|
161
|
+
chart.fix_versions = project_config.fix_versions
|
|
166
162
|
|
|
167
163
|
time_range = @file_config.project_config.time_range
|
|
168
164
|
chart.date_range = time_range.begin.to_date..time_range.end.to_date
|
|
@@ -171,6 +167,7 @@ class HtmlReportConfig
|
|
|
171
167
|
after_init_block&.call chart
|
|
172
168
|
|
|
173
169
|
@charts << chart
|
|
170
|
+
chart.before_run
|
|
174
171
|
html chart.run
|
|
175
172
|
end
|
|
176
173
|
|