jirametrics 2.20 → 2.22
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/lib/jirametrics/aging_work_bar_chart.rb +176 -134
- data/lib/jirametrics/bar_chart_range.rb +17 -0
- data/lib/jirametrics/board.rb +4 -0
- data/lib/jirametrics/board_config.rb +2 -1
- data/lib/jirametrics/change_item.rb +10 -3
- data/lib/jirametrics/chart_base.rb +31 -0
- data/lib/jirametrics/cycletime_config.rb +4 -5
- data/lib/jirametrics/cycletime_scatterplot.rb +36 -17
- data/lib/jirametrics/daily_wip_by_age_chart.rb +3 -4
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +13 -3
- data/lib/jirametrics/daily_wip_chart.rb +1 -1
- data/lib/jirametrics/data_quality_report.rb +2 -0
- data/lib/jirametrics/exporter.rb +4 -2
- data/lib/jirametrics/fix_version.rb +13 -0
- data/lib/jirametrics/groupable_issue_chart.rb +7 -1
- data/lib/jirametrics/html/aging_work_bar_chart.erb +2 -1
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +2 -0
- data/lib/jirametrics/html/aging_work_table.erb +2 -0
- data/lib/jirametrics/html/cycletime_histogram.erb +2 -0
- data/lib/jirametrics/html/cycletime_scatterplot.erb +6 -6
- data/lib/jirametrics/html/daily_wip_chart.erb +2 -0
- data/lib/jirametrics/html/estimate_accuracy_chart.erb +2 -0
- data/lib/jirametrics/html/expedited_chart.erb +3 -1
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +2 -0
- data/lib/jirametrics/html/index.css +17 -0
- data/lib/jirametrics/html/index.erb +1 -1
- data/lib/jirametrics/html/index.js +24 -0
- data/lib/jirametrics/html/sprint_burndown.erb +6 -0
- data/lib/jirametrics/html/throughput_chart.erb +2 -2
- data/lib/jirametrics/html_generator.rb +31 -0
- data/lib/jirametrics/html_report_config.rb +5 -24
- data/lib/jirametrics/issue.rb +97 -4
- data/lib/jirametrics/jira_gateway.rb +1 -1
- data/lib/jirametrics/project_config.rb +12 -2
- data/lib/jirametrics/raw_javascript.rb +13 -0
- data/lib/jirametrics/sprint.rb +12 -0
- data/lib/jirametrics/sprint_burndown.rb +6 -2
- data/lib/jirametrics/stitcher.rb +75 -0
- data/lib/jirametrics.rb +8 -1
- metadata +5 -1
|
@@ -51,8 +51,6 @@ class DailyWipByAgeChart < DailyWipChart
|
|
|
51
51
|
def default_grouping_rules issue:, rules:
|
|
52
52
|
started, stopped = issue.board.cycletime.started_stopped_dates(issue)
|
|
53
53
|
|
|
54
|
-
rules.issue_hint = "(age: #{label_days (rules.current_date - started + 1).to_i})" if started
|
|
55
|
-
|
|
56
54
|
if stopped && started.nil? # We can't tell when it started
|
|
57
55
|
@has_completed_but_not_started = true
|
|
58
56
|
not_started stopped: stopped, rules: rules, created: issue.created.to_date
|
|
@@ -72,7 +70,7 @@ class DailyWipByAgeChart < DailyWipChart
|
|
|
72
70
|
rules.label = 'Start date unknown'
|
|
73
71
|
rules.color = '--body-background'
|
|
74
72
|
rules.group_priority = 11
|
|
75
|
-
created_days = rules.current_date - created
|
|
73
|
+
created_days = rules.current_date - created
|
|
76
74
|
rules.issue_hint = "(created: #{label_days created_days.to_i} earlier, stopped on #{stopped})"
|
|
77
75
|
end
|
|
78
76
|
end
|
|
@@ -84,7 +82,8 @@ class DailyWipByAgeChart < DailyWipChart
|
|
|
84
82
|
end
|
|
85
83
|
|
|
86
84
|
def group_by_age started:, rules:
|
|
87
|
-
age = rules.current_date - started + 1
|
|
85
|
+
age = (rules.current_date - started).to_i + 1
|
|
86
|
+
rules.issue_hint = "(age: #{label_days age})"
|
|
88
87
|
|
|
89
88
|
case age
|
|
90
89
|
when 1
|
|
@@ -41,21 +41,30 @@ class DailyWipByBlockedStalledChart < DailyWipChart
|
|
|
41
41
|
def default_grouping_rules issue:, rules:
|
|
42
42
|
started, stopped = issue.board.cycletime.started_stopped_times(issue)
|
|
43
43
|
stopped_date = stopped&.to_date
|
|
44
|
+
started_date = started&.to_date
|
|
44
45
|
|
|
45
46
|
date = rules.current_date
|
|
46
47
|
change = issue.blocked_stalled_by_date(date_range: date..date, chart_end_time: time_range.end)[date]
|
|
47
|
-
|
|
48
48
|
stopped_today = stopped_date == rules.current_date
|
|
49
49
|
|
|
50
|
+
days = nil
|
|
51
|
+
if started_date && stopped_date
|
|
52
|
+
days = (stopped_date - started_date).to_i + 1 # cycletime
|
|
53
|
+
elsif started_date
|
|
54
|
+
days = (time_range.end.to_date - started_date).to_i + 1 # age
|
|
55
|
+
end
|
|
56
|
+
|
|
50
57
|
if stopped_today && started.nil?
|
|
51
58
|
@has_completed_but_not_started = true
|
|
52
59
|
rules.label = 'Completed but not started'
|
|
53
60
|
rules.color = '--wip-chart-completed-but-not-started-color'
|
|
54
61
|
rules.group_priority = -1
|
|
62
|
+
rules.issue_hint = '(Cycle time: Unknown)'
|
|
55
63
|
elsif stopped_today
|
|
56
64
|
rules.label = 'Completed'
|
|
57
65
|
rules.color = '--wip-chart-completed-color'
|
|
58
66
|
rules.group_priority = -2
|
|
67
|
+
rules.issue_hint = "(Cycle time: #{label_days days})"
|
|
59
68
|
elsif started.nil?
|
|
60
69
|
rules.label = 'Start date unknown'
|
|
61
70
|
rules.color = '--body-background'
|
|
@@ -64,16 +73,17 @@ class DailyWipByBlockedStalledChart < DailyWipChart
|
|
|
64
73
|
rules.label = 'Blocked'
|
|
65
74
|
rules.color = '--blocked-color'
|
|
66
75
|
rules.group_priority = 1
|
|
67
|
-
rules.issue_hint = "(#{change.reasons})"
|
|
76
|
+
rules.issue_hint = "(Age: #{label_days days}, #{change.reasons})"
|
|
68
77
|
elsif change&.stalled?
|
|
69
78
|
rules.label = 'Stalled'
|
|
70
79
|
rules.color = '--stalled-color'
|
|
71
80
|
rules.group_priority = 2
|
|
72
|
-
rules.issue_hint = "(#{change.reasons})"
|
|
81
|
+
rules.issue_hint = "(Age: #{label_days days}, #{change.reasons})"
|
|
73
82
|
else
|
|
74
83
|
rules.label = 'Active'
|
|
75
84
|
rules.color = '--wip-chart-active-color'
|
|
76
85
|
rules.group_priority = 3
|
|
86
|
+
rules.issue_hint = "(Age: #{label_days days})"
|
|
77
87
|
end
|
|
78
88
|
end
|
|
79
89
|
end
|
|
@@ -66,7 +66,7 @@ class DailyWipChart < ChartBase
|
|
|
66
66
|
hash = {}
|
|
67
67
|
|
|
68
68
|
@issues.each do |issue|
|
|
69
|
-
start, stop = issue.
|
|
69
|
+
start, stop = cycletime_for_issue(issue).started_stopped_dates(issue)
|
|
70
70
|
next if start.nil? && stop.nil?
|
|
71
71
|
|
|
72
72
|
# If it stopped but never started then assume it started at creation so the data points
|
|
@@ -266,6 +266,8 @@ class DataQualityReport < ChartBase
|
|
|
266
266
|
|
|
267
267
|
def scan_for_items_blocked_on_closed_tickets entry:
|
|
268
268
|
entry.issue.issue_links.each do |link|
|
|
269
|
+
next unless settings['blocked_link_text'].include?(link.label)
|
|
270
|
+
|
|
269
271
|
this_active = !entry.stopped
|
|
270
272
|
other_active = !link.other_issue.board.cycletime.started_stopped_times(link.other_issue).last
|
|
271
273
|
next unless this_active && !other_active
|
data/lib/jirametrics/exporter.rb
CHANGED
|
@@ -50,8 +50,6 @@ class Exporter
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
project.download_config.run
|
|
53
|
-
# load_jira_config(download_config.project_config.jira_config)
|
|
54
|
-
# @ignore_ssl_errors = download_config.project_config.settings['ignore_ssl_errors']
|
|
55
53
|
gateway = JiraGateway.new(
|
|
56
54
|
file_system: file_system, jira_config: project.jira_config, settings: project.settings
|
|
57
55
|
)
|
|
@@ -90,6 +88,10 @@ class Exporter
|
|
|
90
88
|
end
|
|
91
89
|
end
|
|
92
90
|
|
|
91
|
+
def stitch stitch_file
|
|
92
|
+
Stitcher.new(file_system: file_system).run(stitch_file: stitch_file)
|
|
93
|
+
end
|
|
94
|
+
|
|
93
95
|
def each_project_config name_filter:
|
|
94
96
|
@project_configs.each do |project|
|
|
95
97
|
yield project if project.name.nil? || File.fnmatch(name_filter, project.name)
|
|
@@ -11,11 +11,24 @@ class FixVersion
|
|
|
11
11
|
@raw['name']
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
def description
|
|
15
|
+
@raw['description']
|
|
16
|
+
end
|
|
17
|
+
|
|
14
18
|
def id
|
|
15
19
|
@raw['id'].to_i
|
|
16
20
|
end
|
|
17
21
|
|
|
22
|
+
def release_date
|
|
23
|
+
text = @raw['releaseDate']
|
|
24
|
+
text.nil? ? nil : Date.parse(text)
|
|
25
|
+
end
|
|
26
|
+
|
|
18
27
|
def released?
|
|
19
28
|
@raw['released']
|
|
20
29
|
end
|
|
30
|
+
|
|
31
|
+
def archived?
|
|
32
|
+
@raw['archived']
|
|
33
|
+
end
|
|
21
34
|
end
|
|
@@ -15,14 +15,20 @@ module GroupableIssueChart
|
|
|
15
15
|
|
|
16
16
|
def group_issues completed_issues
|
|
17
17
|
result = {}
|
|
18
|
+
ignored_issues = []
|
|
18
19
|
completed_issues.each do |issue|
|
|
19
20
|
rules = GroupingRules.new
|
|
20
21
|
@group_by_block.call(issue, rules)
|
|
21
|
-
|
|
22
|
+
if rules.ignored?
|
|
23
|
+
ignored_issues << issue
|
|
24
|
+
next
|
|
25
|
+
end
|
|
22
26
|
|
|
23
27
|
(result[rules] ||= []) << issue
|
|
24
28
|
end
|
|
25
29
|
|
|
30
|
+
completed_issues.reject! { |issue| ignored_issues.include? issue }
|
|
31
|
+
|
|
26
32
|
result.each_key do |rules|
|
|
27
33
|
rules.color = random_color if rules.color.nil?
|
|
28
34
|
end
|
|
@@ -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>
|
|
@@ -66,4 +67,4 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
66
67
|
}
|
|
67
68
|
});
|
|
68
69
|
</script>
|
|
69
|
-
|
|
70
|
+
<%= seam_end %>
|
|
@@ -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>
|
|
@@ -73,3 +74,4 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
|
|
|
73
74
|
}
|
|
74
75
|
});
|
|
75
76
|
</script>
|
|
77
|
+
<%= seam_end %>
|
|
@@ -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>
|
|
@@ -119,3 +120,4 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
119
120
|
}
|
|
120
121
|
});
|
|
121
122
|
</script>
|
|
123
|
+
<%= seam_end %>
|
|
@@ -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,15 +11,14 @@ 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 %>
|
|
@@ -29,13 +29,12 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
29
29
|
y: {
|
|
30
30
|
scaleLabel: {
|
|
31
31
|
display: true,
|
|
32
|
-
labelString: 'Days',
|
|
33
32
|
min: 0,
|
|
34
|
-
max: <%= @
|
|
33
|
+
max: <%= @highest_y_value %>
|
|
35
34
|
},
|
|
36
35
|
title: {
|
|
37
36
|
display: true,
|
|
38
|
-
text: '
|
|
37
|
+
text: '<%= y_axis_heading %>'
|
|
39
38
|
},
|
|
40
39
|
grid: {
|
|
41
40
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
@@ -98,3 +97,4 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
98
97
|
}
|
|
99
98
|
});
|
|
100
99
|
</script>
|
|
100
|
+
<%= seam_end %>
|
|
@@ -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>
|
|
@@ -65,3 +66,4 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
65
66
|
}
|
|
66
67
|
});
|
|
67
68
|
</script>
|
|
69
|
+
<%= seam_end %>
|
|
@@ -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>
|
|
@@ -60,3 +61,4 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
60
61
|
}
|
|
61
62
|
});
|
|
62
63
|
</script>
|
|
64
|
+
<%= seam_end %>
|
|
@@ -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>
|
|
@@ -61,4 +62,5 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
});
|
|
64
|
-
</script>
|
|
65
|
+
</script>
|
|
66
|
+
<%= seam_end %>
|
|
@@ -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>
|
|
@@ -83,3 +84,4 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
83
84
|
}
|
|
84
85
|
});
|
|
85
86
|
</script>
|
|
87
|
+
<%= seam_start %>
|
|
@@ -67,10 +67,21 @@
|
|
|
67
67
|
--sprint-burndown-sprint-color-4: red;
|
|
68
68
|
--sprint-burndown-sprint-color-5: brown;
|
|
69
69
|
|
|
70
|
+
--sprint-color: lightblue;
|
|
71
|
+
|
|
70
72
|
--daily-view-selected-issue-background: lightgray;
|
|
71
73
|
--daily-view-issue-border: green;
|
|
72
74
|
--daily-view-selected-issue-border: red;
|
|
73
75
|
|
|
76
|
+
/* The first five are the standard priorities that Jira creates by default. */
|
|
77
|
+
--priority-color-highest: #dc2626; /* red-600 - urgent red */
|
|
78
|
+
--priority-color-high: #ea580c; /* orange-600 - warning orange */
|
|
79
|
+
--priority-color-medium: #9ca3af; /* gray-400 - neutral light gray */
|
|
80
|
+
--priority-color-low: #0891b2; /* cyan-600 - calm blue */
|
|
81
|
+
--priority-color-lowest: #64748b; /* slate-500 - muted slate */
|
|
82
|
+
/* Then here are some values we've seen in multiple instances. */
|
|
83
|
+
--priority-color-notset: gray;
|
|
84
|
+
--priority-color-critical: red;
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
body {
|
|
@@ -237,6 +248,12 @@ div.child_issue {
|
|
|
237
248
|
--wip-chart-duration-more-than-four-weeks-color: #8e0000;
|
|
238
249
|
|
|
239
250
|
--daily-view-selected-issue-background: #474747;
|
|
251
|
+
|
|
252
|
+
--priority-color-highest: #ef4444; /* red-500 - bright urgent red */
|
|
253
|
+
--priority-color-high: #f97316; /* orange-500 - bright orange */
|
|
254
|
+
--priority-color-medium: #9ca3af; /* gray-400 - neutral light gray */
|
|
255
|
+
--priority-color-low: #06b6d4; /* cyan-500 - bright calm blue */
|
|
256
|
+
--priority-color-lowest: #94a3b8; /* slate-400 - muted light slate */
|
|
240
257
|
}
|
|
241
258
|
|
|
242
259
|
a[href] {
|
|
@@ -23,6 +23,6 @@
|
|
|
23
23
|
</div>
|
|
24
24
|
</noscript>
|
|
25
25
|
<%= "\n" + @sections.collect { |text, type| text if type == :header }.compact.join("\n\n") %>
|
|
26
|
-
<%= "\n" + @sections.collect { |text, type| text if type
|
|
26
|
+
<%= "\n" + @sections.collect { |text, type| text if type != :header }.compact.join("\n\n") %>
|
|
27
27
|
</body>
|
|
28
28
|
</html>
|
|
@@ -88,3 +88,27 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
88
88
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
|
|
89
89
|
location.reload()
|
|
90
90
|
})
|
|
91
|
+
|
|
92
|
+
// Draw a diagonal pattern to highlight sections of a bar chart. Based on code found at:
|
|
93
|
+
// https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns
|
|
94
|
+
function createDiagonalPattern(color = 'black') {
|
|
95
|
+
// create a 5x5 px canvas for the pattern's base shape
|
|
96
|
+
let shape = document.createElement('canvas')
|
|
97
|
+
shape.width = 5
|
|
98
|
+
shape.height = 5
|
|
99
|
+
// get the context for drawing
|
|
100
|
+
let c = shape.getContext('2d')
|
|
101
|
+
// draw 1st line of the shape
|
|
102
|
+
c.strokeStyle = color
|
|
103
|
+
c.beginPath()
|
|
104
|
+
c.moveTo(1, 0)
|
|
105
|
+
c.lineTo(5, 4)
|
|
106
|
+
c.stroke()
|
|
107
|
+
// draw 2nd line of the shape
|
|
108
|
+
c.beginPath()
|
|
109
|
+
c.moveTo(0, 4)
|
|
110
|
+
c.lineTo(1, 5)
|
|
111
|
+
c.stroke()
|
|
112
|
+
// create the pattern from the shape
|
|
113
|
+
return c.createPattern(shape, 'repeat')
|
|
114
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<h2>Burndown by <%= y_axis_title %></h2>
|
|
2
2
|
|
|
3
|
+
<%= seam_start %>
|
|
3
4
|
<div class="chart">
|
|
4
5
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
5
6
|
</div>
|
|
@@ -63,6 +64,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
63
64
|
}
|
|
64
65
|
});
|
|
65
66
|
</script>
|
|
67
|
+
<%= seam_end %>
|
|
66
68
|
|
|
67
69
|
<%
|
|
68
70
|
link_id = next_id
|
|
@@ -71,9 +73,11 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
71
73
|
<section>
|
|
72
74
|
<div class='foldable startFolded'>Show statistics</div>
|
|
73
75
|
<div id="<%= issues_id %>">
|
|
76
|
+
<%= seam_start 'stats_table' %>
|
|
74
77
|
<table class='standard' style="margin-left: 1em;">
|
|
75
78
|
<thead>
|
|
76
79
|
<th>Sprint</th>
|
|
80
|
+
<th>Length</th>
|
|
77
81
|
<th>State</th>
|
|
78
82
|
<th>Started</th>
|
|
79
83
|
<th>Completed</th>
|
|
@@ -86,6 +90,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
86
90
|
<% @summary_stats.keys.sort_by(&:start_time).each do |sprint| %>
|
|
87
91
|
<tr>
|
|
88
92
|
<td><%= sprint.name %></td>
|
|
93
|
+
<td><%= sprint.day_count %></td>
|
|
89
94
|
<td><%= sprint.raw['state'] %></td>
|
|
90
95
|
<% stats = @summary_stats[sprint] %>
|
|
91
96
|
<td><%= stats.started %></td>
|
|
@@ -102,6 +107,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
102
107
|
<% end %>
|
|
103
108
|
</tbody>
|
|
104
109
|
</table>
|
|
110
|
+
<%= seam_end 'stats_table' %>
|
|
105
111
|
|
|
106
112
|
<p>Legend:
|
|
107
113
|
<ul>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
<%= seam_start %>
|
|
2
2
|
<div class="chart">
|
|
3
3
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
4
4
|
</div>
|
|
@@ -59,4 +59,4 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
59
59
|
}
|
|
60
60
|
});
|
|
61
61
|
</script>
|
|
62
|
-
|
|
62
|
+
<%= seam_end %>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class HtmlGenerator
|
|
4
|
+
attr_accessor :file_system, :settings
|
|
5
|
+
|
|
6
|
+
def create_html output_filename:, settings:
|
|
7
|
+
@settings = settings
|
|
8
|
+
html_directory = "#{Pathname.new(File.realpath(__FILE__)).dirname}/html"
|
|
9
|
+
css = load_css html_directory: html_directory
|
|
10
|
+
javascript = file_system.load(File.join(html_directory, 'index.js'))
|
|
11
|
+
erb = ERB.new file_system.load(File.join(html_directory, 'index.erb'))
|
|
12
|
+
file_system.save_file content: erb.result(binding), filename: output_filename
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def load_css html_directory:
|
|
16
|
+
base_css_filename = File.join(html_directory, 'index.css')
|
|
17
|
+
base_css = file_system.load(base_css_filename)
|
|
18
|
+
|
|
19
|
+
extra_css_filename = settings['include_css']
|
|
20
|
+
if extra_css_filename
|
|
21
|
+
if File.exist?(extra_css_filename)
|
|
22
|
+
base_css << "\n\n" << file_system.load(extra_css_filename)
|
|
23
|
+
log("Loaded CSS: #{extra_css_filename}")
|
|
24
|
+
else
|
|
25
|
+
log("Unable to find specified CSS file: #{extra_css_filename}")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
base_css
|
|
30
|
+
end
|
|
31
|
+
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
|
|
@@ -52,7 +52,8 @@ class HtmlReportConfig
|
|
|
52
52
|
raise 'Multiple cycletimes not supported' if board.cycletime
|
|
53
53
|
|
|
54
54
|
board.cycletime = CycleTimeConfig.new(
|
|
55
|
-
|
|
55
|
+
possible_statuses: file_config.project_config, label: label, block: block,
|
|
56
|
+
file_system: file_system, settings: settings
|
|
56
57
|
)
|
|
57
58
|
end
|
|
58
59
|
end
|
|
@@ -72,11 +73,7 @@ class HtmlReportConfig
|
|
|
72
73
|
|
|
73
74
|
html create_footer
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
css = load_css html_directory: html_directory
|
|
77
|
-
javascript = file_system.load(File.join(html_directory, 'index.js'))
|
|
78
|
-
erb = ERB.new file_system.load(File.join(html_directory, 'index.erb'))
|
|
79
|
-
file_system.save_file content: erb.result(binding), filename: @file_config.output_filename
|
|
76
|
+
create_html output_filename: @file_config.output_filename, settings: settings
|
|
80
77
|
end
|
|
81
78
|
|
|
82
79
|
def file_system
|
|
@@ -87,23 +84,6 @@ class HtmlReportConfig
|
|
|
87
84
|
file_system.log message
|
|
88
85
|
end
|
|
89
86
|
|
|
90
|
-
def load_css html_directory:
|
|
91
|
-
base_css_filename = File.join(html_directory, 'index.css')
|
|
92
|
-
base_css = file_system.load(base_css_filename)
|
|
93
|
-
|
|
94
|
-
extra_css_filename = settings['include_css']
|
|
95
|
-
if extra_css_filename
|
|
96
|
-
if File.exist?(extra_css_filename)
|
|
97
|
-
base_css << "\n\n" << file_system.load(extra_css_filename)
|
|
98
|
-
log("Loaded CSS: #{extra_css_filename}")
|
|
99
|
-
else
|
|
100
|
-
log("Unable to find specified CSS file: #{extra_css_filename}")
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
base_css
|
|
105
|
-
end
|
|
106
|
-
|
|
107
87
|
def board_id id
|
|
108
88
|
@board_id = id
|
|
109
89
|
end
|
|
@@ -175,6 +155,7 @@ class HtmlReportConfig
|
|
|
175
155
|
after_init_block&.call chart
|
|
176
156
|
|
|
177
157
|
@charts << chart
|
|
158
|
+
chart.before_run
|
|
178
159
|
html chart.run
|
|
179
160
|
end
|
|
180
161
|
|
data/lib/jirametrics/issue.rb
CHANGED
|
@@ -212,8 +212,87 @@ class Issue
|
|
|
212
212
|
first_time_in_status(*board.visible_columns.collect(&:status_ids).flatten)
|
|
213
213
|
end
|
|
214
214
|
|
|
215
|
+
# If this issue will ever be in an active sprint then return the time that it
|
|
216
|
+
# was first added to that sprint, whether or not the sprint was active at that
|
|
217
|
+
# time. Although it seems like an odd thing to calculate, it's a reasonable proxy
|
|
218
|
+
# for 'ready' in cases where the team doesn't have an explicit 'ready' status.
|
|
219
|
+
# You'd be better off with an explicit 'ready' but sometimes that's not an option.
|
|
220
|
+
def first_time_added_to_active_sprint
|
|
221
|
+
unless board.scrum?
|
|
222
|
+
raise 'first_time_added_to_active_sprint() can only be used with Scrum boards: ' \
|
|
223
|
+
"issue=#{key}, board=#{board.inspect}"
|
|
224
|
+
end
|
|
225
|
+
data_clazz = Struct.new(:sprint_id, :sprint_start, :sprint_stop, :change)
|
|
226
|
+
|
|
227
|
+
matching_changes = []
|
|
228
|
+
all_datas = []
|
|
229
|
+
|
|
230
|
+
@changes.each do |change|
|
|
231
|
+
next unless change.sprint?
|
|
232
|
+
|
|
233
|
+
added_sprint_ids = change.value_id - change.old_value_id
|
|
234
|
+
added_sprint_ids.each do |id|
|
|
235
|
+
data = data_clazz.new
|
|
236
|
+
data.sprint_id = id
|
|
237
|
+
data.change = change
|
|
238
|
+
data.sprint_start, data.sprint_stop = find_sprint_start_end(sprint_id: id, change: change)
|
|
239
|
+
all_datas << data
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
removed_sprint_ids = change.old_value_id - change.value_id
|
|
243
|
+
removed_sprint_ids.each do |id|
|
|
244
|
+
data = all_datas.find { |d| d.sprint_id == id }
|
|
245
|
+
# It's possible for an issue to be created inside a sprint and therefore for
|
|
246
|
+
# that add-to-sprint not show in the history.
|
|
247
|
+
next unless data
|
|
248
|
+
|
|
249
|
+
all_datas.delete(data)
|
|
250
|
+
next if data.sprint_start.nil? || data.sprint_start >= change.time
|
|
251
|
+
|
|
252
|
+
matching_changes << data.change
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# There can't be any more removes so whatever is left is a valid option
|
|
257
|
+
# Now all we care about is if the sprint has started.
|
|
258
|
+
all_datas.each do |data|
|
|
259
|
+
matching_changes << data.change if data.sprint_start
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
matching_changes.min_by(&:time)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def find_sprint_start_end sprint_id:, change:
|
|
266
|
+
# There are two different places that sprint data could be found. In theory all
|
|
267
|
+
# sprints would be found in both places. In practice, sometimes what we need is
|
|
268
|
+
# in one or the other but not both.
|
|
269
|
+
|
|
270
|
+
# First look in the actual sprints json. If any issues are in this sprint then it should
|
|
271
|
+
# be here.
|
|
272
|
+
sprint = board.sprints.find { |s| s.id == sprint_id }
|
|
273
|
+
return [sprint.start_time, sprint.completed_time] if sprint
|
|
274
|
+
|
|
275
|
+
# Then look at the sprints inside the issue. Even though the field id may be specified,
|
|
276
|
+
# that custom field may not be present. This happens if it was in that sprint but was
|
|
277
|
+
# then removed, whether or not that sprint had ever started.
|
|
278
|
+
sprint_data = raw['fields'][change.field_id]&.find { |sd| sd['id'].to_i == sprint_id }
|
|
279
|
+
if sprint_data
|
|
280
|
+
start = parse_time(sprint_data['startDate'])
|
|
281
|
+
stop = parse_time(sprint_data['completeDate'])
|
|
282
|
+
return [start, stop]
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# If we got this far then the sprint can't be found anywhere, so we pretend that it never
|
|
286
|
+
# started. Is this guaranteed to be true? No. In theory if all issues were removed from
|
|
287
|
+
# an active sprint then it would also disappear, even though it had started. Nothing we
|
|
288
|
+
# can do to detect that edge-case though.
|
|
289
|
+
[nil, nil]
|
|
290
|
+
end
|
|
291
|
+
|
|
215
292
|
def parse_time text
|
|
216
|
-
if text.
|
|
293
|
+
if text.nil?
|
|
294
|
+
nil
|
|
295
|
+
elsif text.is_a? String
|
|
217
296
|
Time.parse(text).getlocal(@timezone_offset)
|
|
218
297
|
else
|
|
219
298
|
Time.at(text / 1000).getlocal(@timezone_offset)
|
|
@@ -225,6 +304,10 @@ class Issue
|
|
|
225
304
|
parse_time @raw['fields']['created'] if @raw['fields']['created']
|
|
226
305
|
end
|
|
227
306
|
|
|
307
|
+
def time_created
|
|
308
|
+
@changes.first
|
|
309
|
+
end
|
|
310
|
+
|
|
228
311
|
def updated
|
|
229
312
|
parse_time @raw['fields']['updated']
|
|
230
313
|
end
|
|
@@ -305,9 +388,7 @@ class Issue
|
|
|
305
388
|
results
|
|
306
389
|
end
|
|
307
390
|
|
|
308
|
-
def
|
|
309
|
-
settings ||= @board.project_config.settings
|
|
310
|
-
|
|
391
|
+
def blocked_stalled_statuses settings
|
|
311
392
|
blocked_statuses = settings['blocked_statuses']
|
|
312
393
|
stalled_statuses = settings['stalled_statuses']
|
|
313
394
|
unless blocked_statuses.is_a?(Array) && stalled_statuses.is_a?(Array)
|
|
@@ -315,6 +396,14 @@ class Issue
|
|
|
315
396
|
"stalled_statuses(#{stalled_statuses.inspect}) must both be arrays"
|
|
316
397
|
end
|
|
317
398
|
|
|
399
|
+
[blocked_statuses, stalled_statuses]
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def blocked_stalled_changes end_time:, settings: nil
|
|
403
|
+
settings ||= @board.project_config.settings
|
|
404
|
+
|
|
405
|
+
blocked_statuses, stalled_statuses = blocked_stalled_statuses(settings)
|
|
406
|
+
|
|
318
407
|
blocked_link_texts = settings['blocked_link_text']
|
|
319
408
|
stalled_threshold = settings['stalled_threshold_days']
|
|
320
409
|
flagged_means_blocked = !!settings['flagged_means_blocked'] # rubocop:disable Style/DoubleNegation
|
|
@@ -710,6 +799,10 @@ class Issue
|
|
|
710
799
|
board.sprints.select { |s| sprint_ids.include? s.id }
|
|
711
800
|
end
|
|
712
801
|
|
|
802
|
+
def started_sprints
|
|
803
|
+
sprints.reject { |sprint| sprint.future? }
|
|
804
|
+
end
|
|
805
|
+
|
|
713
806
|
def compact_text text, max: 60
|
|
714
807
|
return '' if text.nil?
|
|
715
808
|
|
|
@@ -67,7 +67,7 @@ class JiraGateway
|
|
|
67
67
|
|
|
68
68
|
def sanitize_message message
|
|
69
69
|
token = @jira_api_token || @jira_personal_access_token
|
|
70
|
-
|
|
70
|
+
return message unless token # cookie based authentication
|
|
71
71
|
|
|
72
72
|
message.gsub(token, '[API_TOKEN]')
|
|
73
73
|
end
|