jirametrics 2.0 → 2.11

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 +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +19 -26
  3. data/lib/jirametrics/aging_work_bar_chart.rb +79 -54
  4. data/lib/jirametrics/aging_work_in_progress_chart.rb +106 -40
  5. data/lib/jirametrics/aging_work_table.rb +78 -43
  6. data/lib/jirametrics/anonymizer.rb +6 -5
  7. data/lib/jirametrics/blocked_stalled_change.rb +24 -4
  8. data/lib/jirametrics/board.rb +44 -15
  9. data/lib/jirametrics/board_config.rb +8 -4
  10. data/lib/jirametrics/board_movement_calculator.rb +147 -0
  11. data/lib/jirametrics/change_item.rb +31 -10
  12. data/lib/jirametrics/chart_base.rb +102 -61
  13. data/lib/jirametrics/columns_config.rb +4 -0
  14. data/lib/jirametrics/css_variable.rb +33 -0
  15. data/lib/jirametrics/cycletime_config.rb +59 -8
  16. data/lib/jirametrics/cycletime_histogram.rb +69 -4
  17. data/lib/jirametrics/cycletime_scatterplot.rb +11 -15
  18. data/lib/jirametrics/daily_wip_by_age_chart.rb +44 -20
  19. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +37 -35
  20. data/lib/jirametrics/daily_wip_by_parent_chart.rb +38 -0
  21. data/lib/jirametrics/daily_wip_chart.rb +61 -14
  22. data/lib/jirametrics/data_quality_report.rb +222 -41
  23. data/lib/jirametrics/dependency_chart.rb +54 -23
  24. data/lib/jirametrics/download_config.rb +12 -0
  25. data/lib/jirametrics/downloader.rb +76 -57
  26. data/lib/jirametrics/{story_point_accuracy_chart.rb → estimate_accuracy_chart.rb} +48 -33
  27. data/lib/jirametrics/examples/aggregated_project.rb +22 -39
  28. data/lib/jirametrics/examples/standard_project.rb +25 -49
  29. data/lib/jirametrics/expedited_chart.rb +28 -25
  30. data/lib/jirametrics/exporter.rb +59 -32
  31. data/lib/jirametrics/file_config.rb +34 -13
  32. data/lib/jirametrics/file_system.rb +48 -3
  33. data/lib/jirametrics/flow_efficiency_scatterplot.rb +111 -0
  34. data/lib/jirametrics/groupable_issue_chart.rb +2 -6
  35. data/lib/jirametrics/grouping_rules.rb +7 -1
  36. data/lib/jirametrics/hierarchy_table.rb +4 -4
  37. data/lib/jirametrics/html/aging_work_bar_chart.erb +13 -16
  38. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +28 -5
  39. data/lib/jirametrics/html/aging_work_table.erb +19 -25
  40. data/lib/jirametrics/html/cycletime_histogram.erb +83 -3
  41. data/lib/jirametrics/html/cycletime_scatterplot.erb +9 -12
  42. data/lib/jirametrics/html/daily_wip_chart.erb +17 -13
  43. data/lib/jirametrics/html/{story_point_accuracy_chart.erb → estimate_accuracy_chart.erb} +9 -4
  44. data/lib/jirametrics/html/expedited_chart.erb +10 -13
  45. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +85 -0
  46. data/lib/jirametrics/html/hierarchy_table.erb +2 -2
  47. data/lib/jirametrics/html/index.css +209 -0
  48. data/lib/jirametrics/html/index.erb +16 -39
  49. data/lib/jirametrics/html/sprint_burndown.erb +10 -14
  50. data/lib/jirametrics/html/throughput_chart.erb +10 -13
  51. data/lib/jirametrics/html_report_config.rb +108 -86
  52. data/lib/jirametrics/issue.rb +357 -96
  53. data/lib/jirametrics/jira_gateway.rb +29 -11
  54. data/lib/jirametrics/project_config.rb +256 -144
  55. data/lib/jirametrics/rules.rb +2 -2
  56. data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
  57. data/lib/jirametrics/settings.json +10 -0
  58. data/lib/jirametrics/sprint_burndown.rb +24 -7
  59. data/lib/jirametrics/status.rb +84 -19
  60. data/lib/jirametrics/status_collection.rb +80 -39
  61. data/lib/jirametrics/throughput_chart.rb +12 -4
  62. data/lib/jirametrics/value_equality.rb +2 -2
  63. data/lib/jirametrics.rb +25 -7
  64. metadata +16 -17
  65. data/lib/jirametrics/discard_changes_before.rb +0 -37
  66. data/lib/jirametrics/experimental/generator.rb +0 -210
  67. data/lib/jirametrics/experimental/info.rb +0 -77
  68. data/lib/jirametrics/html/data_quality_report.erb +0 -126
@@ -0,0 +1,209 @@
1
+ :root {
2
+ --body-background: white;
3
+ --default-text-color: black;
4
+ --grid-line-color: lightgray;
5
+ --warning-banner: yellow;
6
+
7
+ --cycletime-scatterplot-overall-trendline-color: gray;
8
+
9
+ --non-working-days-color: #F0F0F0;
10
+ --expedited-color: red;
11
+ --blocked-color: #FF7400;
12
+ --stalled-color: orange;
13
+ --dead-color: black;
14
+
15
+ --type-story-color: #4bc14b;
16
+ --type-task-color: blue;
17
+ --type-bug-color: orange;
18
+ --type-spike-color: #9400D3;
19
+
20
+ --status-category-todo-color: gray;
21
+ --status-category-inprogress-color: #2663ff;
22
+ --status-category-done-color: #00ff00;
23
+ --status-category-unknown-color: black;
24
+
25
+ --aging-work-bar-chart-percentage-line-color: red;
26
+ --aging-work-bar-chart-separator-color: white;
27
+
28
+ --throughput_chart_total_line_color: gray;
29
+
30
+ --aging-work-in-progress-chart-shading-color: lightgray;
31
+ --aging-work-in-progress-chart-shading-50-color: #2E8BC0; // green;
32
+ --aging-work-in-progress-chart-shading-85-color: #ADD8E6; // yellow;
33
+ --aging-work-in-progress-chart-shading-98-color: #FF8A8A; // orange;
34
+ --aging-work-in-progress-chart-shading-100-color: #FF2E2E; // red;
35
+
36
+ --aging-work-in-progress-by-age-trend-line-color: gray;
37
+
38
+ --aging-work-table-date-in-jeopardy: yellow;
39
+ --aging-work-table-date-overdue: red;
40
+
41
+ --hierarchy-table-inactive-item-text-color: gray;
42
+
43
+ --wip-chart-completed-color: #00ff00;
44
+ --wip-chart-completed-but-not-started-color: #99FF99;
45
+ --wip-chart-duration-less-than-day-color: #ffef41;
46
+ --wip-chart-duration-week-or-less-color: #dcc900;
47
+ --wip-chart-duration-two-weeks-or-less-color: #dfa000;
48
+ --wip-chart-duration-four-weeks-or-less-color: #eb7200;
49
+ --wip-chart-duration-more-than-four-weeks-color: #e70000;
50
+ --wip-chart-active-color: #326cff;
51
+ --wip-chart-border-color: gray;
52
+
53
+ --estimate-accuracy-chart-completed-fill-color: #00ff00;
54
+ --estimate-accuracy-chart-completed-border-color: green;
55
+ --estimate-accuracy-chart-active-fill-color: #FFCCCB;
56
+ --estimate-accuracy-chart-active-border-color: red;
57
+
58
+ --expedited-chart-no-longer-expedited: gray;
59
+ --expedited-chart-dot-issue-started-color: orange;
60
+ --expedited-chart-dot-issue-stopped-color: green;
61
+ --expedited-chart-dot-expedite-started-color: red;
62
+ --expedited-chart-dot-expedite-stopped-color: green;
63
+
64
+ --sprint-burndown-sprint-color-1: blue;
65
+ --sprint-burndown-sprint-color-2: orange;
66
+ --sprint-burndown-sprint-color-3: green;
67
+ --sprint-burndown-sprint-color-4: red;
68
+ --sprint-burndown-sprint-color-5: brown;
69
+
70
+
71
+ }
72
+
73
+ body {
74
+ background-color: var(--body-background);
75
+ color: var(--default-text-color);
76
+ }
77
+
78
+ h1 {
79
+ border: 1px solid black;
80
+ background: lightgray;
81
+ padding-left: 0.2em;
82
+ }
83
+ dl, dd, dt {
84
+ padding: 0;
85
+ margin: 0;
86
+ }
87
+ dd {
88
+ margin-bottom: 0.4em;
89
+ }
90
+ span.highlight {
91
+ background: #FDD5B1;
92
+ }
93
+ a.issue_key {
94
+ white-space: nowrap;
95
+ }
96
+ table.standard {
97
+ th {
98
+ border-bottom: 1px solid gray;
99
+ position: sticky;
100
+ top: 0;
101
+ background: white;
102
+ }
103
+ td {
104
+ padding-left: 0.5em;
105
+ padding-right: 0.5em;
106
+ vertical-align: top;
107
+ }
108
+ tr:nth-child(odd){
109
+ background-color: #eee;
110
+ }
111
+ }
112
+
113
+ .chart {
114
+ background-color: white;
115
+ }
116
+
117
+ div.p {
118
+ margin: 0.5em 0;
119
+ padding: 0;
120
+ }
121
+
122
+ div.color_block {
123
+ display: inline-block;
124
+ width: 0.9em;
125
+ height: 0.9em;
126
+ border: 1px solid black;
127
+ }
128
+
129
+ ul.quality_report {
130
+ list-style-type: '⮕';
131
+ ::marker {
132
+ color: red;
133
+ }
134
+ li {
135
+ padding: 0.2em;
136
+ }
137
+ }
138
+
139
+ #footer {
140
+ text-align: center;
141
+ margin-top: 1em;
142
+ border-top: 1px solid gray;
143
+ }
144
+
145
+ @media screen and (prefers-color-scheme: dark) {
146
+ :root {
147
+ --warning-banner: #9F2B00;
148
+
149
+ --non-working-days-color: #2f2f2f;
150
+ --type-story-color: #6fb86f;
151
+ --type-task-color: #0021b3;
152
+ --type-bug-color: #bb5603;
153
+
154
+ --body-background: #343434;
155
+ --default-text-color: #aaa;
156
+ --grid-line-color: #424242;
157
+
158
+ --expedited-color: #b90000;
159
+ --blocked-color: #c75b02;
160
+ --stalled-color: #ae7202;
161
+ --dead-color: black;
162
+ --wip-chart-active-color: #2551c1;
163
+
164
+ --status-category-inprogress-color: #1c49bb;
165
+
166
+ --cycletime-scatterplot-overall-trendline-color: gray;
167
+
168
+ --hierarchy-table-inactive-item-text-color: #939393;
169
+
170
+ --wip-chart-completed-color: #03cb03;
171
+ --wip-chart-completed-but-not-started-color: #99FF99;
172
+ --wip-chart-duration-less-than-day-color: #d2d988;
173
+ --wip-chart-duration-week-or-less-color: #dfcd00;
174
+ --wip-chart-duration-two-weeks-or-less-color: #cf9400;
175
+ --wip-chart-duration-four-weeks-or-less-color: #c25e00;
176
+ --wip-chart-duration-more-than-four-weeks-color: #8e0000;
177
+ }
178
+
179
+ h1 {
180
+ color: #e0e0e0;
181
+ background-color: #656565;
182
+ }
183
+
184
+ a[href] {
185
+ color: #1e8ad6;
186
+ }
187
+
188
+ a[href]:hover {
189
+ color: #3ba0e6;
190
+ }
191
+
192
+ .chart {
193
+ background: var(--body-background);
194
+ }
195
+
196
+ table.standard {
197
+ th {
198
+ border-bottom: 1px solid gray;
199
+ background: var(--body-background);
200
+ }
201
+ tr:nth-child(odd){
202
+ background-color: #656565;
203
+ }
204
+ }
205
+
206
+ div.color_block {
207
+ border: 1px solid lightgray;
208
+ }
209
+ }
@@ -1,6 +1,7 @@
1
1
  <html>
2
2
  <head>
3
3
  <meta charset="UTF-8">
4
+ <link rel="icon" type="image/png" href="https://github.com/mikebowler/jirametrics/blob/main/favicon.png?raw=true" />
4
5
  <script src="https://cdn.jsdelivr.net/npm/moment@2.29.1/moment.js"></script>
5
6
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
6
7
  <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@^1"></script>
@@ -17,49 +18,25 @@
17
18
  document.getElementById(issues_id).style.display = 'none'
18
19
  }
19
20
  }
21
+ // If we switch between light/dark mode then force a refresh so all charts will redraw correctly
22
+ // in the other colour scheme.
23
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
24
+ location.reload()
25
+ })
20
26
  </script>
21
27
  <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>
28
+ <%= css %>
29
+ </style>
30
+ <script type="text/javascript">
31
+ Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--default-text-color');
32
+ </script>
58
33
  </head>
59
34
  <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>
35
+ <noscript>
36
+ <div style="padding: 1em; background: gray; color: white; font-size: 2em;">
37
+ Javascript is currently disabled and that means that almost all of the charts in this report won't render. If you'd loaded this from a folder on SharePoint then save it locally and load it again.
38
+ </div>
39
+ </noscript>
63
40
  <%= "\n" + @sections.collect { |text, type| text if type == :header }.compact.join("\n\n") %>
64
41
  <%= "\n" + @sections.collect { |text, type| text if type == :body }.compact.join("\n\n") %>
65
42
  </body>
@@ -1,6 +1,6 @@
1
1
  <h2>Burndown by <%= y_axis_title %></h2>
2
2
 
3
- <div>
3
+ <div class="chart">
4
4
  <canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
5
5
  </div>
6
6
  <script>
@@ -26,8 +26,10 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
26
26
  labelString: 'Date'
27
27
  },
28
28
  min: "<%= date_range.begin.to_s %>",
29
- max: "<%= (date_range.end + 1).to_s %>"
30
-
29
+ max: "<%= (date_range.end + 1).to_s %>",
30
+ grid: {
31
+ color: <%= CssVariable['--grid-line-color'].to_json %>
32
+ },
31
33
  },
32
34
  y: {
33
35
  scaleLabel: {
@@ -38,7 +40,10 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
38
40
  display: true,
39
41
  text: "<%= y_axis_title %>"
40
42
  },
41
- min: 0.0
43
+ min: 0.0,
44
+ grid: {
45
+ color: <%= CssVariable['--grid-line-color'].to_json %>
46
+ },
42
47
  }
43
48
  },
44
49
  plugins: {
@@ -51,16 +56,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
51
56
  },
52
57
  annotation: {
53
58
  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 %>
59
+ <%= working_days_annotation %>
64
60
  }
65
61
  }
66
62
  }
@@ -1,5 +1,5 @@
1
1
 
2
- <div>
2
+ <div class="chart">
3
3
  <canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
4
4
  </div>
5
5
  <script>
@@ -23,7 +23,10 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
23
23
  scaleLabel: {
24
24
  display: true,
25
25
  labelString: 'Date Completed'
26
- }
26
+ },
27
+ grid: {
28
+ color: <%= CssVariable['--grid-line-color'].to_json %>
29
+ },
27
30
  },
28
31
  y: {
29
32
  scaleLabel: {
@@ -33,7 +36,10 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
33
36
  display: true,
34
37
  text: 'Count of items'
35
38
  },
36
- min: 0
39
+ min: 0,
40
+ grid: {
41
+ color: <%= CssVariable['--grid-line-color'].to_json %>
42
+ },
37
43
  },
38
44
  },
39
45
  plugins: {
@@ -46,16 +52,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
46
52
  },
47
53
  annotation: {
48
54
  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 %>
55
+ <%= working_days_annotation %>
59
56
  }
60
57
  }
61
58
  }
@@ -5,45 +5,104 @@ require 'jirametrics/self_or_issue_dispatcher'
5
5
 
6
6
  class HtmlReportConfig
7
7
  include SelfOrIssueDispatcher
8
- include DiscardChangesBefore
9
8
 
10
- attr_reader :file_config, :sections
9
+ attr_reader :file_config, :sections, :charts
10
+
11
+ def self.define_chart name:, classname:, deprecated_warning: nil, deprecated_date: nil
12
+ lines = []
13
+ lines << "def #{name} &block"
14
+ lines << ' block = ->(_) {} unless block'
15
+ if deprecated_warning
16
+ lines << " file_system.deprecated date: #{deprecated_date.inspect}, message: #{deprecated_warning.inspect}"
17
+ end
18
+ lines << " execute_chart #{classname}.new(block)"
19
+ lines << 'end'
20
+ module_eval lines.join("\n"), __FILE__, __LINE__
21
+ end
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
+ define_chart name: 'daily_wip_by_type', classname: 'DailyWipChart',
38
+ deprecated_warning: 'This is the same as daily_wip_chart. Please use that one', deprecated_date: '2024-05-23'
39
+ define_chart name: 'story_point_accuracy_chart', classname: 'EstimateAccuracyChart',
40
+ deprecated_warning: 'Renamed to estimate_accuracy_chart. Please use that one', deprecated_date: '2024-05-23'
11
41
 
12
42
  def initialize file_config:, block:
13
43
  @file_config = file_config
14
44
  @block = block
15
- # @cycletimes = []
16
- @sections = []
45
+ @sections = [] # Where we store the chunks of text that will be assembled into the HTML
46
+ @charts = [] # Where we store all the charts we executed so we can assert against them.
17
47
  end
18
48
 
19
49
  def cycletime label = nil, &block
20
- # TODO: This is about to become deprecated
21
-
22
50
  @file_config.project_config.all_boards.each_value do |board|
23
- raise 'Multiple cycletimes not supported yet' if board.cycletime
51
+ raise 'Multiple cycletimes not supported' if board.cycletime
24
52
 
25
- board.cycletime = CycleTimeConfig.new(parent_config: self, label: label, block: block)
53
+ board.cycletime = CycleTimeConfig.new(parent_config: self, label: label, block: block, file_system: file_system)
26
54
  end
27
55
  end
28
56
 
57
+ # Mostly this is its own method so it can be called from the config
58
+ def included_projects
59
+ @file_config.project_config.aggregate_config.included_projects
60
+ end
61
+
29
62
  def run
30
63
  instance_eval(&@block)
31
64
 
32
65
  # The quality report has to be generated last because otherwise cycletime won't have been
33
66
  # 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 || {})
67
+ execute_chart DataQualityReport.new(file_config.project_config.discarded_changes_data)
35
68
  @sections.rotate!(-1)
36
69
 
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)
70
+ html create_footer
71
+
72
+ html_directory = "#{Pathname.new(File.realpath(__FILE__)).dirname}/html"
73
+ css = load_css html_directory: html_directory
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
76
+ end
77
+
78
+ def file_system
79
+ @file_config.project_config.exporter.file_system
80
+ end
81
+
82
+ def log message
83
+ file_system.log message
84
+ end
85
+
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
41
99
  end
100
+
101
+ base_css
42
102
  end
43
103
 
44
- def board_id id = nil
45
- @board_id = id unless id.nil?
46
- @board_id
104
+ def board_id id
105
+ @board_id = id
47
106
  end
48
107
 
49
108
  def timezone_offset
@@ -51,6 +110,8 @@ class HtmlReportConfig
51
110
  end
52
111
 
53
112
  def aging_work_in_progress_chart board_id: nil, &block
113
+ block ||= ->(_) {}
114
+
54
115
  if board_id.nil?
55
116
  ids = issues.collect { |i| i.board.id }.uniq.sort
56
117
  else
@@ -64,52 +125,13 @@ class HtmlReportConfig
64
125
  end
65
126
  end
66
127
 
67
- def aging_work_bar_chart &block
68
- execute_chart AgingWorkBarChart.new(block)
69
- end
70
-
71
- def aging_work_table &block
72
- execute_chart AgingWorkTable.new(block)
73
- end
74
-
75
- def cycletime_scatterplot &block
76
- execute_chart CycletimeScatterplot.new block
77
- end
78
-
79
- def daily_wip_chart &block
80
- execute_chart DailyWipChart.new(block)
81
- end
82
-
83
- def daily_wip_by_age_chart &block
84
- execute_chart DailyWipByAgeChart.new block
85
- end
86
-
87
- def daily_wip_by_type &block
88
- execute_chart DailyWipChart.new block
89
- end
90
-
91
- def daily_wip_by_blocked_stalled_chart
92
- execute_chart DailyWipByBlockedStalledChart.new
93
- end
94
-
95
- def throughput_chart &block
96
- execute_chart ThroughputChart.new(block)
97
- end
98
-
99
- def expedited_chart
100
- execute_chart ExpeditedChart.new
101
- end
102
-
103
- def cycletime_histogram &block
104
- execute_chart CycletimeHistogram.new block
105
- end
106
-
107
128
  def random_color
108
129
  "##{Random.bytes(3).unpack1('H*')}"
109
130
  end
110
131
 
111
132
  def html string, type: :body
112
- raise "Unexpected type: #{type}" unless %i[body header].include? type
133
+ allowed_types = %i[body header]
134
+ raise "Unexpected type: #{type} allowed_types: #{allowed_types.inspect}" unless allowed_types.include? type
113
135
 
114
136
  @sections << [string, type]
115
137
  end
@@ -120,41 +142,26 @@ class HtmlReportConfig
120
142
  end
121
143
  end
122
144
 
123
- def story_point_accuracy_chart &block
124
- execute_chart StoryPointAccuracyChart.new block
125
- end
126
-
127
- def hierarchy_table &block
128
- execute_chart HierarchyTable.new block
129
- end
130
-
131
- def discard_changes_before_hook issues_cutoff_times
132
- # raise 'Cycletime must be defined before using discard_changes_before' unless @cycletime
133
-
134
- @original_issue_times = {}
135
- issues_cutoff_times.each do |issue, cutoff_time|
136
- started = issue.board.cycletime.started_time(issue)
137
- if started && started <= cutoff_time
138
- # We only need to log this if data was discarded
139
- @original_issue_times[issue] = { cutoff_time: cutoff_time, started_time: started }
140
- end
141
- end
142
- end
143
-
144
145
  def dependency_chart &block
145
146
  execute_chart DependencyChart.new block
146
147
  end
147
148
 
149
+ # have an explicit method here so that index.erb can call 'settings' just as any other erb can.
150
+ def settings
151
+ @file_config.project_config.settings
152
+ end
153
+
148
154
  def execute_chart chart, &after_init_block
149
155
  project_config = @file_config.project_config
150
156
 
157
+ chart.file_system = file_system
151
158
  chart.issues = issues
152
159
  chart.time_range = project_config.time_range
153
160
  chart.timezone_offset = timezone_offset
154
- chart.settings = project_config.settings
161
+ chart.settings = settings
155
162
 
156
163
  chart.all_boards = project_config.all_boards
157
- chart.board_id = find_board_id if chart.respond_to? :board_id=
164
+ chart.board_id = find_board_id
158
165
  chart.holiday_dates = project_config.exporter.holiday_dates
159
166
 
160
167
  time_range = @file_config.project_config.time_range
@@ -163,6 +170,7 @@ class HtmlReportConfig
163
170
 
164
171
  after_init_block&.call chart
165
172
 
173
+ @charts << chart
166
174
  html chart.run
167
175
  end
168
176
 
@@ -174,19 +182,33 @@ class HtmlReportConfig
174
182
  @file_config.issues
175
183
  end
176
184
 
185
+ # For use by the user config
177
186
  def find_board id
178
187
  @file_config.project_config.all_boards[id]
179
188
  end
180
189
 
181
- def project_name
182
- @file_config.project_config.name
183
- end
184
-
190
+ # For use by the user config
185
191
  def boards
186
192
  @file_config.project_config.board_configs.collect(&:id).collect { |id| find_board id }
187
193
  end
188
194
 
189
- def find_project_by_name name
190
- @file_config.project_config.exporter.project_configs.find { |p| p.name == name }
195
+ def create_footer now: DateTime.now
196
+ now = now.new_offset(timezone_offset)
197
+ version = Gem.loaded_specs['jirametrics']&.version || 'Next'
198
+
199
+ <<~HTML
200
+ <section id="footer">
201
+ Report generated on <b>#{now.strftime('%Y-%b-%d')}</b> at <b>#{now.strftime('%I:%M:%S%P %Z')}</b>
202
+ with <a href="https://jirametrics.org">JiraMetrics</a> <b>v#{version}</b>
203
+ </section>
204
+ HTML
205
+ end
206
+
207
+ def discard_changes_before status_becomes: nil, &block
208
+ file_system.deprecated(
209
+ date: '2025-01-09',
210
+ message: 'discard_changes_before is now only supported at the project level'
211
+ )
212
+ file_config.project_config.discard_changes_before status_becomes: status_becomes, &block
191
213
  end
192
214
  end