jirametrics 2.25.1 → 2.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/jirametrics/aging_work_bar_chart.rb +4 -4
- data/lib/jirametrics/cfd_data_builder.rb +5 -0
- data/lib/jirametrics/cycletime_scatterplot.rb +4 -0
- data/lib/jirametrics/downloader.rb +17 -7
- data/lib/jirametrics/downloader_for_cloud.rb +13 -10
- data/lib/jirametrics/file_system.rb +22 -0
- data/lib/jirametrics/groupable_issue_chart.rb +2 -0
- data/lib/jirametrics/grouping_rules.rb +5 -1
- data/lib/jirametrics/html_generator.rb +1 -0
- data/lib/jirametrics/project_config.rb +10 -0
- data/lib/jirametrics/throughput_chart.rb +18 -2
- data/lib/jirametrics/time_based_scatterplot.rb +8 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b7ae2f8704310fc2fcea52150053a46c0b42cfa70f1a70c3852ec1ed70f7a2ae
|
|
4
|
+
data.tar.gz: 60401ec8596603ee4ba7d6491286a577ea51a3a888d8cc9ea1de5ee7c281dbe7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4b4cd517bb7be4fc1e660b7d3950dfc1ed48125c5fa7334b5d19c115c85a29915fbbf0c02f142a79f68a401a86913058b84e3dfb00dd5ae9d7c39986c3ff6bb3
|
|
7
|
+
data.tar.gz: 6200ef8b7ab73aaad69986fb1f38c1bc7df2ca280c3c8559fc83093276b2b7c628bb91b3d7850464003c01ced5eea839cde155688e1f1cf1e0a42b533bc09244
|
|
@@ -15,7 +15,7 @@ class AgingWorkBarChart < ChartBase
|
|
|
15
15
|
newest at the bottom.
|
|
16
16
|
</p>
|
|
17
17
|
<p>
|
|
18
|
-
There are <%= current_board.scrum? ? 'four' : 'three' %> bars for each issue, and hovering over any of the bars will provide more details.
|
|
18
|
+
There are <%= (current_board.scrum? || aggregated_project?) ? 'four' : 'three' %> bars for each issue, and hovering over any of the bars will provide more details.
|
|
19
19
|
<ol>
|
|
20
20
|
<li>Status: The status the issue was in at any time. The colour indicates the
|
|
21
21
|
status category, which will be one of #{color_block '--status-category-todo-color'} To Do,
|
|
@@ -25,7 +25,7 @@ class AgingWorkBarChart < ChartBase
|
|
|
25
25
|
or #{color_block '--stalled-color'} stalled.</li>
|
|
26
26
|
<li>Priority: This shows the priority over time. If one of these priorities is considered expedited
|
|
27
27
|
then it will be drawn with diagonal lines.</li>
|
|
28
|
-
<% if current_board.scrum? %>
|
|
28
|
+
<% if current_board.scrum? || aggregated_project? %>
|
|
29
29
|
<li>Sprints: The sprints that the issue was in.</li>
|
|
30
30
|
<% end %>
|
|
31
31
|
</ol>
|
|
@@ -84,7 +84,7 @@ class AgingWorkBarChart < ChartBase
|
|
|
84
84
|
['blocked', collect_blocked_stalled_ranges(issue: issue, issue_start_time: issue_start_time)],
|
|
85
85
|
['priority', collect_priority_ranges(issue: issue)]
|
|
86
86
|
]
|
|
87
|
-
bar_data << ['sprints', collect_sprint_ranges(issue: issue)] if current_board.scrum?
|
|
87
|
+
bar_data << ['sprints', collect_sprint_ranges(issue: issue)] if current_board.scrum? || aggregated_project?
|
|
88
88
|
|
|
89
89
|
bar_data.each { |entry| clip_ranges_to_start_time(ranges: entry.last, issue_start_time: issue_start_time) }
|
|
90
90
|
|
|
@@ -113,7 +113,7 @@ class AgingWorkBarChart < ChartBase
|
|
|
113
113
|
def grow_chart_height_if_too_many_issues aging_issue_count:
|
|
114
114
|
px_per_bar = 10
|
|
115
115
|
bars_per_issue = 3
|
|
116
|
-
bars_per_issue += 1 if current_board.scrum?
|
|
116
|
+
bars_per_issue += 1 if current_board.scrum? || aggregated_project?
|
|
117
117
|
|
|
118
118
|
preferred_height = aging_issue_count * px_per_bar * bars_per_issue
|
|
119
119
|
@canvas_height = preferred_height if @canvas_height.nil? || @canvas_height < preferred_height
|
|
@@ -31,12 +31,17 @@ class CfdDataBuilder
|
|
|
31
31
|
|
|
32
32
|
# Returns { hwm_timeline: [[date, hwm_value], ...], correction_windows: [...] }
|
|
33
33
|
def process_issue issue, column_map
|
|
34
|
+
start_time = issue.started_stopped_times.first
|
|
35
|
+
return { hwm_timeline: [], correction_windows: [] } if start_time.nil?
|
|
36
|
+
|
|
34
37
|
high_water_mark = nil
|
|
35
38
|
correction_open_since = nil
|
|
36
39
|
correction_windows = []
|
|
37
40
|
hwm_timeline = [] # sorted chronologically by date
|
|
38
41
|
|
|
39
42
|
issue.status_changes.each do |change|
|
|
43
|
+
next if change.time < start_time
|
|
44
|
+
|
|
40
45
|
col_index = column_map[change.value_id]
|
|
41
46
|
next if col_index.nil?
|
|
42
47
|
|
|
@@ -35,6 +35,10 @@ class CycletimeScatterplot < TimeBasedScatterplot
|
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
def minimum_y_value
|
|
39
|
+
1 # Values under 1 day are data quality problems; they're flagged in the quality report instead
|
|
40
|
+
end
|
|
41
|
+
|
|
38
42
|
def all_items
|
|
39
43
|
completed_issues_in_range include_unstarted: false
|
|
40
44
|
end
|
|
@@ -87,6 +87,23 @@ class Downloader
|
|
|
87
87
|
@file_system.log text, also_write_to_stderr: both
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
def log_start text
|
|
91
|
+
@file_system.log_start text
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def start_progress
|
|
95
|
+
@file_system.start_progress
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def progress_dot message = nil
|
|
99
|
+
@file_system.log message if message
|
|
100
|
+
@file_system.progress_dot
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def end_progress
|
|
104
|
+
@file_system.end_progress
|
|
105
|
+
end
|
|
106
|
+
|
|
90
107
|
def find_board_ids
|
|
91
108
|
ids = @download_config.project_config.board_configs.collect(&:id)
|
|
92
109
|
raise 'Board ids must be specified' if ids.empty?
|
|
@@ -233,13 +250,6 @@ class Downloader
|
|
|
233
250
|
@metadata[key] = value
|
|
234
251
|
end
|
|
235
252
|
|
|
236
|
-
# If rolling_date_count has changed, we may be missing data outside the previous range,
|
|
237
|
-
# so force a full re-download.
|
|
238
|
-
if @metadata['rolling_date_count'] != @download_config.rolling_date_count
|
|
239
|
-
log ' rolling_date_count has changed. Forcing a full download.', both: true
|
|
240
|
-
@cached_data_format_is_current = false
|
|
241
|
-
@metadata = {}
|
|
242
|
-
end
|
|
243
253
|
end
|
|
244
254
|
|
|
245
255
|
# Even if this is the old format, we want to obey this one tag
|
|
@@ -53,6 +53,7 @@ class DownloaderForCloud < Downloader
|
|
|
53
53
|
next_page_token = nil
|
|
54
54
|
issue_count = 0
|
|
55
55
|
|
|
56
|
+
start_progress
|
|
56
57
|
loop do
|
|
57
58
|
relative_url = +''
|
|
58
59
|
relative_url << '/rest/api/3/search/jql'
|
|
@@ -75,11 +76,12 @@ class DownloaderForCloud < Downloader
|
|
|
75
76
|
issue_count += 1
|
|
76
77
|
end
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
log message, both: true
|
|
79
|
+
progress_dot " Found #{issue_count} issues"
|
|
80
80
|
|
|
81
81
|
break unless next_page_token
|
|
82
82
|
end
|
|
83
|
+
end_progress
|
|
84
|
+
|
|
83
85
|
hash
|
|
84
86
|
end
|
|
85
87
|
|
|
@@ -88,7 +90,7 @@ class DownloaderForCloud < Downloader
|
|
|
88
90
|
# that only returns the "recent" changes, not all of them. So now we get the issue
|
|
89
91
|
# without changes and then make a second call for that changes. Then we insert it
|
|
90
92
|
# into the raw issue as if it had been there all along.
|
|
91
|
-
log " Downloading #{issue_datas.size} issues"
|
|
93
|
+
log " Downloading #{issue_datas.size} issues"
|
|
92
94
|
payload = {
|
|
93
95
|
'fields' => ['*all'],
|
|
94
96
|
'issueIdsOrKeys' => issue_datas.collect(&:key)
|
|
@@ -168,13 +170,12 @@ class DownloaderForCloud < Downloader
|
|
|
168
170
|
|
|
169
171
|
loop do
|
|
170
172
|
related_issue_keys = Set.new
|
|
171
|
-
issue_data_hash
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
.each_slice(100) do |slice|
|
|
175
|
-
slice = bulk_fetch_issues(
|
|
176
|
-
|
|
177
|
-
)
|
|
173
|
+
stale = issue_data_hash.values.reject { |data| data.up_to_date }
|
|
174
|
+
unless stale.empty?
|
|
175
|
+
log_start ' Downloading more issues '
|
|
176
|
+
stale.each_slice(100) do |slice|
|
|
177
|
+
slice = bulk_fetch_issues(issue_datas: slice, board: board, in_initial_query: true)
|
|
178
|
+
progress_dot
|
|
178
179
|
slice.each do |data|
|
|
179
180
|
@file_system.save_json(
|
|
180
181
|
json: data.issue.raw, filename: data.cache_path
|
|
@@ -195,6 +196,8 @@ class DownloaderForCloud < Downloader
|
|
|
195
196
|
end
|
|
196
197
|
end
|
|
197
198
|
end
|
|
199
|
+
end_progress
|
|
200
|
+
end
|
|
198
201
|
|
|
199
202
|
# Remove all the ones we already downloaded
|
|
200
203
|
related_issue_keys.reject! { |key| issue_data_hash[key] }
|
|
@@ -68,6 +68,28 @@ class FileSystem
|
|
|
68
68
|
$stderr.puts message # rubocop:disable Style/StderrPuts
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
def log_start message
|
|
72
|
+
logfile.puts message
|
|
73
|
+
return if logfile == $stdout
|
|
74
|
+
|
|
75
|
+
$stderr.print message
|
|
76
|
+
$stderr.flush
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def start_progress
|
|
80
|
+
$stderr.print ' '
|
|
81
|
+
$stderr.flush
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def progress_dot
|
|
85
|
+
$stderr.print '.'
|
|
86
|
+
$stderr.flush
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def end_progress
|
|
90
|
+
$stderr.puts '' # rubocop:disable Style/StderrPuts
|
|
91
|
+
end
|
|
92
|
+
|
|
71
93
|
# In some Jira instances, a sizeable portion of the JSON is made up of empty fields. I've seen
|
|
72
94
|
# cases where this simple compression will drop the filesize by half.
|
|
73
95
|
def compress node
|
|
@@ -17,6 +17,7 @@ module GroupableIssueChart
|
|
|
17
17
|
result = {}
|
|
18
18
|
ignored_issues = []
|
|
19
19
|
@issue_hints = {}
|
|
20
|
+
@issue_periods = {}
|
|
20
21
|
completed_issues.each do |issue|
|
|
21
22
|
rules = GroupingRules.new
|
|
22
23
|
@group_by_block.call(issue, rules)
|
|
@@ -26,6 +27,7 @@ module GroupableIssueChart
|
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
@issue_hints[issue] = rules.issue_hint
|
|
30
|
+
@issue_periods[issue] = rules.last_day_of_period
|
|
29
31
|
(result[rules] ||= []) << issue
|
|
30
32
|
end
|
|
31
33
|
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
class GroupingRules < Rules
|
|
4
4
|
attr_accessor :label, :issue_hint, :label_hint
|
|
5
|
-
attr_reader :color
|
|
5
|
+
attr_reader :color, :last_day_of_period
|
|
6
|
+
|
|
7
|
+
def last_day_of_period= value
|
|
8
|
+
@last_day_of_period = value.is_a?(String) ? Date.parse(value) : value
|
|
9
|
+
end
|
|
6
10
|
|
|
7
11
|
def eql? other
|
|
8
12
|
other.label == @label && other.color == @color
|
|
@@ -5,6 +5,7 @@ class HtmlGenerator
|
|
|
5
5
|
|
|
6
6
|
def create_html output_filename:, settings:, project_name: ''
|
|
7
7
|
@settings = settings
|
|
8
|
+
project_name = project_name.to_s
|
|
8
9
|
html_directory = "#{Pathname.new(File.realpath(__FILE__)).dirname}/html"
|
|
9
10
|
css = load_css html_directory: html_directory
|
|
10
11
|
javascript = file_system.load(File.join(html_directory, 'index.js'))
|
|
@@ -598,6 +598,16 @@ class ProjectConfig
|
|
|
598
598
|
if status_becomes
|
|
599
599
|
status_becomes = [status_becomes] unless status_becomes.is_a? Array
|
|
600
600
|
|
|
601
|
+
status_becomes.each do |status_name|
|
|
602
|
+
next if status_name == :backlog
|
|
603
|
+
|
|
604
|
+
found = possible_statuses.find_all_by_name status_name
|
|
605
|
+
if found.empty?
|
|
606
|
+
raise "discard_changes_before: Status #{status_name.inspect} not found. " \
|
|
607
|
+
"Possible statuses are: #{possible_statuses}"
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
|
|
601
611
|
block = lambda do |issue|
|
|
602
612
|
trigger_statuses = status_becomes.collect do |status_name|
|
|
603
613
|
if status_name == :backlog
|
|
@@ -79,12 +79,21 @@ class ThroughputChart < ChartBase
|
|
|
79
79
|
end
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
def calculate_custom_periods
|
|
83
|
+
last_days = @issue_periods.values.compact.uniq.sort
|
|
84
|
+
last_days.each_with_index.map do |last_day, i|
|
|
85
|
+
first_day = i.zero? ? @date_range.begin : last_days[i - 1] + 1
|
|
86
|
+
first_day..last_day
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
82
90
|
def weekly_throughput_dataset completed_issues:, label:, color:, dashed: false, label_hint: nil
|
|
91
|
+
periods = @issue_periods&.values&.any? ? calculate_custom_periods : calculate_time_periods
|
|
83
92
|
result = {
|
|
84
93
|
label: label,
|
|
85
94
|
label_hint: label_hint,
|
|
86
95
|
data: throughput_dataset(
|
|
87
|
-
periods:
|
|
96
|
+
periods: periods, completed_issues: completed_issues, label_hint: label_hint
|
|
88
97
|
),
|
|
89
98
|
fill: false,
|
|
90
99
|
showLine: true,
|
|
@@ -109,10 +118,17 @@ class ThroughputChart < ChartBase
|
|
|
109
118
|
end
|
|
110
119
|
|
|
111
120
|
def throughput_dataset periods:, completed_issues:, label_hint: nil
|
|
121
|
+
custom_mode = @issue_periods&.values&.any?
|
|
112
122
|
periods.collect do |period|
|
|
113
123
|
closed_issues = completed_issues.filter_map do |issue|
|
|
114
124
|
stop_date = issue.started_stopped_dates.last
|
|
115
|
-
|
|
125
|
+
next unless stop_date
|
|
126
|
+
|
|
127
|
+
if custom_mode
|
|
128
|
+
[stop_date, issue] if @issue_periods[issue] == period.end
|
|
129
|
+
elsif period.include?(stop_date)
|
|
130
|
+
[stop_date, issue]
|
|
131
|
+
end
|
|
116
132
|
end
|
|
117
133
|
|
|
118
134
|
date_label = "on #{period.end}"
|
|
@@ -79,9 +79,14 @@ class TimeBasedScatterplot < ChartBase
|
|
|
79
79
|
}
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
def minimum_y_value
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
82
86
|
def data_for_item item, rules: nil
|
|
83
87
|
y = y_value(item)
|
|
84
|
-
|
|
88
|
+
min = minimum_y_value
|
|
89
|
+
return nil if min && y < min
|
|
85
90
|
|
|
86
91
|
@highest_y_value = y if @highest_y_value < y
|
|
87
92
|
|
|
@@ -93,7 +98,9 @@ class TimeBasedScatterplot < ChartBase
|
|
|
93
98
|
end
|
|
94
99
|
|
|
95
100
|
def calculate_percent_line items
|
|
101
|
+
min = minimum_y_value
|
|
96
102
|
times = items.collect { |item| y_value(item) }
|
|
103
|
+
times.reject! { |y| min && y < min }
|
|
97
104
|
index = times.size * 85 / 100
|
|
98
105
|
times.sort[index]
|
|
99
106
|
end
|