jirametrics 2.20.1 → 2.30
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/bin/jirametrics-mcp +5 -0
- data/lib/jirametrics/aggregate_config.rb +10 -2
- data/lib/jirametrics/aging_work_bar_chart.rb +191 -133
- data/lib/jirametrics/aging_work_in_progress_chart.rb +43 -11
- data/lib/jirametrics/aging_work_table.rb +9 -7
- data/lib/jirametrics/anonymizer.rb +74 -1
- data/lib/jirametrics/atlassian_document_format.rb +93 -93
- data/lib/jirametrics/bar_chart_range.rb +17 -0
- data/lib/jirametrics/blocked_stalled_change.rb +5 -3
- data/lib/jirametrics/board.rb +32 -8
- data/lib/jirametrics/board_config.rb +2 -1
- data/lib/jirametrics/board_feature.rb +14 -0
- data/lib/jirametrics/board_movement_calculator.rb +2 -2
- data/lib/jirametrics/cfd_data_builder.rb +108 -0
- data/lib/jirametrics/change_item.rb +13 -5
- data/lib/jirametrics/chart_base.rb +137 -2
- data/lib/jirametrics/css_variable.rb +1 -1
- data/lib/jirametrics/cumulative_flow_diagram.rb +208 -0
- data/lib/jirametrics/{cycletime_config.rb → cycle_time_config.rb} +4 -6
- data/lib/jirametrics/cycletime_histogram.rb +15 -103
- data/lib/jirametrics/cycletime_scatterplot.rb +17 -83
- data/lib/jirametrics/daily_view.rb +38 -13
- data/lib/jirametrics/daily_wip_by_age_chart.rb +4 -5
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +14 -4
- data/lib/jirametrics/daily_wip_by_parent_chart.rb +4 -2
- data/lib/jirametrics/daily_wip_chart.rb +30 -8
- data/lib/jirametrics/data_quality_report.rb +40 -12
- data/lib/jirametrics/dependency_chart.rb +2 -2
- data/lib/jirametrics/download_config.rb +15 -0
- data/lib/jirametrics/downloader.rb +87 -5
- data/lib/jirametrics/downloader_for_cloud.rb +107 -22
- data/lib/jirametrics/downloader_for_data_center.rb +3 -2
- data/lib/jirametrics/estimate_accuracy_chart.rb +42 -4
- data/lib/jirametrics/examples/aggregated_project.rb +2 -2
- data/lib/jirametrics/examples/standard_project.rb +32 -19
- data/lib/jirametrics/expedited_chart.rb +3 -1
- data/lib/jirametrics/exporter.rb +19 -4
- data/lib/jirametrics/file_config.rb +9 -11
- data/lib/jirametrics/file_system.rb +35 -2
- data/lib/jirametrics/fix_version.rb +13 -0
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +5 -1
- data/lib/jirametrics/github_gateway.rb +115 -0
- data/lib/jirametrics/groupable_issue_chart.rb +11 -1
- data/lib/jirametrics/grouping_rules.rb +26 -4
- data/lib/jirametrics/html/aging_work_bar_chart.erb +5 -5
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +2 -0
- data/lib/jirametrics/html/aging_work_table.erb +5 -0
- data/lib/jirametrics/html/cumulative_flow_diagram.erb +503 -0
- data/lib/jirametrics/html/daily_wip_chart.erb +40 -5
- data/lib/jirametrics/html/estimate_accuracy_chart.erb +4 -12
- data/lib/jirametrics/html/expedited_chart.erb +6 -14
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +4 -8
- data/lib/jirametrics/html/index.css +244 -59
- data/lib/jirametrics/html/index.erb +7 -1
- data/lib/jirametrics/html/index.js +77 -3
- data/lib/jirametrics/html/legacy_colors.css +174 -0
- data/lib/jirametrics/html/sprint_burndown.erb +12 -12
- data/lib/jirametrics/html/throughput_chart.erb +42 -11
- data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +61 -59
- data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +15 -11
- data/lib/jirametrics/html/wip_by_column_chart.erb +250 -0
- data/lib/jirametrics/html_generator.rb +32 -0
- data/lib/jirametrics/html_report_config.rb +49 -56
- data/lib/jirametrics/issue.rb +282 -91
- data/lib/jirametrics/issue_printer.rb +97 -0
- data/lib/jirametrics/jira_gateway.rb +32 -10
- data/lib/jirametrics/mcp_server.rb +531 -0
- data/lib/jirametrics/project_config.rb +98 -9
- data/lib/jirametrics/pull_request.rb +30 -0
- data/lib/jirametrics/pull_request_cycle_time_histogram.rb +77 -0
- data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +88 -0
- data/lib/jirametrics/pull_request_review.rb +13 -0
- data/lib/jirametrics/raw_javascript.rb +17 -0
- data/lib/jirametrics/settings.json +3 -1
- data/lib/jirametrics/sprint.rb +12 -0
- data/lib/jirametrics/sprint_burndown.rb +10 -4
- data/lib/jirametrics/status.rb +1 -1
- data/lib/jirametrics/stitcher.rb +81 -0
- data/lib/jirametrics/throughput_by_completed_resolution_chart.rb +22 -0
- data/lib/jirametrics/throughput_chart.rb +73 -23
- data/lib/jirametrics/time_based_histogram.rb +139 -0
- data/lib/jirametrics/time_based_scatterplot.rb +107 -0
- data/lib/jirametrics/wip_by_column_chart.rb +236 -0
- data/lib/jirametrics.rb +66 -1
- metadata +56 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a1f64f63f13e8cb59d3b18fb1e1ad90f77ca06d0e2f59a75ff7b7bae4db1870f
|
|
4
|
+
data.tar.gz: 9b7d6b8759102d7590e86c114d2ac8b1b2e7c4cc9f45a168002752196a6bf797
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8ec0bee468f8c34c001ea9151b0d78b1018d246cc86f9c2588a70ee55e3940b6263f69032884ec5c2dc596d3182909829f6ee63fe51b6c65444ce667bf70a6ca
|
|
7
|
+
data.tar.gz: 9b9f5337d0fc671639f9f651977cb2ecc60b2d27105cdce1b75a2a4188c093c111ba29ed880adccb515d548b8bd9209b4cf703482a0ede4ccea6b182212a3a57
|
data/bin/jirametrics-mcp
ADDED
|
@@ -65,8 +65,16 @@ class AggregateConfig
|
|
|
65
65
|
|
|
66
66
|
if issues.nil?
|
|
67
67
|
file_system.warning "No issues found for #{project_name}"
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
return
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
@project_config.add_issues issues
|
|
72
|
+
|
|
73
|
+
# Bring fix versions over
|
|
74
|
+
project.fix_versions.each do |fix_version|
|
|
75
|
+
unless @project_config.fix_versions.find { |fv| fv.id == fix_version.id }
|
|
76
|
+
@project_config.fix_versions << fix_version
|
|
77
|
+
end
|
|
70
78
|
end
|
|
71
79
|
end
|
|
72
80
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'jirametrics/chart_base'
|
|
4
|
+
require 'jirametrics/bar_chart_range'
|
|
4
5
|
|
|
5
6
|
class AgingWorkBarChart < ChartBase
|
|
6
7
|
def initialize block
|
|
7
8
|
super()
|
|
8
9
|
|
|
10
|
+
@age_cutoff = nil
|
|
9
11
|
header_text 'Aging Work Bar Chart'
|
|
10
12
|
description_text <<-HTML
|
|
11
13
|
<p>
|
|
@@ -13,16 +15,19 @@ class AgingWorkBarChart < ChartBase
|
|
|
13
15
|
newest at the bottom.
|
|
14
16
|
</p>
|
|
15
17
|
<p>
|
|
16
|
-
There are
|
|
17
|
-
information relevant to that. Hovering over any of the bars will provide more details.
|
|
18
|
+
There are <%= (aggregated_project? || current_board.scrum?) ? 'four' : 'three' %> bars for each issue, and hovering over any of the bars will provide more details.
|
|
18
19
|
<ol>
|
|
19
|
-
<li>The
|
|
20
|
+
<li>Status: The status the issue was in at any time. The colour indicates the
|
|
20
21
|
status category, which will be one of #{color_block '--status-category-todo-color'} To Do,
|
|
21
22
|
#{color_block '--status-category-inprogress-color'} In Progress,
|
|
22
23
|
or #{color_block '--status-category-done-color'} Done</li>
|
|
23
|
-
<li>
|
|
24
|
+
<li>Activity: This bar indicates #{color_block '--blocked-color'} blocked
|
|
24
25
|
or #{color_block '--stalled-color'} stalled.</li>
|
|
25
|
-
<li>
|
|
26
|
+
<li>Priority: This shows the priority over time. If one of these priorities is considered expedited
|
|
27
|
+
then it will be drawn with diagonal lines.</li>
|
|
28
|
+
<% if aggregated_project? || current_board.scrum? %>
|
|
29
|
+
<li>Sprints: The sprints that the issue was in.</li>
|
|
30
|
+
<% end %>
|
|
26
31
|
</ol>
|
|
27
32
|
</p>
|
|
28
33
|
#{describe_non_working_days}
|
|
@@ -36,6 +41,7 @@ class AgingWorkBarChart < ChartBase
|
|
|
36
41
|
|
|
37
42
|
def run
|
|
38
43
|
aging_issues = select_aging_issues issues: @issues
|
|
44
|
+
adjust_time_date_ranges_to_start_from_earliest_issue_start(aging_issues)
|
|
39
45
|
|
|
40
46
|
today = date_range.end
|
|
41
47
|
sort_by_age! issues: aging_issues, today: today
|
|
@@ -58,134 +64,134 @@ class AgingWorkBarChart < ChartBase
|
|
|
58
64
|
wrap_and_render(binding, __FILE__)
|
|
59
65
|
end
|
|
60
66
|
|
|
67
|
+
def adjust_time_date_ranges_to_start_from_earliest_issue_start aging_issues
|
|
68
|
+
earliest_start_time = aging_issues.collect do |issue|
|
|
69
|
+
issue.started_stopped_times.first
|
|
70
|
+
end.min
|
|
71
|
+
return if earliest_start_time.nil? || earliest_start_time >= @time_range.begin
|
|
72
|
+
|
|
73
|
+
@time_range = earliest_start_time..@time_range.end
|
|
74
|
+
@date_range = @time_range.begin.to_date..@time_range.end.to_date
|
|
75
|
+
end
|
|
76
|
+
|
|
61
77
|
def data_sets_for_one_issue issue:, today:
|
|
62
78
|
cycletime = issue.board.cycletime
|
|
63
|
-
issue_start_time
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
[
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
issue_label: issue_label,
|
|
71
|
-
stack: 'blocked',
|
|
72
|
-
issue_start_time: issue_start_time
|
|
73
|
-
),
|
|
74
|
-
data_set_by_block(
|
|
75
|
-
issue: issue,
|
|
76
|
-
issue_label: issue_label,
|
|
77
|
-
title_label: 'Expedited',
|
|
78
|
-
stack: 'expedited',
|
|
79
|
-
color: CssVariable['--expedited-color'],
|
|
80
|
-
start_date: issue_start_date
|
|
81
|
-
) { |day| issue.expedited_on_date?(day) }
|
|
79
|
+
issue_start_time = cycletime.started_stopped_times(issue).first
|
|
80
|
+
end_of_today = Time.parse("#{today}T23:59:59#{@timezone_offset}")
|
|
81
|
+
|
|
82
|
+
bar_data = [
|
|
83
|
+
['status', collect_status_ranges(issue: issue, now: end_of_today)],
|
|
84
|
+
['blocked', collect_blocked_stalled_ranges(issue: issue, issue_start_time: issue_start_time)],
|
|
85
|
+
['priority', collect_priority_ranges(issue: issue)]
|
|
82
86
|
]
|
|
87
|
+
bar_data << ['sprints', collect_sprint_ranges(issue: issue)] if aggregated_project? || current_board.scrum?
|
|
88
|
+
|
|
89
|
+
bar_data.each { |entry| clip_ranges_to_start_time(ranges: entry.last, issue_start_time: issue_start_time) }
|
|
90
|
+
|
|
91
|
+
issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
|
|
92
|
+
bar_data.collect do |stack, ranges|
|
|
93
|
+
bar_chart_range_to_data_set y_value: issue_label, ranges: ranges, stack: stack, issue_start_time: issue_start_time
|
|
94
|
+
end
|
|
83
95
|
end
|
|
84
96
|
|
|
85
97
|
def sort_by_age! issues:, today:
|
|
86
98
|
issues.sort! do |a, b|
|
|
87
|
-
|
|
99
|
+
b.board.cycletime.age(b, today: today) <=> a.board.cycletime.age(a, today: today)
|
|
88
100
|
end
|
|
89
101
|
end
|
|
90
102
|
|
|
91
103
|
def select_aging_issues issues:
|
|
92
104
|
issues.select do |issue|
|
|
93
|
-
started_time, stopped_time = issue.
|
|
94
|
-
started_time && stopped_time.nil?
|
|
105
|
+
started_time, stopped_time = issue.started_stopped_times
|
|
106
|
+
next false unless started_time && stopped_time.nil?
|
|
107
|
+
|
|
108
|
+
age = (date_range.end - started_time.to_date).to_i + 1
|
|
109
|
+
!(@age_cutoff && @age_cutoff >= age)
|
|
95
110
|
end
|
|
96
111
|
end
|
|
97
112
|
|
|
98
113
|
def grow_chart_height_if_too_many_issues aging_issue_count:
|
|
99
|
-
px_per_bar =
|
|
114
|
+
px_per_bar = 10
|
|
100
115
|
bars_per_issue = 3
|
|
116
|
+
bars_per_issue += 1 if aggregated_project? || current_board.scrum?
|
|
117
|
+
|
|
101
118
|
preferred_height = aging_issue_count * px_per_bar * bars_per_issue
|
|
102
119
|
@canvas_height = preferred_height if @canvas_height.nil? || @canvas_height < preferred_height
|
|
103
120
|
end
|
|
104
121
|
|
|
105
|
-
def
|
|
106
|
-
|
|
122
|
+
def clip_ranges_to_start_time ranges:, issue_start_time:
|
|
123
|
+
return if issue_start_time.nil?
|
|
107
124
|
|
|
108
|
-
|
|
125
|
+
ranges.each { |range| range.start = issue_start_time if range.start < issue_start_time }
|
|
126
|
+
ranges.reject! { |range| range.start >= range.stop }
|
|
127
|
+
end
|
|
109
128
|
|
|
129
|
+
def collect_status_ranges issue:, now:
|
|
130
|
+
ranges = []
|
|
131
|
+
issue_started_time = issue.started_stopped_times.first
|
|
110
132
|
previous_start = nil
|
|
111
133
|
previous_status = nil
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
unless previous_start.nil? || previous_start < issue_started_time
|
|
120
|
-
hash = {
|
|
121
|
-
type: 'bar',
|
|
122
|
-
data: [{
|
|
123
|
-
x: [chart_format(previous_start), chart_format(change.time)],
|
|
124
|
-
y: label,
|
|
125
|
-
title: "#{issue.type} : #{change.value}"
|
|
126
|
-
}],
|
|
127
|
-
backgroundColor: status_category_color(status),
|
|
128
|
-
borderColor: CssVariable['--aging-work-bar-chart-separator-color'],
|
|
129
|
-
borderWidth: {
|
|
130
|
-
top: 0,
|
|
131
|
-
right: 1,
|
|
132
|
-
bottom: 0,
|
|
133
|
-
left: 0
|
|
134
|
-
},
|
|
135
|
-
stacked: true,
|
|
136
|
-
stack: 'status'
|
|
137
|
-
}
|
|
138
|
-
data_sets << hash if date_range.include?(change.time.to_date)
|
|
134
|
+
issue.status_changes.each do |change|
|
|
135
|
+
new_status = issue.find_or_create_status id: change.value_id, name: change.value
|
|
136
|
+
if previous_start.nil?
|
|
137
|
+
previous_start = change.time
|
|
138
|
+
previous_status = new_status
|
|
139
|
+
next
|
|
139
140
|
end
|
|
140
141
|
|
|
142
|
+
previous_start = issue_started_time if issue_started_time > previous_start
|
|
143
|
+
|
|
144
|
+
ranges << BarChartRange.new(
|
|
145
|
+
start: previous_start,
|
|
146
|
+
stop: change.time,
|
|
147
|
+
color: status_category_color(previous_status),
|
|
148
|
+
title: previous_status.to_s
|
|
149
|
+
)
|
|
141
150
|
previous_start = change.time
|
|
142
|
-
previous_status =
|
|
151
|
+
previous_status = new_status
|
|
143
152
|
end
|
|
144
153
|
|
|
145
|
-
|
|
146
|
-
|
|
154
|
+
ranges << BarChartRange.new(
|
|
155
|
+
start: previous_start,
|
|
156
|
+
stop: now,
|
|
157
|
+
color: status_category_color(previous_status),
|
|
158
|
+
title: previous_status.to_s
|
|
159
|
+
)
|
|
160
|
+
ranges
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def bar_chart_range_to_data_set y_value:, ranges:, stack:, issue_start_time:
|
|
164
|
+
ranges.filter_map do |bar_chart_range|
|
|
165
|
+
next if bar_chart_range.stop < issue_start_time
|
|
166
|
+
|
|
167
|
+
background_color = bar_chart_range.color
|
|
168
|
+
if bar_chart_range.highlight
|
|
169
|
+
background_color = RawJavascript.new("createDiagonalPattern(#{background_color.to_json})")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
{
|
|
147
173
|
type: 'bar',
|
|
148
174
|
data: [{
|
|
149
|
-
x: [chart_format(
|
|
150
|
-
y:
|
|
151
|
-
title:
|
|
175
|
+
x: [chart_format([bar_chart_range.start, issue_start_time].max), chart_format(bar_chart_range.stop)],
|
|
176
|
+
y: y_value,
|
|
177
|
+
title: bar_chart_range.title
|
|
152
178
|
}],
|
|
153
|
-
backgroundColor:
|
|
179
|
+
backgroundColor: background_color,
|
|
180
|
+
borderColor: CssVariable['--aging-work-bar-chart-separator-color'],
|
|
181
|
+
borderWidth: {
|
|
182
|
+
top: 0,
|
|
183
|
+
right: 1,
|
|
184
|
+
bottom: 0,
|
|
185
|
+
left: 0
|
|
186
|
+
},
|
|
154
187
|
stacked: true,
|
|
155
|
-
stack:
|
|
188
|
+
stack: stack
|
|
156
189
|
}
|
|
157
190
|
end
|
|
158
|
-
|
|
159
|
-
data_sets
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def one_block_change_data_set starting_change:, ending_time:, issue_label:, stack:, issue_start_time:
|
|
163
|
-
if settings['blocked_color']
|
|
164
|
-
file_system.deprecated message: 'blocked color should be set via css now', date: '2024-05-03'
|
|
165
|
-
end
|
|
166
|
-
if settings['stalled_color']
|
|
167
|
-
file_system.deprecated message: 'stalled color should be set via css now', date: '2024-05-03'
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
color = settings['blocked_color'] || '--blocked-color'
|
|
171
|
-
color = settings['stalled_color'] || '--stalled-color' if starting_change.stalled?
|
|
172
|
-
{
|
|
173
|
-
backgroundColor: CssVariable[color],
|
|
174
|
-
data: [
|
|
175
|
-
{
|
|
176
|
-
title: starting_change.reasons,
|
|
177
|
-
x: [chart_format([issue_start_time, starting_change.time].max), chart_format(ending_time)],
|
|
178
|
-
y: issue_label
|
|
179
|
-
}
|
|
180
|
-
],
|
|
181
|
-
stack: stack,
|
|
182
|
-
stacked: true,
|
|
183
|
-
type: 'bar'
|
|
184
|
-
}
|
|
185
191
|
end
|
|
186
192
|
|
|
187
|
-
def
|
|
188
|
-
|
|
193
|
+
def collect_blocked_stalled_ranges issue:, issue_start_time:
|
|
194
|
+
results = []
|
|
189
195
|
starting_change = nil
|
|
190
196
|
|
|
191
197
|
issue.blocked_stalled_changes(end_time: time_range.end).each do |change|
|
|
@@ -195,58 +201,106 @@ class AgingWorkBarChart < ChartBase
|
|
|
195
201
|
end
|
|
196
202
|
|
|
197
203
|
if change.time >= issue_start_time
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
204
|
+
color = settings['blocked_color'] || '--blocked-color'
|
|
205
|
+
color = settings['stalled_color'] || '--stalled-color' if starting_change.stalled?
|
|
206
|
+
|
|
207
|
+
results << BarChartRange.new(
|
|
208
|
+
start: starting_change.time, stop: change.time, color: CssVariable[color], title: starting_change.reasons
|
|
201
209
|
)
|
|
202
210
|
end
|
|
203
211
|
|
|
204
212
|
starting_change = change
|
|
205
213
|
end
|
|
214
|
+
results
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def collect_priority_ranges issue:
|
|
218
|
+
expedited_priority_names = settings['expedited_priority_names']
|
|
219
|
+
|
|
220
|
+
previous_change = nil
|
|
221
|
+
results = []
|
|
222
|
+
|
|
223
|
+
issue.changes.each do |change|
|
|
224
|
+
next unless change.priority?
|
|
225
|
+
|
|
226
|
+
if previous_change.nil?
|
|
227
|
+
previous_change = change
|
|
228
|
+
next
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
results << create_range_for_priority(
|
|
232
|
+
previous_change: previous_change, stop_time: change.time,
|
|
233
|
+
expedited_priority_names: expedited_priority_names
|
|
234
|
+
)
|
|
235
|
+
previous_change = change
|
|
236
|
+
end
|
|
206
237
|
|
|
207
|
-
|
|
238
|
+
if previous_change
|
|
239
|
+
results << create_range_for_priority(
|
|
240
|
+
previous_change: previous_change, stop_time: time_range.end,
|
|
241
|
+
expedited_priority_names: expedited_priority_names
|
|
242
|
+
)
|
|
243
|
+
end
|
|
244
|
+
results
|
|
208
245
|
end
|
|
209
246
|
|
|
210
|
-
def
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
247
|
+
def collect_sprint_ranges issue:
|
|
248
|
+
results = []
|
|
249
|
+
open_sprints = {}
|
|
250
|
+
|
|
251
|
+
issue.changes.each do |change|
|
|
252
|
+
next unless change.sprint?
|
|
253
|
+
|
|
254
|
+
removed_sprint_ids = change.old_value_id - change.value_id
|
|
255
|
+
added_sprint_ids = change.value_id - change.old_value_id
|
|
256
|
+
|
|
257
|
+
removed_sprint_ids.each do |id|
|
|
258
|
+
data = open_sprints.delete(id)
|
|
259
|
+
next unless data
|
|
260
|
+
|
|
261
|
+
completed = data[:sprint].completed_time
|
|
262
|
+
stop = completed ? [change.time, completed].min : change.time
|
|
263
|
+
results << BarChartRange.new(
|
|
264
|
+
start: data[:start_time], stop: stop,
|
|
265
|
+
color: CssVariable['--sprint-color'], title: data[:sprint].name
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
added_sprint_ids.each do |id|
|
|
270
|
+
sprint = issue.board.sprints.find { |s| s.id == id }
|
|
271
|
+
next unless sprint
|
|
272
|
+
next if sprint.future?
|
|
273
|
+
|
|
274
|
+
start_time = [sprint.start_time, change.time].max
|
|
275
|
+
open_sprints[id] = { start_time: start_time, sprint: sprint }
|
|
230
276
|
end
|
|
231
277
|
end
|
|
232
278
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
279
|
+
open_sprints.each_value do |data|
|
|
280
|
+
next if data[:sprint].future?
|
|
281
|
+
|
|
282
|
+
stop = data[:sprint].completed_time || time_range.end
|
|
283
|
+
results << BarChartRange.new(
|
|
284
|
+
start: data[:start_time], stop: stop,
|
|
285
|
+
color: CssVariable['--sprint-color'], title: data[:sprint].name
|
|
286
|
+
)
|
|
239
287
|
end
|
|
240
288
|
|
|
241
|
-
|
|
289
|
+
results
|
|
290
|
+
end
|
|
242
291
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
292
|
+
def create_range_for_priority previous_change:, stop_time:, expedited_priority_names:
|
|
293
|
+
expedited = expedited_priority_names.include?(previous_change.value)
|
|
294
|
+
title = "Priority: #{previous_change.value}"
|
|
295
|
+
title << ' (expedited)' if expedited
|
|
296
|
+
|
|
297
|
+
BarChartRange.new(
|
|
298
|
+
start: previous_change.time,
|
|
299
|
+
stop: stop_time,
|
|
300
|
+
color: CssVariable["--priority-color-#{previous_change.value.downcase.gsub(/\s/, '')}"],
|
|
301
|
+
title: title,
|
|
302
|
+
highlight: expedited
|
|
303
|
+
)
|
|
250
304
|
end
|
|
251
305
|
|
|
252
306
|
def calculate_percent_line percentage: 85
|
|
@@ -255,4 +309,8 @@ class AgingWorkBarChart < ChartBase
|
|
|
255
309
|
|
|
256
310
|
days[days.length * percentage / 100]
|
|
257
311
|
end
|
|
312
|
+
|
|
313
|
+
def age_cutoff days
|
|
314
|
+
@age_cutoff = days
|
|
315
|
+
end
|
|
258
316
|
end
|
|
@@ -6,6 +6,7 @@ require 'jirametrics/board_movement_calculator'
|
|
|
6
6
|
|
|
7
7
|
class AgingWorkInProgressChart < ChartBase
|
|
8
8
|
include GroupableIssueChart
|
|
9
|
+
|
|
9
10
|
attr_accessor :possible_statuses, :board_id
|
|
10
11
|
attr_reader :board_columns
|
|
11
12
|
|
|
@@ -55,7 +56,7 @@ class AgingWorkInProgressChart < ChartBase
|
|
|
55
56
|
def run
|
|
56
57
|
determine_board_columns
|
|
57
58
|
|
|
58
|
-
@header_text += " on board: #{
|
|
59
|
+
@header_text += " on board: #{current_board.name}"
|
|
59
60
|
data_sets = make_data_sets
|
|
60
61
|
|
|
61
62
|
adjust_visibility_of_unmapped_status_column data_sets: data_sets
|
|
@@ -76,7 +77,7 @@ class AgingWorkInProgressChart < ChartBase
|
|
|
76
77
|
|
|
77
78
|
@fake_column = BoardColumn.new({
|
|
78
79
|
'name' => '[Unmapped Statuses]',
|
|
79
|
-
'statuses' => unmapped_statuses.collect { |id| { 'id' => id.to_s } }.uniq
|
|
80
|
+
'statuses' => unmapped_statuses.collect { |id| { 'id' => id.to_s } }.uniq
|
|
80
81
|
})
|
|
81
82
|
@board_columns = columns + [@fake_column]
|
|
82
83
|
end
|
|
@@ -114,14 +115,7 @@ class AgingWorkInProgressChart < ChartBase
|
|
|
114
115
|
|
|
115
116
|
calculator = BoardMovementCalculator.new board: @all_boards[@board_id], issues: issues, today: date_range.end
|
|
116
117
|
|
|
117
|
-
column_indexes_to_remove =
|
|
118
|
-
unless @show_all_columns
|
|
119
|
-
column_indexes_to_remove = indexes_of_leading_and_trailing_zeros(calculator.age_data_for(percentage: 100))
|
|
120
|
-
|
|
121
|
-
column_indexes_to_remove.reverse_each do |index|
|
|
122
|
-
@board_columns.delete_at index
|
|
123
|
-
end
|
|
124
|
-
end
|
|
118
|
+
column_indexes_to_remove = trim_board_columns data_sets: data_sets, calculator: calculator
|
|
125
119
|
|
|
126
120
|
@row_index_offset = data_sets.size
|
|
127
121
|
|
|
@@ -177,6 +171,44 @@ class AgingWorkInProgressChart < ChartBase
|
|
|
177
171
|
result
|
|
178
172
|
end
|
|
179
173
|
|
|
174
|
+
def trim_board_columns data_sets:, calculator:
|
|
175
|
+
return [] if @show_all_columns
|
|
176
|
+
|
|
177
|
+
columns_with_aging_items = data_sets.flat_map do |set|
|
|
178
|
+
set['data'].filter_map { |d| d['x'] if d.is_a? Hash }
|
|
179
|
+
end.uniq
|
|
180
|
+
|
|
181
|
+
# @fake_column is always the last element and is handled separately.
|
|
182
|
+
real_column_count = @board_columns.size - 1
|
|
183
|
+
|
|
184
|
+
# The last visible column always has artificially inflated age_data because
|
|
185
|
+
# ages_of_issues_when_leaving_column uses `today` as end_date when there is no
|
|
186
|
+
# next column. Exclude it from the right-boundary search so it is only kept when
|
|
187
|
+
# it has current aging items (handled by the last_aging fallback below).
|
|
188
|
+
age_data = calculator.age_data_for(percentage: 100)
|
|
189
|
+
last_data = (0...(real_column_count - 1)).to_a.reverse.find { |i| !age_data[i].zero? }
|
|
190
|
+
|
|
191
|
+
in_current = ->(i) { columns_with_aging_items.include?(@board_columns[i].name) }
|
|
192
|
+
first_aging = (0...real_column_count).find(&in_current)
|
|
193
|
+
last_aging = (0...real_column_count).to_a.reverse.find(&in_current)
|
|
194
|
+
|
|
195
|
+
# Combine: include any column with age_data (up to but not including the last visible
|
|
196
|
+
# column) and any column with current aging items.
|
|
197
|
+
first_data = (0...real_column_count).find { |i| !age_data[i].zero? }
|
|
198
|
+
left_bound = [first_data, first_aging].compact.min
|
|
199
|
+
right_bound = [last_data, last_aging].compact.max
|
|
200
|
+
|
|
201
|
+
indexes_to_remove =
|
|
202
|
+
if left_bound && right_bound
|
|
203
|
+
(0...left_bound).to_a + ((right_bound + 1)...real_column_count).to_a
|
|
204
|
+
else
|
|
205
|
+
(0...real_column_count).to_a
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
indexes_to_remove.reverse_each { |index| @board_columns.delete_at index }
|
|
209
|
+
indexes_to_remove
|
|
210
|
+
end
|
|
211
|
+
|
|
180
212
|
def column_for issue:
|
|
181
213
|
@board_columns.find do |board_column|
|
|
182
214
|
board_column.status_ids.include? issue.status.id
|
|
@@ -192,7 +224,7 @@ class AgingWorkInProgressChart < ChartBase
|
|
|
192
224
|
end
|
|
193
225
|
end
|
|
194
226
|
|
|
195
|
-
if has_unmapped
|
|
227
|
+
if has_unmapped && @description_text
|
|
196
228
|
@description_text += "<p>The items shown in #{column_name.inspect} are not visible on the " \
|
|
197
229
|
'board but are still active. Most likely everyone has forgotten about them.</p>'
|
|
198
230
|
else
|
|
@@ -45,20 +45,21 @@ class AgingWorkTable < ChartBase
|
|
|
45
45
|
# This is its own method simply so the tests can initialize the calculator without doing a full run.
|
|
46
46
|
def initialize_calculator
|
|
47
47
|
@today = date_range.end
|
|
48
|
-
@
|
|
48
|
+
@calculators = @all_boards.transform_values do |board|
|
|
49
|
+
BoardMovementCalculator.new board: board, issues: issues, today: @today
|
|
50
|
+
end
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
def expedited_but_not_started
|
|
52
54
|
@issues.select do |issue|
|
|
53
|
-
started_time, stopped_time = issue.
|
|
55
|
+
started_time, stopped_time = issue.started_stopped_times
|
|
54
56
|
started_time.nil? && stopped_time.nil? && issue.expedited?
|
|
55
57
|
end.sort_by(&:created)
|
|
56
58
|
end
|
|
57
59
|
|
|
58
60
|
def select_aging_issues
|
|
59
61
|
aging_issues = @issues.select do |issue|
|
|
60
|
-
|
|
61
|
-
started, stopped = cycletime.started_stopped_times(issue)
|
|
62
|
+
started, stopped = issue.started_stopped_times
|
|
62
63
|
next false if started.nil? || stopped
|
|
63
64
|
next true if issue.blocked_on_date?(@today, end_time: time_range.end) || issue.expedited?
|
|
64
65
|
|
|
@@ -77,7 +78,7 @@ class AgingWorkTable < ChartBase
|
|
|
77
78
|
end
|
|
78
79
|
|
|
79
80
|
def blocked_text issue
|
|
80
|
-
started_time, _stopped_time = issue.
|
|
81
|
+
started_time, _stopped_time = issue.started_stopped_times
|
|
81
82
|
return nil if started_time.nil?
|
|
82
83
|
|
|
83
84
|
current = issue.blocked_stalled_changes(end_time: time_range.end)[-1]
|
|
@@ -124,7 +125,8 @@ class AgingWorkTable < ChartBase
|
|
|
124
125
|
due = issue.due_date
|
|
125
126
|
message = nil
|
|
126
127
|
|
|
127
|
-
|
|
128
|
+
calculator = @calculators[issue.board.id]
|
|
129
|
+
days_remaining, error = calculator.forecasted_days_remaining_and_message issue: issue, today: @today
|
|
128
130
|
|
|
129
131
|
unless error
|
|
130
132
|
if due
|
|
@@ -174,6 +176,6 @@ class AgingWorkTable < ChartBase
|
|
|
174
176
|
end
|
|
175
177
|
|
|
176
178
|
def priority_text issue
|
|
177
|
-
"<img src='#{issue.priority_url}' title='Priority: #{issue.priority_name}' />"
|
|
179
|
+
"<img src='#{issue.priority_url}' title='Priority: #{issue.priority_name}' style='max-width: 1em;'/>"
|
|
178
180
|
end
|
|
179
181
|
end
|