jirametrics 2.13 → 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 +9 -7
- data/lib/jirametrics/anonymizer.rb +81 -6
- data/lib/jirametrics/atlassian_document_format.rb +101 -97
- data/lib/jirametrics/bar_chart_range.rb +17 -0
- data/lib/jirametrics/blocked_stalled_change.rb +5 -3
- data/lib/jirametrics/board.rb +32 -8
- data/lib/jirametrics/board_config.rb +4 -1
- data/lib/jirametrics/board_feature.rb +14 -0
- data/lib/jirametrics/board_movement_calculator.rb +2 -2
- data/lib/jirametrics/cfd_data_builder.rb +108 -0
- data/lib/jirametrics/change_item.rb +14 -6
- data/lib/jirametrics/chart_base.rb +141 -3
- 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} +21 -4
- data/lib/jirametrics/cycletime_histogram.rb +15 -101
- data/lib/jirametrics/cycletime_scatterplot.rb +17 -83
- data/lib/jirametrics/daily_view.rb +85 -53
- 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 +117 -100
- 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 +42 -4
- data/lib/jirametrics/examples/aggregated_project.rb +2 -2
- data/lib/jirametrics/examples/standard_project.rb +41 -28
- data/lib/jirametrics/expedited_chart.rb +3 -1
- data/lib/jirametrics/exporter.rb +26 -6
- data/lib/jirametrics/file_config.rb +9 -11
- 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 +5 -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 +249 -69
- data/lib/jirametrics/html/index.erb +9 -35
- 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 -57
- data/lib/jirametrics/issue.rb +304 -101
- data/lib/jirametrics/issue_printer.rb +97 -0
- data/lib/jirametrics/jira_gateway.rb +77 -17
- data/lib/jirametrics/mcp_server.rb +531 -0
- data/lib/jirametrics/project_config.rb +128 -12
- 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 +5 -1
- data/lib/jirametrics/sprint.rb +12 -0
- data/lib/jirametrics/sprint_burndown.rb +10 -4
- data/lib/jirametrics/status.rb +1 -1
- 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/wip_by_column_chart.rb +236 -0
- data/lib/jirametrics.rb +83 -69
- metadata +60 -6
|
@@ -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,38 +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
|
-
define_chart name: 'daily_view', classname: 'DailyView'
|
|
37
|
-
|
|
38
23
|
define_chart name: 'daily_wip_by_type', classname: 'DailyWipChart',
|
|
39
24
|
deprecated_warning: 'This is the same as daily_wip_chart. Please use that one', deprecated_date: '2024-05-23'
|
|
40
25
|
define_chart name: 'story_point_accuracy_chart', classname: 'EstimateAccuracyChart',
|
|
41
26
|
deprecated_warning: 'Renamed to estimate_accuracy_chart. Please use that one', deprecated_date: '2024-05-23'
|
|
42
27
|
|
|
43
28
|
def initialize file_config:, block:
|
|
29
|
+
super()
|
|
44
30
|
@file_config = file_config
|
|
45
31
|
@block = block
|
|
46
32
|
@sections = [] # Where we store the chunks of text that will be assembled into the HTML
|
|
47
33
|
@charts = [] # Where we store all the charts we executed so we can assert against them.
|
|
48
34
|
end
|
|
49
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
|
+
|
|
50
75
|
def cycletime label = nil, &block
|
|
51
76
|
@file_config.project_config.all_boards.each_value do |board|
|
|
52
77
|
raise 'Multiple cycletimes not supported' if board.cycletime
|
|
53
78
|
|
|
54
|
-
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
|
+
)
|
|
55
83
|
end
|
|
56
84
|
end
|
|
57
85
|
|
|
@@ -70,10 +98,8 @@ class HtmlReportConfig
|
|
|
70
98
|
|
|
71
99
|
html create_footer
|
|
72
100
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
erb = ERB.new file_system.load(File.join(html_directory, 'index.erb'))
|
|
76
|
-
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
|
|
77
103
|
end
|
|
78
104
|
|
|
79
105
|
def file_system
|
|
@@ -84,24 +110,6 @@ class HtmlReportConfig
|
|
|
84
110
|
file_system.log message
|
|
85
111
|
end
|
|
86
112
|
|
|
87
|
-
def load_css html_directory:
|
|
88
|
-
base_css_filename = File.join(html_directory, 'index.css')
|
|
89
|
-
base_css = file_system.load(base_css_filename)
|
|
90
|
-
log("Loaded CSS: #{base_css_filename}")
|
|
91
|
-
|
|
92
|
-
extra_css_filename = settings['include_css']
|
|
93
|
-
if extra_css_filename
|
|
94
|
-
if File.exist?(extra_css_filename)
|
|
95
|
-
base_css << "\n\n" << file_system.load(extra_css_filename)
|
|
96
|
-
log("Loaded CSS: #{extra_css_filename}")
|
|
97
|
-
else
|
|
98
|
-
log("Unable to find specified CSS file: #{extra_css_filename}")
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
base_css
|
|
103
|
-
end
|
|
104
|
-
|
|
105
113
|
def board_id id
|
|
106
114
|
@board_id = id
|
|
107
115
|
end
|
|
@@ -110,24 +118,9 @@ class HtmlReportConfig
|
|
|
110
118
|
@file_config.project_config.exporter.timezone_offset
|
|
111
119
|
end
|
|
112
120
|
|
|
113
|
-
def aging_work_in_progress_chart board_id: nil, &block
|
|
114
|
-
block ||= ->(_) {}
|
|
115
|
-
|
|
116
|
-
if board_id.nil?
|
|
117
|
-
ids = issues.collect { |i| i.board.id }.uniq.sort
|
|
118
|
-
else
|
|
119
|
-
ids = [board_id]
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
ids.each do |id|
|
|
123
|
-
execute_chart(AgingWorkInProgressChart.new(block)) do |chart|
|
|
124
|
-
chart.board_id = id
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
121
|
def random_color
|
|
130
|
-
|
|
122
|
+
@palette_index = (@palette_index || -1) + 1
|
|
123
|
+
ChartBase::OKABE_ITO_PALETTE[@palette_index % ChartBase::OKABE_ITO_PALETTE.size]
|
|
131
124
|
end
|
|
132
125
|
|
|
133
126
|
def html string, type: :body
|
|
@@ -160,11 +153,12 @@ class HtmlReportConfig
|
|
|
160
153
|
chart.time_range = project_config.time_range
|
|
161
154
|
chart.timezone_offset = timezone_offset
|
|
162
155
|
chart.settings = settings
|
|
163
|
-
chart.
|
|
156
|
+
chart.atlassian_document_format = project_config.atlassian_document_format
|
|
164
157
|
|
|
165
158
|
chart.all_boards = project_config.all_boards
|
|
166
159
|
chart.board_id = find_board_id
|
|
167
160
|
chart.holiday_dates = project_config.exporter.holiday_dates
|
|
161
|
+
chart.fix_versions = project_config.fix_versions
|
|
168
162
|
|
|
169
163
|
time_range = @file_config.project_config.time_range
|
|
170
164
|
chart.date_range = time_range.begin.to_date..time_range.end.to_date
|
|
@@ -173,6 +167,7 @@ class HtmlReportConfig
|
|
|
173
167
|
after_init_block&.call chart
|
|
174
168
|
|
|
175
169
|
@charts << chart
|
|
170
|
+
chart.before_run
|
|
176
171
|
html chart.run
|
|
177
172
|
end
|
|
178
173
|
|