jirametrics 2.14 → 2.15

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5b1b9e8d837f6d74990d377db007ed5e55670de77738ab38a04c6d023d865c3
4
- data.tar.gz: 99e8ef3e85a3dfa2bd6d845a32d974718c0e9884d2f4750891ba491fd884ab0b
3
+ metadata.gz: c8045db95ffbb1368c3df8be3ee88c88ddb1a3d2fee46c989ba9d94e8fdb4ac3
4
+ data.tar.gz: a4a245da182c96238ec99918eefaf097c3fcc3cd3f4a09f85d4fbdd25020b713
5
5
  SHA512:
6
- metadata.gz: 804a25c8a19df9ae9862e2f95539183ece53e3841e590c64b8deb7048601bfb2b42ad5b75c075b85058abb7cea0cc875d517f4208bd5edbf98e2129e5567af59
7
- data.tar.gz: '0059cde9746423c5baf3ca1f47243b6489ccf77b8fd409255f7301f638eb1975b8b2815d266f720cd9f1cd51e2cf0bc9e33cbad3e34110ca1dedc4ad7fc9ff5b'
6
+ metadata.gz: cb301db36a63ec0e2da49c5f7ce1f7c58ee44f9c1ab1dad7811667a72d598a8a36282bccc795bebf755706ea1b0d1f0ebdf092f75fe89dc128203645182b9a30
7
+ data.tar.gz: e1d08efa142f9d824621831cfc465fd6d17ba3c374f0259d860798feec2706214989c9eac9e41c5eafbd66be0d4c147c29bdec638eec7e956b87858384bf6bb8
@@ -157,4 +157,4 @@ class AtlassianDocumentFormat
157
157
  text = "@#{user.display_name}" if user
158
158
  "<span class='account_id'>#{text}</span>"
159
159
  end
160
- end
160
+ end
@@ -2,7 +2,8 @@
2
2
 
3
3
  class ChartBase
4
4
  attr_accessor :timezone_offset, :board_id, :all_boards, :date_range,
5
- :time_range, :data_quality, :holiday_dates, :settings, :issues, :file_system, :users
5
+ :time_range, :data_quality, :holiday_dates, :settings, :issues, :file_system,
6
+ :atlassian_document_format
6
7
  attr_writer :aggregated_project
7
8
  attr_reader :canvas_width, :canvas_height
8
9
 
@@ -44,7 +45,7 @@ class ChartBase
44
45
 
45
46
  def render_top_text caller_binding
46
47
  result = +''
47
- result << "<h1>#{@header_text}</h1>" if @header_text
48
+ result << "<h1 class='foldable'>#{@header_text}</h1>" if @header_text
48
49
  result << ERB.new(@description_text).result(caller_binding) if @description_text
49
50
  result
50
51
  end
@@ -62,7 +62,9 @@ class CycletimeHistogram < ChartBase
62
62
  )
63
63
  end
64
64
 
65
- return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if data_sets.empty?
65
+ if data_sets.empty?
66
+ return "<h1 class='foldable'>#{@header_text}</h1><div>No data matched the selected criteria. Nothing to show.</div>"
67
+ end
66
68
 
67
69
  wrap_and_render(binding, __FILE__)
68
70
  end
@@ -23,7 +23,7 @@ class DailyView < ChartBase
23
23
  def run
24
24
  aging_issues = select_aging_issues
25
25
 
26
- return "<h1>#{@header_text}</h1>There are no items currently in progress" if aging_issues.empty?
26
+ return "<h1 class='foldable'>#{@header_text}</h1>There are no items currently in progress" if aging_issues.empty?
27
27
 
28
28
  result = +''
29
29
  result << render_top_text(binding)
@@ -33,10 +33,6 @@ class DailyView < ChartBase
33
33
  result
34
34
  end
35
35
 
36
- def atlassian_document_format
37
- @atlassian_document_format ||= AtlassianDocumentFormat.new(users: users, timezone_offset: timezone_offset)
38
- end
39
-
40
36
  def select_aging_issues
41
37
  aging_issues = issues.select do |issue|
42
38
  started_at, stopped_at = issue.board.cycletime.started_stopped_times(issue)
@@ -170,13 +166,7 @@ class DailyView < ChartBase
170
166
 
171
167
  return lines if subtasks.empty?
172
168
 
173
- id = next_id
174
- lines <<
175
- "<a href=\"javascript:toggle_visibility('open#{id}', 'close#{id}', 'section#{id}');\">" \
176
- "<span id='open#{id}' style='display: none'>▶ Child issues</span>" \
177
- "<span id='close#{id}'>▼ Child issues</span></a>"
178
- lines << "<section id='section#{id}'>"
179
-
169
+ lines << '<section><div class="foldable">Child issues</div>'
180
170
  lines += subtasks
181
171
  lines << '</section>'
182
172
 
@@ -187,14 +177,9 @@ class DailyView < ChartBase
187
177
  history = issue.changes.reverse
188
178
  lines = []
189
179
 
190
- id = next_id
191
- lines << [
192
- "<a href=\"javascript:toggle_visibility('open#{id}', 'close#{id}', 'table#{id}');\">" \
193
- "<span id='open#{id}'>▶ Issue History</span>" \
194
- "<span id='close#{id}' style='display: none'>▼ Issue History</span></a>"
195
- ]
180
+ lines << '<section><div class="foldable startFolded">Issue history</div>'
196
181
  table = +''
197
- table << "<table id='table#{id}' style='display: none'>"
182
+ table << '<table>'
198
183
  history.each do |c|
199
184
  time = c.time.strftime '%b %d, %I:%M%P'
200
185
 
@@ -207,6 +192,7 @@ class DailyView < ChartBase
207
192
  end
208
193
  table << '</table>'
209
194
  lines << [table]
195
+ lines << '</section>'
210
196
  lines
211
197
  end
212
198
 
@@ -51,7 +51,10 @@ class DependencyChart < ChartBase
51
51
  instance_eval(&@rules_block) if @rules_block
52
52
 
53
53
  dot_graph = build_dot_graph
54
- return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if dot_graph.nil?
54
+ if dot_graph.nil?
55
+ return "<h1 class='foldable'>#{@header_text}</h1>" \
56
+ '<div>No data matched the selected criteria. Nothing to show.</div>'
57
+ end
55
58
 
56
59
  svg = execute_graphviz(dot_graph.join("\n"))
57
60
  "<h1>#{@header_text}</h1><div>#{@description_text}</div>#{shrink_svg svg}"
@@ -48,7 +48,7 @@ class ExpeditedChart < ChartBase
48
48
  end
49
49
 
50
50
  if data_sets.empty?
51
- '<h1>Expedited work</h1>There is no expedited work in this time period.'
51
+ '<h1 class="foldable">Expedited work</h1><p>There is no expedited work in this time period.</p>'
52
52
  else
53
53
  wrap_and_render(binding, __FILE__)
54
54
  end
@@ -60,7 +60,7 @@ class FlowEfficiencyScatterplot < ChartBase
60
60
  create_dataset(issues: issues, label: rules.label, color: rules.color)
61
61
  end
62
62
 
63
- return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if data_sets.empty?
63
+ return "<h1 class='foldable'>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if data_sets.empty?
64
64
 
65
65
  wrap_and_render(binding, __FILE__)
66
66
  end
@@ -1,5 +1,5 @@
1
- [<a id='<%= link_id %>' href="#" onclick='expand_collapse("<%= link_id %>", "<%= issues_id %>"); return false;'>Show details</a>]
2
- <table class='standard' id='<%= issues_id %>' style='display: none;'>
1
+ <div class='foldable startFolded'>Show details</div>
2
+ <table class='standard' id='<%= issues_id %>'>
3
3
  <thead>
4
4
  <tr>
5
5
  <th>Issue</th>
@@ -6,8 +6,8 @@ if show_stats
6
6
  link_id = next_id
7
7
  issues_id = next_id
8
8
  %>
9
- [<a id='<%= link_id %>' href="#" onclick='expand_collapse("<%= link_id %>", "<%= issues_id %>"); return false;'>Show details</a>]
10
- <div id="<%= issues_id %>" style="display: none;">
9
+ <div class='foldable' style="padding-left: 1em;">Statistics</div>
10
+ <div id="<%= issues_id %>" style="padding-left: 1em;">
11
11
  <div>
12
12
  <table class="standard">
13
13
  <tr>
@@ -78,11 +78,6 @@ body {
78
78
  color: var(--default-text-color);
79
79
  }
80
80
 
81
- h1 {
82
- border: 1px solid black;
83
- background: lightgray;
84
- padding-left: 0.2em;
85
- }
86
81
  dl, dd, dt {
87
82
  padding: 0;
88
83
  margin: 0;
@@ -244,11 +239,6 @@ div.child_issue {
244
239
  --daily-view-selected-issue-background: #474747;
245
240
  }
246
241
 
247
- h1 {
248
- color: #e0e0e0;
249
- background-color: #656565;
250
- }
251
-
252
242
  a[href] {
253
243
  color: #1e8ad6;
254
244
  }
@@ -7,39 +7,7 @@
7
7
  <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@^1"></script>
8
8
  <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>
9
9
  <script type="text/javascript">
10
- function expand_collapse(link_id, issues_id) {
11
- link_text = document.getElementById(link_id).textContent
12
- if( link_text == 'Show details') {
13
- document.getElementById(link_id).textContent = 'Hide details'
14
- document.getElementById(issues_id).style.display = 'block'
15
- }
16
- else {
17
- document.getElementById(link_id).textContent = 'Show details'
18
- document.getElementById(issues_id).style.display = 'none'
19
- }
20
- }
21
-
22
- function toggle_visibility(open_link_id, close_link_id, toggleable_id) {
23
- let open_link = document.getElementById(open_link_id)
24
- let close_link = document.getElementById(close_link_id)
25
- let toggleable_element = document.getElementById(toggleable_id)
26
-
27
- if(open_link.style.display == 'none') {
28
- open_link.style.display = 'block'
29
- close_link.style.display = 'none'
30
- toggleable_element.style.display = 'none'
31
- }
32
- else {
33
- open_link.style.display = 'none'
34
- close_link.style.display = 'block'
35
- toggleable_element.style.display = 'block'
36
- }
37
- }
38
- // If we switch between light/dark mode then force a refresh so all charts will redraw correctly
39
- // in the other colour scheme.
40
- window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
41
- location.reload()
42
- })
10
+ <%= javascript %>
43
11
  </script>
44
12
  <style>
45
13
  <%= css %>
@@ -0,0 +1,90 @@
1
+ function makeFoldable() {
2
+ // Get all elements with the "foldable" class
3
+ const foldableElements = document.querySelectorAll('.foldable');
4
+
5
+ if (foldableElements.length === 0) {
6
+ return; // No foldable elements found
7
+ }
8
+
9
+ // Process each foldable element
10
+ foldableElements.forEach((element, index) => {
11
+ // Skip if this is the footer element
12
+ if (element.id === 'footer') {
13
+ return;
14
+ }
15
+
16
+ // Create a unique ID for this section
17
+ const sectionId = `foldable-section-${index}`;
18
+ const toggleId = `foldable-toggle-${index}`;
19
+
20
+ // Create a container div for the foldable element and its content
21
+ const container = document.createElement('div');
22
+ container.className = 'foldable-section';
23
+ container.id = sectionId;
24
+
25
+ // Create a toggle button
26
+ const toggleButton = document.createElement(element.tagName); //'button');
27
+ toggleButton.id = toggleId;
28
+ toggleButton.className = 'foldable-toggle-btn';
29
+ toggleButton.innerHTML = '▼ ' + element.textContent;
30
+
31
+ // Create a content container
32
+ const contentContainer = document.createElement('div');
33
+ contentContainer.className = 'foldable-content';
34
+ contentContainer.style.cssText = `
35
+ border-left: 2px solid #ccc;
36
+ padding-left: 15px;
37
+ `;
38
+
39
+ // Move the foldable element into the container and replace it with the toggle button
40
+ element.parentNode.insertBefore(container, element);
41
+ container.appendChild(toggleButton);
42
+ container.appendChild(contentContainer);
43
+
44
+ // Move all elements between this foldable element and the next foldable element (or end of document) into the content container
45
+ let nextElement = element.nextElementSibling;
46
+ while (nextElement && !nextElement.classList.contains('foldable')) {
47
+ // Skip the footer element
48
+ if (nextElement.id === 'footer') {
49
+ break;
50
+ }
51
+
52
+ const temp = nextElement.nextElementSibling;
53
+ contentContainer.appendChild(nextElement);
54
+ nextElement = temp;
55
+ }
56
+
57
+ // Remove the original foldable element
58
+ element.remove();
59
+
60
+ // Add click event to toggle visibility
61
+ toggleButton.addEventListener('click', function() {
62
+ const content = this.nextElementSibling;
63
+ if (content.style.display === 'none') {
64
+ content.style.display = 'block';
65
+ this.innerHTML = '▼ ' + this.innerHTML.substring(2);
66
+ } else {
67
+ content.style.display = 'none';
68
+ this.innerHTML = '▶ ' + this.innerHTML.substring(2);
69
+ }
70
+ });
71
+
72
+ // Initially show the content (you can change this to 'none' if you want sections collapsed by default)
73
+ contentContainer.style.display = 'block';
74
+ if(element.classList.contains('startFolded')) {
75
+ toggleButton.click();
76
+ }
77
+ });
78
+ }
79
+
80
+ // Auto-initialize when DOM is loaded
81
+ document.addEventListener('DOMContentLoaded', function() {
82
+ makeFoldable();
83
+ });
84
+
85
+
86
+ // If we switch between light/dark mode then force a refresh so all charts will redraw correctly
87
+ // in the other colour scheme.
88
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
89
+ location.reload()
90
+ })
@@ -68,8 +68,9 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
68
68
  link_id = next_id
69
69
  issues_id = next_id
70
70
  %>
71
- [<a id='<%= link_id %>' href="#" onclick='expand_collapse("<%= link_id %>", "<%= issues_id %>"); return false;'>Show details</a>]
72
- <div id="<%= issues_id %>" style="display: none;">
71
+ <section>
72
+ <div class='foldable startFolded'>Show statistics</div>
73
+ <div id="<%= issues_id %>">
73
74
  <table class='standard' style="margin-left: 1em;">
74
75
  <thead>
75
76
  <th>Sprint</th>
@@ -109,4 +110,5 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
109
110
  <% end %>
110
111
  </ul>
111
112
  </p>
112
- </div>
113
+ </div>
114
+ </section>
@@ -72,6 +72,7 @@ class HtmlReportConfig
72
72
 
73
73
  html_directory = "#{Pathname.new(File.realpath(__FILE__)).dirname}/html"
74
74
  css = load_css html_directory: html_directory
75
+ javascript = file_system.load(File.join(html_directory, 'index.js'))
75
76
  erb = ERB.new file_system.load(File.join(html_directory, 'index.erb'))
76
77
  file_system.save_file content: erb.result(binding), filename: @file_config.output_filename
77
78
  end
@@ -87,7 +88,6 @@ class HtmlReportConfig
87
88
  def load_css html_directory:
88
89
  base_css_filename = File.join(html_directory, 'index.css')
89
90
  base_css = file_system.load(base_css_filename)
90
- log("Loaded CSS: #{base_css_filename}")
91
91
 
92
92
  extra_css_filename = settings['include_css']
93
93
  if extra_css_filename
@@ -160,7 +160,7 @@ class HtmlReportConfig
160
160
  chart.time_range = project_config.time_range
161
161
  chart.timezone_offset = timezone_offset
162
162
  chart.settings = settings
163
- chart.users = project_config.users
163
+ chart.atlassian_document_format = project_config.atlassian_document_format
164
164
 
165
165
  chart.all_boards = project_config.all_boards
166
166
  chart.board_id = find_board_id
@@ -608,7 +608,7 @@ class Issue
608
608
 
609
609
  def dump
610
610
  result = +''
611
- result << "#{key} (#{type}): #{compact_text summary, 200}\n"
611
+ result << "#{key} (#{type}): #{compact_text summary, max: 200}\n"
612
612
 
613
613
  assignee = raw['fields']['assignee']
614
614
  result << " [assignee] #{assignee['name'].inspect} <#{assignee['emailAddress']}>\n" unless assignee.nil?
@@ -705,6 +705,19 @@ class Issue
705
705
  board.sprints.select { |s| sprint_ids.include? s.id }
706
706
  end
707
707
 
708
+ def compact_text text, max: 60
709
+ return '' if text.nil?
710
+
711
+ if text.is_a? Hash
712
+ # We can't effectively compact it but we can convert it into a string.
713
+ text = @board.project_config.atlassian_document_format.to_html(text)
714
+ else
715
+ text = text.gsub(/\s+/, ' ').strip
716
+ text = "#{text[0...max]}..." if text.length > max
717
+ end
718
+ text
719
+ end
720
+
708
721
  private
709
722
 
710
723
  def load_history_into_changes
@@ -729,14 +742,6 @@ class Issue
729
742
  end
730
743
  end
731
744
 
732
- def compact_text text, max = 60
733
- return nil if text.nil?
734
-
735
- text = text.gsub(/\s+/, ' ').strip
736
- text = "#{text[0..max]}..." if text.length > max
737
- text
738
- end
739
-
740
745
  def sort_changes!
741
746
  @changes.sort! do |a, b|
742
747
  # It's common that a resolved will happen at the same time as a status change.
@@ -17,7 +17,9 @@ class JiraGateway
17
17
  begin
18
18
  json = JSON.parse(result)
19
19
  rescue # rubocop:disable Style/RescueStandardError
20
- raise "Error when parsing result: #{result.inspect}"
20
+ message = "Unable to parse results from #{sanitize_message(command)}"
21
+ @file_system.error message, more: result
22
+ raise message
21
23
  end
22
24
 
23
25
  raise "Download failed with: #{JSON.pretty_generate(json)}" unless json_successful?(json)
@@ -25,9 +27,16 @@ class JiraGateway
25
27
  json
26
28
  end
27
29
 
30
+ def sanitize_message message
31
+ token = @jira_api_token || @jira_personal_access_token
32
+ raise 'Neither Jira API Token or personal access token has been set' unless token
33
+
34
+ message.gsub(@jira_api_token, '[API_TOKEN]')
35
+ end
36
+
28
37
  def call_command command
29
38
  log_entry = " #{command.gsub(/\s+/, ' ')}"
30
- log_entry = log_entry.gsub(@jira_api_token, '[API_TOKEN]') if @jira_api_token
39
+ log_entry = sanitize_message log_entry if @jira_api_token
31
40
  @file_system.log log_entry
32
41
 
33
42
  result = `#{command}`
@@ -352,6 +352,12 @@ class ProjectConfig
352
352
  json.each { |user_data| @users << User.new(raw: user_data) }
353
353
  end
354
354
 
355
+ def atlassian_document_format
356
+ @atlassian_document_format ||= AtlassianDocumentFormat.new(
357
+ users: @users, timezone_offset: exporter.timezone_offset
358
+ )
359
+ end
360
+
355
361
  def to_time string, end_of_day: false
356
362
  time = end_of_day ? '23:59:59' : '00:00:00'
357
363
  string = "#{string}T#{time}#{exporter.timezone_offset}" if string.match?(/^\d{4}-\d{2}-\d{2}$/)
data/lib/jirametrics.rb CHANGED
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'thor'
4
+ require 'require_all'
5
+
6
+ # This one does need to be loaded early. The rest will be loaded later.
7
+ require './lib/jirametrics/file_system'
4
8
 
5
9
  class JiraMetrics < Thor
6
10
  def self.exit_on_failure?
@@ -48,76 +52,21 @@ class JiraMetrics < Thor
48
52
  Exporter.instance.info(keys, name_filter: options[:name] || '*')
49
53
  end
50
54
 
51
- private
55
+ no_commands do
56
+ def load_config config_file, file_system: FileSystem.new
57
+ config_file = './config.rb' if config_file.nil?
52
58
 
53
- def load_config config_file
54
- config_file = './config.rb' if config_file.nil?
59
+ if File.exist? config_file
60
+ # The fact that File.exist can see the file does not mean that require will be
61
+ # able to load it. Convert this to an absolute pathname now for require.
62
+ config_file = File.absolute_path(config_file).to_s
63
+ else
64
+ file_system.error "Cannot find configuration file #{config_file.inspect}"
65
+ exit 1
66
+ end
55
67
 
56
- if File.exist? config_file
57
- # The fact that File.exist can see the file does not mean that require will be
58
- # able to load it. Convert this to an absolute pathname now for require.
59
- config_file = File.absolute_path(config_file).to_s
60
- else
61
- puts "Cannot find configuration file #{config_file.inspect}"
62
- exit 1
68
+ require_rel 'jirametrics'
69
+ load config_file
63
70
  end
64
-
65
- require 'jirametrics/value_equality'
66
- require 'jirametrics/chart_base'
67
- require 'jirametrics/rules'
68
- require 'jirametrics/grouping_rules'
69
- require 'jirametrics/daily_wip_chart'
70
- require 'jirametrics/groupable_issue_chart'
71
- require 'jirametrics/css_variable'
72
- require 'jirametrics/issue_collection'
73
-
74
- require 'jirametrics/aggregate_config'
75
- require 'jirametrics/expedited_chart'
76
- require 'jirametrics/board_config'
77
- require 'jirametrics/file_config'
78
- require 'jirametrics/jira_gateway'
79
- require 'jirametrics/trend_line_calculator'
80
- require 'jirametrics/status'
81
- require 'jirametrics/issue_link'
82
- require 'jirametrics/estimate_accuracy_chart'
83
- require 'jirametrics/status_collection'
84
- require 'jirametrics/sprint'
85
- require 'jirametrics/issue'
86
- require 'jirametrics/daily_wip_by_age_chart'
87
- require 'jirametrics/daily_wip_by_parent_chart'
88
- require 'jirametrics/aging_work_in_progress_chart'
89
- require 'jirametrics/cycletime_scatterplot'
90
- require 'jirametrics/flow_efficiency_scatterplot'
91
- require 'jirametrics/sprint_issue_change_data'
92
- require 'jirametrics/cycletime_histogram'
93
- require 'jirametrics/daily_wip_by_blocked_stalled_chart'
94
- require 'jirametrics/html_report_config'
95
- require 'jirametrics/data_quality_report'
96
- require 'jirametrics/aging_work_bar_chart'
97
- require 'jirametrics/change_item'
98
- require 'jirametrics/project_config'
99
- require 'jirametrics/dependency_chart'
100
- require 'jirametrics/cycletime_config'
101
- require 'jirametrics/tree_organizer'
102
- require 'jirametrics/aging_work_table'
103
- require 'jirametrics/sprint_burndown'
104
- require 'jirametrics/self_or_issue_dispatcher'
105
- require 'jirametrics/throughput_chart'
106
- require 'jirametrics/exporter'
107
- require 'jirametrics/file_system'
108
- require 'jirametrics/blocked_stalled_change'
109
- require 'jirametrics/board_column'
110
- require 'jirametrics/anonymizer'
111
- require 'jirametrics/downloader'
112
- require 'jirametrics/fix_version'
113
- require 'jirametrics/download_config'
114
- require 'jirametrics/columns_config'
115
- require 'jirametrics/hierarchy_table'
116
- require 'jirametrics/estimation_configuration'
117
- require 'jirametrics/board'
118
- require 'jirametrics/daily_view'
119
- require 'jirametrics/user'
120
- require 'jirametrics/atlassian_document_format'
121
- load config_file
122
71
  end
123
72
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jirametrics
3
3
  version: !ruby/object:Gem::Version
4
- version: '2.14'
4
+ version: '2.15'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-08-18 00:00:00.000000000 Z
10
+ date: 2025-09-08 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: random-word
@@ -113,6 +113,7 @@ files:
113
113
  - lib/jirametrics/html/hierarchy_table.erb
114
114
  - lib/jirametrics/html/index.css
115
115
  - lib/jirametrics/html/index.erb
116
+ - lib/jirametrics/html/index.js
116
117
  - lib/jirametrics/html/sprint_burndown.erb
117
118
  - lib/jirametrics/html/throughput_chart.erb
118
119
  - lib/jirametrics/html_report_config.rb