jirametrics 1.0.0

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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/bin/jirametrics +4 -0
  3. data/lib/jirametrics/aggregate_config.rb +89 -0
  4. data/lib/jirametrics/aging_work_bar_chart.rb +235 -0
  5. data/lib/jirametrics/aging_work_in_progress_chart.rb +148 -0
  6. data/lib/jirametrics/aging_work_table.rb +149 -0
  7. data/lib/jirametrics/anonymizer.rb +186 -0
  8. data/lib/jirametrics/blocked_stalled_change.rb +43 -0
  9. data/lib/jirametrics/board.rb +85 -0
  10. data/lib/jirametrics/board_column.rb +14 -0
  11. data/lib/jirametrics/board_config.rb +31 -0
  12. data/lib/jirametrics/change_item.rb +80 -0
  13. data/lib/jirametrics/chart_base.rb +239 -0
  14. data/lib/jirametrics/columns_config.rb +42 -0
  15. data/lib/jirametrics/cycletime_config.rb +69 -0
  16. data/lib/jirametrics/cycletime_histogram.rb +74 -0
  17. data/lib/jirametrics/cycletime_scatterplot.rb +128 -0
  18. data/lib/jirametrics/daily_wip_by_age_chart.rb +88 -0
  19. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +77 -0
  20. data/lib/jirametrics/daily_wip_chart.rb +123 -0
  21. data/lib/jirametrics/data_quality_report.rb +278 -0
  22. data/lib/jirametrics/dependency_chart.rb +217 -0
  23. data/lib/jirametrics/discard_changes_before.rb +37 -0
  24. data/lib/jirametrics/download_config.rb +41 -0
  25. data/lib/jirametrics/downloader.rb +337 -0
  26. data/lib/jirametrics/examples/aggregated_project.rb +36 -0
  27. data/lib/jirametrics/examples/standard_project.rb +111 -0
  28. data/lib/jirametrics/expedited_chart.rb +169 -0
  29. data/lib/jirametrics/experimental/generator.rb +209 -0
  30. data/lib/jirametrics/experimental/info.rb +77 -0
  31. data/lib/jirametrics/exporter.rb +127 -0
  32. data/lib/jirametrics/file_config.rb +119 -0
  33. data/lib/jirametrics/fix_version.rb +21 -0
  34. data/lib/jirametrics/groupable_issue_chart.rb +44 -0
  35. data/lib/jirametrics/grouping_rules.rb +13 -0
  36. data/lib/jirametrics/hierarchy_table.rb +31 -0
  37. data/lib/jirametrics/html/aging_work_bar_chart.erb +72 -0
  38. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +52 -0
  39. data/lib/jirametrics/html/aging_work_table.erb +60 -0
  40. data/lib/jirametrics/html/collapsible_issues_panel.erb +32 -0
  41. data/lib/jirametrics/html/cycletime_histogram.erb +41 -0
  42. data/lib/jirametrics/html/cycletime_scatterplot.erb +103 -0
  43. data/lib/jirametrics/html/daily_wip_chart.erb +63 -0
  44. data/lib/jirametrics/html/data_quality_report.erb +126 -0
  45. data/lib/jirametrics/html/expedited_chart.erb +67 -0
  46. data/lib/jirametrics/html/hierarchy_table.erb +29 -0
  47. data/lib/jirametrics/html/index.erb +66 -0
  48. data/lib/jirametrics/html/sprint_burndown.erb +116 -0
  49. data/lib/jirametrics/html/story_point_accuracy_chart.erb +57 -0
  50. data/lib/jirametrics/html/throughput_chart.erb +65 -0
  51. data/lib/jirametrics/html_report_config.rb +217 -0
  52. data/lib/jirametrics/issue.rb +521 -0
  53. data/lib/jirametrics/issue_link.rb +60 -0
  54. data/lib/jirametrics/json_file_loader.rb +9 -0
  55. data/lib/jirametrics/project_config.rb +442 -0
  56. data/lib/jirametrics/rules.rb +34 -0
  57. data/lib/jirametrics/self_or_issue_dispatcher.rb +15 -0
  58. data/lib/jirametrics/sprint.rb +43 -0
  59. data/lib/jirametrics/sprint_burndown.rb +335 -0
  60. data/lib/jirametrics/sprint_issue_change_data.rb +31 -0
  61. data/lib/jirametrics/status.rb +26 -0
  62. data/lib/jirametrics/status_collection.rb +67 -0
  63. data/lib/jirametrics/story_point_accuracy_chart.rb +139 -0
  64. data/lib/jirametrics/throughput_chart.rb +91 -0
  65. data/lib/jirametrics/tree_organizer.rb +96 -0
  66. data/lib/jirametrics/trend_line_calculator.rb +74 -0
  67. data/lib/jirametrics.rb +85 -0
  68. metadata +167 -0
@@ -0,0 +1,66 @@
1
+ <html>
2
+ <head>
3
+ <meta charset="UTF-8">
4
+ <script src="https://cdn.jsdelivr.net/npm/moment@2.29.1/moment.js"></script>
5
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
6
+ <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@^1"></script>
7
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-annotation/1.2.2/chartjs-plugin-annotation.min.js" integrity="sha512-HycvvBSFvDEVyJ0tjE2rPmymkt6XqsP/Zo96XgLRjXwn6SecQqsn+6V/7KYev66OshZZ9+f9AttCGmYqmzytiw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
8
+ <script type="text/javascript">
9
+ function expand_collapse(link_id, issues_id) {
10
+ link_text = document.getElementById(link_id).textContent
11
+ if( link_text == 'Show details') {
12
+ document.getElementById(link_id).textContent = 'Hide details'
13
+ document.getElementById(issues_id).style.display = 'block'
14
+ }
15
+ else {
16
+ document.getElementById(link_id).textContent = 'Show details'
17
+ document.getElementById(issues_id).style.display = 'none'
18
+ }
19
+ }
20
+ </script>
21
+ <style>
22
+ h1 {
23
+ border: 1px solid black;
24
+ background: lightgray;
25
+ padding-left: 0.2em;
26
+ }
27
+ dl, dd, dt {
28
+ padding: 0;
29
+ margin: 0;
30
+ }
31
+ dd {
32
+ margin-bottom: 0.4em;
33
+ }
34
+ span.highlight {
35
+ background: #FDD5B1;
36
+ }
37
+ a.issue_key {
38
+ white-space: nowrap;
39
+ }
40
+ table.standard thead tr th {
41
+ border-bottom: 1px solid gray;
42
+ position: sticky;
43
+ top: 0;
44
+ background: white;
45
+ }
46
+ table.standard tbody tr td {
47
+ padding-left: 0.5em;
48
+ padding-right: 0.5em;
49
+ vertical-align: top;
50
+ }
51
+ table.standard tbody tr:nth-child(odd){
52
+ background-color: #eee;
53
+ }
54
+ .quality_note_bullet {
55
+ color: red;
56
+ }
57
+ </style>
58
+ </head>
59
+ <body>
60
+ <div>
61
+ Page generated <%= (timezone_offset.nil? ? DateTime.now : DateTime.now.new_offset(timezone_offset)).strftime('%Y-%b-%d at %I:%M:%S%P') %>
62
+ </div>
63
+ <%= "\n" + @sections.collect { |text, type| text if type == :header }.compact.join("\n\n") %>
64
+ <%= "\n" + @sections.collect { |text, type| text if type == :body }.compact.join("\n\n") %>
65
+ </body>
66
+ </html>
@@ -0,0 +1,116 @@
1
+ <h2>Burndown by <%= y_axis_title %></h2>
2
+
3
+ <div>
4
+ <canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
5
+ </div>
6
+ <script>
7
+ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
8
+ type: 'scatter',
9
+ data: {
10
+ datasets: <%= JSON.generate(data_sets) %>
11
+ },
12
+ options: {
13
+ title: {
14
+ display: true,
15
+ text: "Sprint Burndown"
16
+ },
17
+ responsive: <%= canvas_responsive? %>, // If responsive is true then it fills the screen
18
+ scales: {
19
+ x: {
20
+ type: "time",
21
+ time: {
22
+ format: 'YYYY-MM-DD'
23
+ },
24
+ scaleLabel: {
25
+ display: true,
26
+ labelString: 'Date'
27
+ },
28
+ min: "<%= date_range.begin.to_s %>",
29
+ max: "<%= (date_range.end + 1).to_s %>"
30
+
31
+ },
32
+ y: {
33
+ scaleLabel: {
34
+ display: true,
35
+ labelString: 'Items remaining'
36
+ },
37
+ title: {
38
+ display: true,
39
+ text: "<%= y_axis_title %>"
40
+ },
41
+ min: 0.0
42
+ }
43
+ },
44
+ plugins: {
45
+ tooltip: {
46
+ callbacks: {
47
+ label: function(context) {
48
+ return context.dataset.data[context.dataIndex].title
49
+ }
50
+ }
51
+ },
52
+ annotation: {
53
+ annotations: {
54
+ <% holidays().each_with_index do |range, index| %>
55
+ holiday<%= index %>: {
56
+ drawTime: 'beforeDraw',
57
+ type: 'box',
58
+ xMin: '<%= range.begin %>T00:00:00',
59
+ xMax: '<%= range.end %>T23:59:59',
60
+ backgroundColor: '#F0F0F0',
61
+ borderColor: '#F0F0F0'
62
+ },
63
+ <% end %>
64
+ }
65
+ }
66
+ }
67
+ }
68
+ });
69
+ </script>
70
+
71
+ <%
72
+ link_id = next_id
73
+ issues_id = next_id
74
+ %>
75
+ [<a id='<%= link_id %>' href="#" onclick='expand_collapse("<%= link_id %>", "<%= issues_id %>"); return false;'>Show details</a>]
76
+ <div id="<%= issues_id %>" style="display: none;">
77
+ <table class='standard' style="margin-left: 1em;">
78
+ <thead>
79
+ <th>Sprint</th>
80
+ <th>State</th>
81
+ <th>Started</th>
82
+ <th>Completed</th>
83
+ <th>Added</th>
84
+ <th>Removed</th>
85
+ <th>Remaining</th>
86
+ <th>Note</th>
87
+ </thead>
88
+ <tbody>
89
+ <% @summary_stats.keys.sort_by(&:start_time).each do |sprint| %>
90
+ <tr>
91
+ <td><%= sprint.name %></td>
92
+ <td><%= sprint.raw['state'] %></td>
93
+ <% stats = @summary_stats[sprint] %>
94
+ <td><%= stats.started %></td>
95
+ <td><%= stats.completed %></td>
96
+ <td><%= stats.added %></td>
97
+ <td><%= stats.removed %></td>
98
+ <td><%= stats.remaining %></td>
99
+ <td>
100
+ <% if stats.points_values_changed %>
101
+ Points values changed mid-sprint. Numbers may not add up
102
+ <% end %>
103
+ </td>
104
+ </tr>
105
+ <% end %>
106
+ </tbody>
107
+ </table>
108
+
109
+ <p>Legend:
110
+ <ul>
111
+ <% legend.each do |line| %>
112
+ <li><%= line %></li>
113
+ <% end %>
114
+ </ul>
115
+ </p>
116
+ </div>
@@ -0,0 +1,57 @@
1
+ <div>
2
+ <canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
3
+ </div>
4
+ <script>
5
+ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
6
+ type: 'bubble',
7
+ data: {
8
+ datasets: <%= JSON.generate(data_sets) %>
9
+ },
10
+ options: {
11
+ title: {
12
+ display: true,
13
+ text: "Sprint Burndown"
14
+ },
15
+ responsive: <%= canvas_responsive? %>, // If responsive is true then it fills the screen
16
+ scales: {
17
+ x: {
18
+ type: "linear",
19
+ scaleLabel: {
20
+ display: true,
21
+ labelString: 'Date'
22
+ },
23
+ title: {
24
+ display: true,
25
+ text: "Cycletime (days)"
26
+ },
27
+ min: 0
28
+
29
+ },
30
+ y: {
31
+ type: "<%= @y_axis_type %>",
32
+ <% if @y_axis_sort_order %>
33
+ labels: ["","<%= @y_axis_sort_order.reverse.join('","')%>",""],
34
+ <% end %>
35
+ scaleLabel: {
36
+ display: true,
37
+ labelString: 'Items remaining'
38
+ },
39
+ title: {
40
+ display: true,
41
+ text: "<%= @y_axis_label %>"
42
+ },
43
+ min: 0.0
44
+ }
45
+ },
46
+ plugins: {
47
+ tooltip: {
48
+ callbacks: {
49
+ label: function(context) {
50
+ return context.dataset.data[context.dataIndex].title
51
+ }
52
+ }
53
+ },
54
+ }
55
+ }
56
+ });
57
+ </script>
@@ -0,0 +1,65 @@
1
+
2
+ <div>
3
+ <canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
4
+ </div>
5
+ <script>
6
+ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
7
+ type: 'scatter',
8
+ data: {
9
+ datasets: <%= JSON.generate(data_sets) %>
10
+ },
11
+ options: {
12
+ title: {
13
+ display: true,
14
+ text: "Throughput Chart"
15
+ },
16
+ responsive: <%= canvas_responsive? %>, // If responsive is true then it fills the screen
17
+ scales: {
18
+ x: {
19
+ type: "time",
20
+ time: {
21
+ format: 'YYYY-MM-DD'
22
+ },
23
+ scaleLabel: {
24
+ display: true,
25
+ labelString: 'Date Completed'
26
+ }
27
+ },
28
+ y: {
29
+ scaleLabel: {
30
+ display: true,
31
+ },
32
+ title: {
33
+ display: true,
34
+ text: 'Count of items'
35
+ },
36
+ min: 0
37
+ },
38
+ },
39
+ plugins: {
40
+ tooltip: {
41
+ callbacks: {
42
+ label: function(context) {
43
+ return context.dataset.data[context.dataIndex].title
44
+ }
45
+ }
46
+ },
47
+ annotation: {
48
+ annotations: {
49
+ <% holidays.each_with_index do |range, index| %>
50
+ holiday<%= index %>: {
51
+ drawTime: 'beforeDraw',
52
+ type: 'box',
53
+ xMin: '<%= range.begin %>T00:00:00',
54
+ xMax: '<%= range.end %>T23:59:59',
55
+ backgroundColor: '#F0F0F0',
56
+ borderColor: '#F0F0F0'
57
+ },
58
+ <% end %>
59
+ }
60
+ }
61
+ }
62
+ }
63
+ });
64
+ </script>
65
+
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'jirametrics/self_or_issue_dispatcher'
5
+
6
+ class HtmlReportConfig
7
+ include SelfOrIssueDispatcher
8
+ include DiscardChangesBefore
9
+
10
+ attr_reader :file_config, :sections
11
+
12
+ def initialize file_config:, block:
13
+ @file_config = file_config
14
+ @block = block
15
+ # @cycletimes = []
16
+ @sections = []
17
+ end
18
+
19
+ def cycletime label = nil, &block
20
+ # TODO: This is about to become deprecated
21
+
22
+ @file_config.project_config.all_boards.each do |_id, board|
23
+ raise 'Multiple cycletimes not supported yet' if board.cycletime
24
+
25
+ board.cycletime = CycleTimeConfig.new(parent_config: self, label: label, block: block)
26
+ end
27
+ end
28
+
29
+ def run
30
+ instance_eval(&@block)
31
+
32
+ # The quality report has to be generated last because otherwise cycletime won't have been
33
+ # set. Then we have to rotate it to the first position so it's at the top of the report.
34
+ execute_chart DataQualityReport.new(@original_issue_times || {})
35
+ @sections.rotate!(-1)
36
+
37
+ File.open @file_config.output_filename, 'w' do |file|
38
+ html_directory = "#{Pathname.new(File.realpath(__FILE__)).dirname}/html"
39
+ erb = ERB.new File.read("#{html_directory}/index.erb")
40
+ file.puts erb.result(binding)
41
+ end
42
+ end
43
+
44
+ def board_id id = nil
45
+ @board_id = id unless id.nil?
46
+ @board_id
47
+ end
48
+
49
+ def timezone_offset
50
+ @file_config.project_config.exporter.timezone_offset
51
+ end
52
+
53
+ def aging_work_in_progress_chart board_id: nil, &block
54
+ if board_id.nil?
55
+ ids = issues.collect { |i| i.board.id }.uniq.sort
56
+ else
57
+ ids = [board_id]
58
+ end
59
+
60
+ ids.each do |id|
61
+ execute_chart(AgingWorkInProgressChart.new(block)) do |chart|
62
+ chart.board_id = id
63
+ end
64
+ end
65
+ end
66
+
67
+ def aging_work_bar_chart &block
68
+ execute_chart AgingWorkBarChart.new(block)
69
+ end
70
+
71
+ def aging_work_table priority_name = nil, &block
72
+ if priority_name
73
+ deprecated message: 'priority name should no longer be passed into the chart. Specify it in the ' \
74
+ 'board declaration. See https://github.com/mikebowler/jira-export/wiki/Deprecated',
75
+ date: '2022-12-26'
76
+ end
77
+ execute_chart AgingWorkTable.new(block)
78
+ end
79
+
80
+ def cycletime_scatterplot &block
81
+ execute_chart CycletimeScatterplot.new block
82
+ end
83
+
84
+ def total_wip_over_time_chart &block
85
+ puts 'Deprecated(total_wip_over_time_chart). Use daily_wip_by_age_chart instead.'
86
+ execute_chart DailyWipByAgeChart.new block
87
+ end
88
+
89
+ def daily_wip_chart &block
90
+ execute_chart DailyWipChart.new(block)
91
+ end
92
+
93
+ def daily_wip_by_age_chart &block
94
+ execute_chart DailyWipByAgeChart.new block
95
+ end
96
+
97
+ def daily_wip_by_type &block
98
+ execute_chart DailyWipChart.new block
99
+ end
100
+
101
+ def daily_wip_by_blocked_stalled_chart
102
+ execute_chart DailyWipByBlockedStalledChart.new
103
+ end
104
+
105
+ def throughput_chart &block
106
+ execute_chart ThroughputChart.new(block)
107
+ end
108
+
109
+ def blocked_stalled_chart
110
+ puts 'Deprecated(blocked_stalled_chart). Use daily_wip_by_blocked_stalled_chart instead.'
111
+ execute_chart DailyWipByBlockedStalledChart.new
112
+ end
113
+
114
+ def expedited_chart priority_name = nil
115
+ if priority_name
116
+ deprecated message: 'priority name should no longer be passed into the chart. Specify it in the ' \
117
+ 'board declaration. See https://github.com/mikebowler/jira-export/wiki/Deprecated',
118
+ date: '2022-12-26'
119
+ end
120
+ execute_chart ExpeditedChart.new
121
+ end
122
+
123
+ def cycletime_histogram &block
124
+ execute_chart CycletimeHistogram.new block
125
+ end
126
+
127
+ def random_color
128
+ "\##{Random.bytes(3).unpack1('H*')}"
129
+ end
130
+
131
+ def html string, type: :body
132
+ raise "Unexpected type: #{type}" unless [:body, :header].include? type
133
+
134
+ @sections << [string, type]
135
+ end
136
+
137
+ def sprint_burndown options = :points_and_counts
138
+ execute_chart SprintBurndown.new do |chart|
139
+ chart.options = options
140
+ end
141
+ end
142
+
143
+ def story_point_accuracy_chart &block
144
+ execute_chart StoryPointAccuracyChart.new block
145
+ end
146
+
147
+ def hierarchy_table &block
148
+ execute_chart HierarchyTable.new block
149
+ end
150
+
151
+ def discard_changes_before_hook issues_cutoff_times
152
+ # raise 'Cycletime must be defined before using discard_changes_before' unless @cycletime
153
+
154
+ @original_issue_times = {}
155
+ issues_cutoff_times.each do |issue, cutoff_time|
156
+ started = issue.board.cycletime.started_time(issue)
157
+ if started && started <= cutoff_time
158
+ # We only need to log this if data was discarded
159
+ @original_issue_times[issue] = { cutoff_time: cutoff_time, started_time: started }
160
+ end
161
+ end
162
+ end
163
+
164
+ def discarded_changes_report
165
+ puts 'Deprecated(discarded_changes_report) No need to specify this anymore as this information is ' \
166
+ 'now included in the data quality checks.'
167
+ end
168
+
169
+ def dependency_chart &block
170
+ execute_chart DependencyChart.new block
171
+ end
172
+
173
+ def execute_chart chart, &after_init_block
174
+ project_config = @file_config.project_config
175
+
176
+ chart.issues = issues
177
+ chart.time_range = project_config.time_range
178
+ chart.timezone_offset = timezone_offset
179
+ chart.settings = project_config.settings
180
+
181
+ chart.all_boards = project_config.all_boards
182
+ chart.board_id = find_board_id if chart.respond_to? :board_id=
183
+ chart.holiday_dates = project_config.exporter.holiday_dates
184
+
185
+ time_range = @file_config.project_config.time_range
186
+ chart.date_range = time_range.begin.to_date..time_range.end.to_date
187
+ chart.aggregated_project = project_config.aggregated_project?
188
+
189
+ after_init_block&.call chart
190
+
191
+ html chart.run
192
+ end
193
+
194
+ def find_board_id
195
+ @board_id || @file_config.project_config.guess_board_id
196
+ end
197
+
198
+ def issues
199
+ @file_config.issues
200
+ end
201
+
202
+ def find_board id
203
+ @file_config.project_config.all_boards[id]
204
+ end
205
+
206
+ def project_name
207
+ @file_config.project_config.name
208
+ end
209
+
210
+ def boards
211
+ @file_config.project_config.board_configs.collect(&:id).collect { |id| find_board id }
212
+ end
213
+
214
+ def find_project_by_name name
215
+ @file_config.project_config.exporter.project_configs.find { |p| p.name == name }
216
+ end
217
+ end