jirametrics 2.7 → 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.
- checksums.yaml +4 -4
- data/lib/jirametrics/aggregate_config.rb +4 -4
- data/lib/jirametrics/aging_work_bar_chart.rb +7 -5
- data/lib/jirametrics/aging_work_in_progress_chart.rb +105 -41
- data/lib/jirametrics/aging_work_table.rb +50 -2
- data/lib/jirametrics/board.rb +33 -5
- data/lib/jirametrics/board_config.rb +6 -2
- data/lib/jirametrics/board_movement_calculator.rb +147 -0
- data/lib/jirametrics/change_item.rb +19 -6
- data/lib/jirametrics/chart_base.rb +59 -21
- data/lib/jirametrics/css_variable.rb +1 -1
- data/lib/jirametrics/cycletime_config.rb +37 -5
- data/lib/jirametrics/cycletime_histogram.rb +67 -2
- data/lib/jirametrics/data_quality_report.rb +174 -35
- data/lib/jirametrics/download_config.rb +2 -2
- data/lib/jirametrics/downloader.rb +44 -25
- data/lib/jirametrics/examples/aggregated_project.rb +2 -5
- data/lib/jirametrics/examples/standard_project.rb +4 -6
- data/lib/jirametrics/expedited_chart.rb +7 -7
- data/lib/jirametrics/exporter.rb +10 -20
- data/lib/jirametrics/file_config.rb +23 -6
- data/lib/jirametrics/file_system.rb +39 -4
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +2 -4
- data/lib/jirametrics/groupable_issue_chart.rb +1 -3
- data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -12
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +22 -5
- data/lib/jirametrics/html/aging_work_table.erb +6 -4
- data/lib/jirametrics/html/cycletime_histogram.erb +74 -0
- data/lib/jirametrics/html/cycletime_scatterplot.erb +1 -10
- data/lib/jirametrics/html/daily_wip_chart.erb +1 -10
- data/lib/jirametrics/html/expedited_chart.erb +1 -10
- data/lib/jirametrics/html/hierarchy_table.erb +1 -1
- data/lib/jirametrics/html/index.css +28 -5
- data/lib/jirametrics/html/index.erb +8 -4
- data/lib/jirametrics/html/sprint_burndown.erb +1 -10
- data/lib/jirametrics/html/throughput_chart.erb +1 -10
- data/lib/jirametrics/html_report_config.rb +32 -23
- data/lib/jirametrics/issue.rb +104 -44
- data/lib/jirametrics/jira_gateway.rb +16 -3
- data/lib/jirametrics/project_config.rb +223 -120
- data/lib/jirametrics/sprint_burndown.rb +1 -1
- data/lib/jirametrics/status.rb +81 -26
- data/lib/jirametrics/status_collection.rb +74 -40
- data/lib/jirametrics/throughput_chart.rb +1 -1
- data/lib/jirametrics/value_equality.rb +2 -2
- data/lib/jirametrics.rb +7 -1
- metadata +8 -13
- data/lib/jirametrics/discard_changes_before.rb +0 -37
- data/lib/jirametrics/html/data_quality_report.erb +0 -138
|
@@ -39,6 +39,7 @@ class Downloader
|
|
|
39
39
|
# board_ids = @download_config.board_ids
|
|
40
40
|
|
|
41
41
|
remove_old_files
|
|
42
|
+
update_status_history_file
|
|
42
43
|
download_statuses
|
|
43
44
|
find_board_ids.each do |id|
|
|
44
45
|
board = download_board_configuration board_id: id
|
|
@@ -66,7 +67,7 @@ class Downloader
|
|
|
66
67
|
|
|
67
68
|
def download_issues board:
|
|
68
69
|
log " Downloading primary issues for board #{board.id}", both: true
|
|
69
|
-
path =
|
|
70
|
+
path = File.join(@target_path, "#{file_prefix}_issues/")
|
|
70
71
|
unless Dir.exist?(path)
|
|
71
72
|
log " Creating path #{path}"
|
|
72
73
|
Dir.mkdir(path)
|
|
@@ -102,8 +103,6 @@ class Downloader
|
|
|
102
103
|
json = @jira_gateway.call_url relative_url: '/rest/api/2/search' \
|
|
103
104
|
"?jql=#{escaped_jql}&maxResults=#{max_results}&startAt=#{start_at}&expand=changelog&fields=*all"
|
|
104
105
|
|
|
105
|
-
exit_if_call_failed json
|
|
106
|
-
|
|
107
106
|
json['issues'].each do |issue_json|
|
|
108
107
|
issue_json['exporter'] = {
|
|
109
108
|
'in_initial_query' => initial_query
|
|
@@ -138,45 +137,62 @@ class Downloader
|
|
|
138
137
|
end
|
|
139
138
|
end
|
|
140
139
|
|
|
141
|
-
def exit_if_call_failed json
|
|
142
|
-
# Sometimes Jira returns the singular form of errorMessage and sometimes the plural. Consistency FTW.
|
|
143
|
-
return unless json['error'] || json['errorMessages'] || json['errorMessage']
|
|
144
|
-
|
|
145
|
-
log "Download failed. See #{@file_system.logfile_name} for details.", both: true
|
|
146
|
-
log " #{JSON.pretty_generate(json)}"
|
|
147
|
-
exit 1
|
|
148
|
-
end
|
|
149
|
-
|
|
150
140
|
def download_statuses
|
|
151
141
|
log ' Downloading all statuses', both: true
|
|
152
142
|
json = @jira_gateway.call_url relative_url: '/rest/api/2/status'
|
|
153
143
|
|
|
154
144
|
@file_system.save_json(
|
|
155
145
|
json: json,
|
|
156
|
-
filename:
|
|
146
|
+
filename: File.join(@target_path, "#{file_prefix}_statuses.json")
|
|
157
147
|
)
|
|
158
148
|
end
|
|
159
149
|
|
|
150
|
+
def update_status_history_file
|
|
151
|
+
status_filename = File.join(@target_path, "#{file_prefix}_statuses.json")
|
|
152
|
+
return unless file_system.file_exist? status_filename
|
|
153
|
+
|
|
154
|
+
status_json = file_system.load_json(status_filename)
|
|
155
|
+
|
|
156
|
+
history_filename = File.join(@target_path, "#{file_prefix}_status_history.json")
|
|
157
|
+
history_json = file_system.load_json(history_filename) if file_system.file_exist? history_filename
|
|
158
|
+
|
|
159
|
+
if history_json
|
|
160
|
+
file_system.log ' Updating status history file', also_write_to_stderr: true
|
|
161
|
+
else
|
|
162
|
+
file_system.log ' Creating status history file', also_write_to_stderr: true
|
|
163
|
+
history_json = []
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
status_json.each do |status_item|
|
|
167
|
+
id = status_item['id']
|
|
168
|
+
history_item = history_json.find { |s| s['id'] == id }
|
|
169
|
+
history_json.delete(history_item) if history_item
|
|
170
|
+
history_json << status_item
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
file_system.save_json(filename: history_filename, json: history_json)
|
|
174
|
+
end
|
|
175
|
+
|
|
160
176
|
def download_board_configuration board_id:
|
|
161
177
|
log " Downloading board configuration for board #{board_id}", both: true
|
|
162
178
|
json = @jira_gateway.call_url relative_url: "/rest/agile/1.0/board/#{board_id}/configuration"
|
|
163
179
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
180
|
+
@file_system.save_json(
|
|
181
|
+
json: json,
|
|
182
|
+
filename: File.join(@target_path, "#{file_prefix}_board_#{board_id}_configuration.json")
|
|
183
|
+
)
|
|
168
184
|
|
|
169
185
|
# We have a reported bug that blew up on this line. Moved it after the save so we can
|
|
170
186
|
# actually look at the returned json.
|
|
171
187
|
@board_id_to_filter_id[board_id] = json['filter']['id'].to_i
|
|
172
188
|
|
|
173
189
|
download_sprints board_id: board_id if json['type'] == 'scrum'
|
|
174
|
-
|
|
190
|
+
# TODO: Should be passing actual statuses, not empty list
|
|
191
|
+
Board.new raw: json, possible_statuses: StatusCollection.new
|
|
175
192
|
end
|
|
176
193
|
|
|
177
194
|
def download_sprints board_id:
|
|
178
195
|
log " Downloading sprints for board #{board_id}", both: true
|
|
179
|
-
file_prefix = @download_config.project_config.file_prefix
|
|
180
196
|
max_results = 100
|
|
181
197
|
start_at = 0
|
|
182
198
|
is_last = false
|
|
@@ -184,11 +200,10 @@ class Downloader
|
|
|
184
200
|
while is_last == false
|
|
185
201
|
json = @jira_gateway.call_url relative_url: "/rest/agile/1.0/board/#{board_id}/sprint?" \
|
|
186
202
|
"maxResults=#{max_results}&startAt=#{start_at}"
|
|
187
|
-
exit_if_call_failed json
|
|
188
203
|
|
|
189
204
|
@file_system.save_json(
|
|
190
205
|
json: json,
|
|
191
|
-
filename:
|
|
206
|
+
filename: File.join(@target_path, "#{file_prefix}_board_#{board_id}_sprints_#{start_at}.json")
|
|
192
207
|
)
|
|
193
208
|
is_last = json['isLast']
|
|
194
209
|
max_results = json['maxResults']
|
|
@@ -201,7 +216,7 @@ class Downloader
|
|
|
201
216
|
end
|
|
202
217
|
|
|
203
218
|
def metadata_pathname
|
|
204
|
-
|
|
219
|
+
File.join(@target_path, "#{file_prefix}_meta.json")
|
|
205
220
|
end
|
|
206
221
|
|
|
207
222
|
def load_metadata
|
|
@@ -244,17 +259,17 @@ class Downloader
|
|
|
244
259
|
end
|
|
245
260
|
|
|
246
261
|
def remove_old_files
|
|
247
|
-
file_prefix = @download_config.project_config.file_prefix
|
|
248
262
|
Dir.foreach @target_path do |file|
|
|
249
263
|
next unless file.match?(/^#{file_prefix}_\d+\.json$/)
|
|
264
|
+
next if file == "#{file_prefix}_status_history.json"
|
|
250
265
|
|
|
251
|
-
File.unlink
|
|
266
|
+
File.unlink File.join(@target_path, file)
|
|
252
267
|
end
|
|
253
268
|
|
|
254
269
|
return if @cached_data_format_is_current
|
|
255
270
|
|
|
256
271
|
# Also throw away all the previously downloaded issues.
|
|
257
|
-
path = File.join
|
|
272
|
+
path = File.join(@target_path, "#{file_prefix}_issues")
|
|
258
273
|
return unless File.exist? path
|
|
259
274
|
|
|
260
275
|
Dir.foreach path do |file|
|
|
@@ -292,4 +307,8 @@ class Downloader
|
|
|
292
307
|
|
|
293
308
|
segments.join ' AND '
|
|
294
309
|
end
|
|
310
|
+
|
|
311
|
+
def file_prefix
|
|
312
|
+
@download_config.project_config.get_file_prefix
|
|
313
|
+
end
|
|
295
314
|
end
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
# This file is really intended to give you ideas about how you might configure your own reports, not
|
|
4
4
|
# as a complete setup that will work in every case.
|
|
5
5
|
#
|
|
6
|
-
# See https://github.com/mikebowler/jirametrics/wiki/Examples-folder for more details
|
|
7
|
-
#
|
|
8
6
|
# The point of an AGGREGATED report is that we're now looking at a higher level. We might use this in a
|
|
9
7
|
# S2 meeting (Scrum of Scrums) to talk about the things that are happening across teams, not within a
|
|
10
8
|
# single team. For that reason, we look at slightly different things that we would on a single team board.
|
|
@@ -13,6 +11,7 @@ class Exporter
|
|
|
13
11
|
def aggregated_project name:, project_names:, settings: {}
|
|
14
12
|
project name: name do
|
|
15
13
|
puts name
|
|
14
|
+
file_prefix name
|
|
16
15
|
self.settings.merge! settings
|
|
17
16
|
|
|
18
17
|
aggregate do
|
|
@@ -21,8 +20,6 @@ class Exporter
|
|
|
21
20
|
end
|
|
22
21
|
end
|
|
23
22
|
|
|
24
|
-
file_prefix name
|
|
25
|
-
|
|
26
23
|
file do
|
|
27
24
|
file_suffix '.html'
|
|
28
25
|
issues.reject! do |issue|
|
|
@@ -34,7 +31,7 @@ class Exporter
|
|
|
34
31
|
board_lines = []
|
|
35
32
|
included_projects.each do |project|
|
|
36
33
|
project.all_boards.each_value do |board|
|
|
37
|
-
board_lines << "<a href='#{project.
|
|
34
|
+
board_lines << "<a href='#{project.get_file_prefix}.html'>#{board.name}</a> from project #{project.name}"
|
|
38
35
|
end
|
|
39
36
|
end
|
|
40
37
|
board_lines.sort.each { |line| html "<li>#{line}</li>", type: :header }
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
# This file is really intended to give you ideas about how you might configure your own reports, not
|
|
4
4
|
# as a complete setup that will work in every case.
|
|
5
|
-
#
|
|
6
|
-
# See https://github.com/mikebowler/jirametrics/wiki/Examples-folder for more
|
|
7
5
|
class Exporter
|
|
8
6
|
def standard_project name:, file_prefix:, ignore_issues: nil, starting_status: nil, boards: {},
|
|
9
7
|
default_board: nil, anonymize: false, settings: {}, status_category_mappings: {},
|
|
@@ -12,15 +10,15 @@ class Exporter
|
|
|
12
10
|
|
|
13
11
|
project name: name do
|
|
14
12
|
puts name
|
|
15
|
-
|
|
13
|
+
file_prefix file_prefix
|
|
16
14
|
|
|
15
|
+
self.anonymize if anonymize
|
|
17
16
|
self.settings.merge! settings
|
|
18
17
|
|
|
19
18
|
status_category_mappings.each do |status, category|
|
|
20
19
|
status_category_mapping status: status, category: category
|
|
21
20
|
end
|
|
22
21
|
|
|
23
|
-
file_prefix file_prefix
|
|
24
22
|
download do
|
|
25
23
|
self.rolling_date_count(rolling_date_count) if rolling_date_count
|
|
26
24
|
self.no_earlier_than(no_earlier_than) if no_earlier_than
|
|
@@ -30,8 +28,8 @@ class Exporter
|
|
|
30
28
|
block = boards[board_id]
|
|
31
29
|
if block == :default
|
|
32
30
|
block = lambda do |_|
|
|
33
|
-
start_at first_time_in_status_category(
|
|
34
|
-
stop_at still_in_status_category(
|
|
31
|
+
start_at first_time_in_status_category(:indeterminate)
|
|
32
|
+
stop_at still_in_status_category(:done)
|
|
35
33
|
end
|
|
36
34
|
end
|
|
37
35
|
board id: board_id do
|
|
@@ -63,7 +63,7 @@ class ExpeditedChart < ChartBase
|
|
|
63
63
|
next unless change.priority?
|
|
64
64
|
|
|
65
65
|
if expedited_priority_names.include? change.value
|
|
66
|
-
expedite_start = change.time
|
|
66
|
+
expedite_start = change.time.to_date
|
|
67
67
|
elsif expedite_start
|
|
68
68
|
start_date = expedite_start.to_date
|
|
69
69
|
stop_date = change.time.to_date
|
|
@@ -72,7 +72,7 @@ class ExpeditedChart < ChartBase
|
|
|
72
72
|
(start_date < date_range.begin && stop_date > date_range.end)
|
|
73
73
|
|
|
74
74
|
result << [expedite_start, :expedite_start]
|
|
75
|
-
result << [change.time, :expedite_stop]
|
|
75
|
+
result << [change.time.to_date, :expedite_stop]
|
|
76
76
|
end
|
|
77
77
|
expedite_start = nil
|
|
78
78
|
end
|
|
@@ -109,11 +109,11 @@ class ExpeditedChart < ChartBase
|
|
|
109
109
|
|
|
110
110
|
def make_expedite_lines_data_set issue:, expedite_data:
|
|
111
111
|
cycletime = issue.board.cycletime
|
|
112
|
-
|
|
112
|
+
started_date, stopped_date = cycletime.started_stopped_dates(issue)
|
|
113
113
|
|
|
114
|
-
expedite_data << [
|
|
115
|
-
expedite_data << [
|
|
116
|
-
expedite_data.sort_by!
|
|
114
|
+
expedite_data << [started_date, :issue_started] if started_date
|
|
115
|
+
expedite_data << [stopped_date, :issue_stopped] if stopped_date
|
|
116
|
+
expedite_data.sort_by!(&:first)
|
|
117
117
|
|
|
118
118
|
# If none of the data would be visible on the chart then skip it.
|
|
119
119
|
return nil unless expedite_data.any? { |time, _action| time.to_date >= date_range.begin }
|
|
@@ -150,7 +150,7 @@ class ExpeditedChart < ChartBase
|
|
|
150
150
|
|
|
151
151
|
unless expedite_data.empty?
|
|
152
152
|
last_change_time = expedite_data[-1][0].to_date
|
|
153
|
-
if last_change_time && last_change_time <= date_range.end &&
|
|
153
|
+
if last_change_time && last_change_time <= date_range.end && stopped_date.nil?
|
|
154
154
|
data << make_point(issue: issue, time: date_range.end, label: 'Still ongoing', expedited: expedited)
|
|
155
155
|
dot_colors << '' # It won't be visible so it doesn't matter
|
|
156
156
|
point_styles << 'dash'
|
data/lib/jirametrics/exporter.rb
CHANGED
|
@@ -2,18 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
|
|
5
|
-
class Object
|
|
6
|
-
def deprecated message:, date:
|
|
7
|
-
text = +''
|
|
8
|
-
text << "Deprecated(#{date}): "
|
|
9
|
-
text << message
|
|
10
|
-
caller(1..2).each do |line|
|
|
11
|
-
text << "\n-> Called from #{line}"
|
|
12
|
-
end
|
|
13
|
-
warn text
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
5
|
class Exporter
|
|
18
6
|
attr_reader :project_configs
|
|
19
7
|
attr_accessor :file_system
|
|
@@ -76,12 +64,8 @@ class Exporter
|
|
|
76
64
|
selected = []
|
|
77
65
|
each_project_config(name_filter: name_filter) do |project|
|
|
78
66
|
project.evaluate_next_level
|
|
79
|
-
# next if project.aggregated_project?
|
|
80
67
|
|
|
81
68
|
project.run load_only: true
|
|
82
|
-
project.board_configs.each do |board_config|
|
|
83
|
-
board_config.run
|
|
84
|
-
end
|
|
85
69
|
project.issues.each do |issue|
|
|
86
70
|
selected << [project, issue] if keys.include? issue.key
|
|
87
71
|
end
|
|
@@ -91,9 +75,13 @@ class Exporter
|
|
|
91
75
|
raise unless e.message.start_with? 'This is an aggregated project and issues should have been included'
|
|
92
76
|
end
|
|
93
77
|
|
|
94
|
-
selected.
|
|
95
|
-
|
|
96
|
-
|
|
78
|
+
if selected.empty?
|
|
79
|
+
file_system.log "No issues found to match #{keys.collect(&:inspect).join(', ')}"
|
|
80
|
+
else
|
|
81
|
+
selected.each do |project, issue|
|
|
82
|
+
file_system.log "\nProject #{project.name}", also_write_to_stderr: true
|
|
83
|
+
file_system.log issue.dump, also_write_to_stderr: true
|
|
84
|
+
end
|
|
97
85
|
end
|
|
98
86
|
end
|
|
99
87
|
|
|
@@ -128,7 +116,9 @@ class Exporter
|
|
|
128
116
|
|
|
129
117
|
def jira_config filename = nil
|
|
130
118
|
if filename
|
|
131
|
-
@jira_config = file_system.load_json(filename)
|
|
119
|
+
@jira_config = file_system.load_json(filename, fail_on_error: false)
|
|
120
|
+
raise "Unable to load Jira configuration file and cannot continue: #{filename.inspect}" if @jira_config.nil?
|
|
121
|
+
|
|
132
122
|
@jira_config['url'] = $1 if @jira_config['url'] =~ /^(.+)\/+$/
|
|
133
123
|
end
|
|
134
124
|
@jira_config
|
|
@@ -56,7 +56,7 @@ class FileConfig
|
|
|
56
56
|
def output_filename
|
|
57
57
|
segments = []
|
|
58
58
|
segments << project_config.target_path
|
|
59
|
-
segments << project_config.
|
|
59
|
+
segments << project_config.get_file_prefix
|
|
60
60
|
segments << (@file_suffix || "-#{@today}.csv")
|
|
61
61
|
segments.join
|
|
62
62
|
end
|
|
@@ -66,15 +66,20 @@ class FileConfig
|
|
|
66
66
|
# is that all empty values in the first column should be at the bottom.
|
|
67
67
|
def sort_output all_lines
|
|
68
68
|
all_lines.sort do |a, b|
|
|
69
|
+
result = nil
|
|
69
70
|
if a[0] == b[0]
|
|
70
|
-
a[1..] <=> b[1..]
|
|
71
|
+
result = a[1..] <=> b[1..]
|
|
71
72
|
elsif a[0].nil?
|
|
72
|
-
1
|
|
73
|
+
result = 1
|
|
73
74
|
elsif b[0].nil?
|
|
74
|
-
-1
|
|
75
|
+
result = -1
|
|
75
76
|
else
|
|
76
|
-
a[0] <=> b[0]
|
|
77
|
+
result = a[0] <=> b[0]
|
|
77
78
|
end
|
|
79
|
+
|
|
80
|
+
# This will only happen if one of the objects isn't comparable. Seen in production.
|
|
81
|
+
result = -1 if result.nil?
|
|
82
|
+
result
|
|
78
83
|
end
|
|
79
84
|
end
|
|
80
85
|
|
|
@@ -85,6 +90,11 @@ class FileConfig
|
|
|
85
90
|
|
|
86
91
|
def html_report &block
|
|
87
92
|
assert_only_one_filetype_config_set
|
|
93
|
+
if block.nil?
|
|
94
|
+
project_config.file_system.warning 'No charts were specified for the report. This is almost certainly a mistake.'
|
|
95
|
+
block = ->(_) {}
|
|
96
|
+
end
|
|
97
|
+
|
|
88
98
|
@html_report = HtmlReportConfig.new file_config: self, block: block
|
|
89
99
|
end
|
|
90
100
|
|
|
@@ -103,7 +113,7 @@ class FileConfig
|
|
|
103
113
|
def to_datetime object
|
|
104
114
|
return nil if object.nil?
|
|
105
115
|
|
|
106
|
-
object = object.to_datetime
|
|
116
|
+
object = object.to_time.to_datetime
|
|
107
117
|
object = object.new_offset(@timezone_offset) if @timezone_offset
|
|
108
118
|
object
|
|
109
119
|
end
|
|
@@ -120,4 +130,11 @@ class FileConfig
|
|
|
120
130
|
@file_suffix = suffix unless suffix.nil?
|
|
121
131
|
@file_suffix
|
|
122
132
|
end
|
|
133
|
+
|
|
134
|
+
def children
|
|
135
|
+
result = []
|
|
136
|
+
result << @columns if @columns
|
|
137
|
+
result << @html_report if @html_report
|
|
138
|
+
result
|
|
139
|
+
end
|
|
123
140
|
end
|
|
@@ -6,14 +6,18 @@ class FileSystem
|
|
|
6
6
|
attr_accessor :logfile, :logfile_name
|
|
7
7
|
|
|
8
8
|
# Effectively the same as File.read except it forces the encoding to UTF-8
|
|
9
|
-
def load filename
|
|
9
|
+
def load filename, supress_deprecation: false
|
|
10
|
+
if filename.end_with?('.json') && !supress_deprecation
|
|
11
|
+
deprecated(message: 'call load_json instead', date: '2024-11-13')
|
|
12
|
+
end
|
|
13
|
+
|
|
10
14
|
File.read filename, encoding: 'UTF-8'
|
|
11
15
|
end
|
|
12
16
|
|
|
13
17
|
def load_json filename, fail_on_error: true
|
|
14
18
|
return nil if fail_on_error == false && File.exist?(filename) == false
|
|
15
19
|
|
|
16
|
-
JSON.parse load(filename)
|
|
20
|
+
JSON.parse load(filename, supress_deprecation: true)
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
def save_json json:, filename:
|
|
@@ -27,9 +31,22 @@ class FileSystem
|
|
|
27
31
|
File.write(filename, content)
|
|
28
32
|
end
|
|
29
33
|
|
|
30
|
-
def
|
|
34
|
+
def warning message, more: nil
|
|
35
|
+
log "Warning: #{message}", more: more, also_write_to_stderr: true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def error message, more: nil
|
|
39
|
+
log "Error: #{message}", more: more, also_write_to_stderr: true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def log message, more: nil, also_write_to_stderr: false
|
|
43
|
+
message += " See #{logfile_name} for more details about this message." if more
|
|
44
|
+
|
|
31
45
|
logfile.puts message
|
|
32
|
-
|
|
46
|
+
logfile.puts more if more
|
|
47
|
+
return unless also_write_to_stderr
|
|
48
|
+
|
|
49
|
+
$stderr.puts message # rubocop:disable Style/StderrPuts
|
|
33
50
|
end
|
|
34
51
|
|
|
35
52
|
# In some Jira instances, a sizeable portion of the JSON is made up of empty fields. I've seen
|
|
@@ -43,4 +60,22 @@ class FileSystem
|
|
|
43
60
|
end
|
|
44
61
|
node
|
|
45
62
|
end
|
|
63
|
+
|
|
64
|
+
def foreach root, &block
|
|
65
|
+
Dir.foreach root, &block
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def file_exist? filename
|
|
69
|
+
File.exist? filename
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def deprecated message:, date:, depth: 2
|
|
73
|
+
text = +''
|
|
74
|
+
text << "Deprecated(#{date}): "
|
|
75
|
+
text << message
|
|
76
|
+
caller(1..depth).each do |line|
|
|
77
|
+
text << "\n-> Called from #{line}"
|
|
78
|
+
end
|
|
79
|
+
log text, also_write_to_stderr: true
|
|
80
|
+
end
|
|
46
81
|
end
|
|
@@ -19,17 +19,15 @@ class FlowEfficiencyScatterplot < ChartBase
|
|
|
19
19
|
</div>
|
|
20
20
|
<div class="p">
|
|
21
21
|
<math>
|
|
22
|
-
<mn>Flow efficiency</mn>
|
|
22
|
+
<mn>Flow efficiency (%)</mn>
|
|
23
23
|
<mo>=</mo>
|
|
24
24
|
<mfrac>
|
|
25
25
|
<mrow><mn>Time adding value</mn></mrow>
|
|
26
26
|
<mrow><mn>Total time</mn></mrow>
|
|
27
27
|
</mfrac>
|
|
28
|
-
<mo>x</mo>
|
|
29
|
-
<mn>100%</mn>
|
|
30
28
|
</math>
|
|
31
29
|
</div>
|
|
32
|
-
<div style="background:
|
|
30
|
+
<div style="background: var(--warning-banner)">Note that for this calculation to be accurate, we must be moving items into a
|
|
33
31
|
blocked or stalled state the moment we stop working on it, and most teams don't do that.
|
|
34
32
|
So be aware that your team may have to change their behaviours if you want this chart to be useful.
|
|
35
33
|
</div>
|
|
@@ -6,9 +6,7 @@ require 'jirametrics/grouping_rules'
|
|
|
6
6
|
module GroupableIssueChart
|
|
7
7
|
def init_configuration_block user_provided_block, &default_block
|
|
8
8
|
instance_eval(&user_provided_block)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
instance_eval(&default_block)
|
|
9
|
+
instance_eval(&default_block) unless @group_by_block
|
|
12
10
|
end
|
|
13
11
|
|
|
14
12
|
def grouping_rules &block
|
|
@@ -38,22 +38,13 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
38
38
|
plugins: {
|
|
39
39
|
annotation: {
|
|
40
40
|
annotations: {
|
|
41
|
-
|
|
42
|
-
holiday<%= index %>: {
|
|
43
|
-
drawTime: 'beforeDraw',
|
|
44
|
-
type: 'box',
|
|
45
|
-
xMin: '<%= range.begin %>T00:00:00',
|
|
46
|
-
xMax: '<%= range.end %>T23:59:59',
|
|
47
|
-
backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
|
|
48
|
-
borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
|
|
49
|
-
},
|
|
50
|
-
<% end %>
|
|
41
|
+
<%= working_days_annotation %>
|
|
51
42
|
|
|
52
43
|
<% if percentage_line_x %>
|
|
53
44
|
line: {
|
|
54
45
|
type: 'line',
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
scaleID: 'x',
|
|
47
|
+
value: '<%= percentage_line_x %>',
|
|
57
48
|
borderColor: <%= CssVariable.new('--aging-work-bar-chart-percentage-line-color').to_json %>,
|
|
58
49
|
borderWidth: 1,
|
|
59
50
|
drawTime: 'afterDraw'
|
|
@@ -6,7 +6,7 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
|
|
|
6
6
|
{
|
|
7
7
|
type: 'bar',
|
|
8
8
|
data: {
|
|
9
|
-
labels: [<%=
|
|
9
|
+
labels: [<%= @board_columns.collect { |c| c.name.inspect }.join(',') %>],
|
|
10
10
|
datasets: <%= JSON.generate(data_sets) %>
|
|
11
11
|
},
|
|
12
12
|
options: {
|
|
@@ -22,8 +22,10 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
|
|
|
22
22
|
labelString: 'Date Completed'
|
|
23
23
|
},
|
|
24
24
|
grid: {
|
|
25
|
-
color: <%= CssVariable['--grid-line-color'].to_json
|
|
25
|
+
color: <%= CssVariable['--grid-line-color'].to_json %>,
|
|
26
|
+
z: 1 // draw the grid lines on top of the bars
|
|
26
27
|
},
|
|
28
|
+
stacked: true
|
|
27
29
|
},
|
|
28
30
|
y: {
|
|
29
31
|
scaleLabel: {
|
|
@@ -35,8 +37,11 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
|
|
|
35
37
|
text: 'Age in days'
|
|
36
38
|
},
|
|
37
39
|
grid: {
|
|
38
|
-
color: <%= CssVariable['--grid-line-color'].to_json
|
|
40
|
+
color: <%= CssVariable['--grid-line-color'].to_json %>,
|
|
41
|
+
z: 1 // draw the grid lines on top of the bars
|
|
39
42
|
},
|
|
43
|
+
stacked: true,
|
|
44
|
+
max: <%= (@max_age * 1.1).to_i %>
|
|
40
45
|
}
|
|
41
46
|
},
|
|
42
47
|
plugins: {
|
|
@@ -44,14 +49,26 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
|
|
|
44
49
|
callbacks: {
|
|
45
50
|
label: function(context) {
|
|
46
51
|
if( typeof(context.dataset.data[context.dataIndex]) == "number" ) {
|
|
47
|
-
|
|
52
|
+
let full_data = <%= @bar_data.inspect %>;
|
|
53
|
+
let columnIndex = context.dataIndex;
|
|
54
|
+
let rowIndex = context.datasetIndex - <%= @row_index_offset %>;
|
|
55
|
+
return context.dataset.label + " of completed work items left this column in " +full_data[rowIndex][columnIndex] + " days or less";
|
|
48
56
|
}
|
|
49
57
|
else {
|
|
50
|
-
return context.dataset.data[context.dataIndex].title
|
|
58
|
+
return context.dataset.data[context.dataIndex].title;
|
|
51
59
|
}
|
|
52
60
|
}
|
|
53
61
|
}
|
|
62
|
+
},
|
|
63
|
+
legend: {
|
|
64
|
+
labels: {
|
|
65
|
+
filter: function(item, chart) {
|
|
66
|
+
// Logic to remove a particular legend item goes here
|
|
67
|
+
return !item.text.includes('%');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
54
70
|
}
|
|
71
|
+
|
|
55
72
|
}
|
|
56
73
|
}
|
|
57
74
|
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<table class='standard'>
|
|
2
2
|
<thead>
|
|
3
3
|
<tr>
|
|
4
|
-
<th
|
|
5
|
-
<th>E</th>
|
|
6
|
-
<th>B</th>
|
|
4
|
+
<th title="Age in days">Age</th>
|
|
5
|
+
<th title="Expedited">E</th>
|
|
6
|
+
<th title="Blocked / Stalled">B/S</th>
|
|
7
7
|
<th>Issue</th>
|
|
8
8
|
<th>Status</th>
|
|
9
|
+
<th>Forecast</th>
|
|
9
10
|
<th>Fix versions</th>
|
|
10
11
|
<% if any_scrum_boards %>
|
|
11
12
|
<th>Sprints</th>
|
|
@@ -40,7 +41,8 @@
|
|
|
40
41
|
</div>
|
|
41
42
|
<% end %>
|
|
42
43
|
</td>
|
|
43
|
-
<td><%= format_status issue.status
|
|
44
|
+
<td><%= format_status issue.status, board: issue.board %></td>
|
|
45
|
+
<td><%= dates_text(issue) %></td>
|
|
44
46
|
<td><%= fix_versions_text(issue) %></td>
|
|
45
47
|
<% if any_scrum_boards %>
|
|
46
48
|
<td><%= sprints_text(issue) %></td>
|