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
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'jirametrics/
|
|
4
|
-
|
|
5
|
-
class CycletimeScatterplot < ChartBase
|
|
6
|
-
include GroupableIssueChart
|
|
3
|
+
require 'jirametrics/time_based_scatterplot'
|
|
7
4
|
|
|
5
|
+
class CycletimeScatterplot < TimeBasedScatterplot
|
|
8
6
|
attr_accessor :possible_statuses
|
|
9
7
|
|
|
10
8
|
def initialize block
|
|
@@ -26,6 +24,8 @@ class CycletimeScatterplot < ChartBase
|
|
|
26
24
|
</div>
|
|
27
25
|
#{describe_non_working_days}
|
|
28
26
|
HTML
|
|
27
|
+
@x_axis_title = 'Date completed'
|
|
28
|
+
@y_axis_title = 'Cycletime in days'
|
|
29
29
|
|
|
30
30
|
init_configuration_block block do
|
|
31
31
|
grouping_rules do |issue, rule|
|
|
@@ -33,95 +33,29 @@ class CycletimeScatterplot < ChartBase
|
|
|
33
33
|
rule.color = color_for type: issue.type
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
|
-
|
|
37
|
-
@percentage_lines = []
|
|
38
|
-
@highest_cycletime = 0
|
|
39
36
|
end
|
|
40
37
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
data_sets = create_datasets completed_issues
|
|
45
|
-
overall_percent_line = calculate_percent_line(completed_issues)
|
|
46
|
-
@percentage_lines << [overall_percent_line, CssVariable['--cycletime-scatterplot-overall-trendline-color']]
|
|
47
|
-
|
|
48
|
-
return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if data_sets.empty?
|
|
49
|
-
|
|
50
|
-
wrap_and_render(binding, __FILE__)
|
|
38
|
+
def minimum_y_value
|
|
39
|
+
1 # Values under 1 day are data quality problems; they're flagged in the quality report instead
|
|
51
40
|
end
|
|
52
41
|
|
|
53
|
-
def
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
group_issues(completed_issues).each do |rules, completed_issues_by_type|
|
|
57
|
-
label = rules.label
|
|
58
|
-
color = rules.color
|
|
59
|
-
percent_line = calculate_percent_line completed_issues_by_type
|
|
60
|
-
data = completed_issues_by_type.filter_map { |issue| data_for_issue(issue) }
|
|
61
|
-
data_sets << {
|
|
62
|
-
label: "#{label} (85% at #{label_days(percent_line)})",
|
|
63
|
-
data: data,
|
|
64
|
-
fill: false,
|
|
65
|
-
showLine: false,
|
|
66
|
-
backgroundColor: color
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
data_sets << trend_line_data_set(label: label, data: data, color: color)
|
|
70
|
-
|
|
71
|
-
@percentage_lines << [percent_line, color]
|
|
72
|
-
end
|
|
73
|
-
data_sets
|
|
42
|
+
def all_items
|
|
43
|
+
completed_issues_in_range include_unstarted: false
|
|
74
44
|
end
|
|
75
45
|
|
|
76
|
-
def
|
|
77
|
-
|
|
46
|
+
def x_value item
|
|
47
|
+
item.started_stopped_times.last
|
|
78
48
|
end
|
|
79
49
|
|
|
80
|
-
def
|
|
81
|
-
|
|
82
|
-
[Time.parse(hash[:x]).to_i, hash[:y]]
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# The trend calculation works with numbers only so convert Time to an int and back
|
|
86
|
-
calculator = TrendLineCalculator.new(points)
|
|
87
|
-
data_points = calculator.chart_datapoints(
|
|
88
|
-
range: time_range.begin.to_i..time_range.end.to_i,
|
|
89
|
-
max_y: @highest_cycletime
|
|
90
|
-
)
|
|
91
|
-
data_points.each do |point_hash|
|
|
92
|
-
point_hash[:x] = chart_format Time.at(point_hash[:x])
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
{
|
|
96
|
-
type: 'line',
|
|
97
|
-
label: "#{label} Trendline",
|
|
98
|
-
data: data_points,
|
|
99
|
-
fill: false,
|
|
100
|
-
borderWidth: 1,
|
|
101
|
-
markerType: 'none',
|
|
102
|
-
borderColor: color,
|
|
103
|
-
borderDash: [6, 3],
|
|
104
|
-
pointStyle: 'dash',
|
|
105
|
-
hidden: !@show_trend_lines
|
|
106
|
-
}
|
|
50
|
+
def y_value item
|
|
51
|
+
item.board.cycletime.cycletime(item)
|
|
107
52
|
end
|
|
108
53
|
|
|
109
|
-
def
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
@highest_cycletime = cycle_time if @highest_cycletime < cycle_time
|
|
114
|
-
|
|
115
|
-
{
|
|
116
|
-
y: cycle_time,
|
|
117
|
-
x: chart_format(issue.board.cycletime.started_stopped_times(issue).last),
|
|
118
|
-
title: ["#{issue.key} : #{issue.summary} (#{label_days(cycle_time)})"]
|
|
119
|
-
}
|
|
54
|
+
def title_value item, rules: nil
|
|
55
|
+
hint = @issue_hints&.fetch(item, nil)
|
|
56
|
+
"#{item.key} : #{item.summary} (#{label_days(y_value(item))})#{" #{hint}" if hint}"
|
|
120
57
|
end
|
|
121
58
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
index = times.size * 85 / 100
|
|
125
|
-
times.sort[index]
|
|
126
|
-
end
|
|
59
|
+
# Kept for backwards compatibility with existing callers and specs
|
|
60
|
+
alias data_for_issue data_for_item
|
|
127
61
|
end
|
|
@@ -9,7 +9,8 @@ class DailyView < ChartBase
|
|
|
9
9
|
header_text 'Daily View'
|
|
10
10
|
description_text <<-HTML
|
|
11
11
|
<div class="p">
|
|
12
|
-
This view shows all the items you'll want to discuss during your daily
|
|
12
|
+
This view shows all the items (<%= aging_issues.count %>) you'll want to discuss during your daily
|
|
13
|
+
coordination meeting
|
|
13
14
|
(aka daily scrum, standup), in the order that you should be discussing them. The most important
|
|
14
15
|
items are at the top, and the least at the bottom.
|
|
15
16
|
</div>
|
|
@@ -23,7 +24,7 @@ class DailyView < ChartBase
|
|
|
23
24
|
def run
|
|
24
25
|
aging_issues = select_aging_issues
|
|
25
26
|
|
|
26
|
-
return "<h1 class='foldable'>#{@header_text}</h1>There are no items currently in progress" if aging_issues.empty?
|
|
27
|
+
return "<h1 class='foldable'>#{@header_text}</h1><div>There are no items currently in progress</div>" if aging_issues.empty?
|
|
27
28
|
|
|
28
29
|
result = +''
|
|
29
30
|
result << render_top_text(binding)
|
|
@@ -35,7 +36,7 @@ class DailyView < ChartBase
|
|
|
35
36
|
|
|
36
37
|
def select_aging_issues
|
|
37
38
|
aging_issues = issues.select do |issue|
|
|
38
|
-
started_at, stopped_at = issue.
|
|
39
|
+
started_at, stopped_at = issue.started_stopped_times
|
|
39
40
|
started_at && !stopped_at
|
|
40
41
|
end
|
|
41
42
|
|
|
@@ -72,7 +73,7 @@ class DailyView < ChartBase
|
|
|
72
73
|
|
|
73
74
|
def make_blocked_stalled_lines issue
|
|
74
75
|
today = date_range.end
|
|
75
|
-
started_date = issue.
|
|
76
|
+
started_date = issue.started_stopped_times.first&.to_date
|
|
76
77
|
return [] unless started_date
|
|
77
78
|
|
|
78
79
|
blocked_stalled = issue.blocked_stalled_by_date(
|
|
@@ -86,9 +87,15 @@ class DailyView < ChartBase
|
|
|
86
87
|
lines << ["#{marker} Blocked by flag"] if blocked_stalled.flag
|
|
87
88
|
lines << ["#{marker} Blocked by status: #{blocked_stalled.status}"] if blocked_stalled.blocked_by_status?
|
|
88
89
|
blocked_stalled.blocking_issue_keys&.each do |key|
|
|
89
|
-
|
|
90
|
-
blocking_issue
|
|
91
|
-
|
|
90
|
+
blocking_issue = issues.find_by_key key: key, include_hidden: true
|
|
91
|
+
if blocking_issue
|
|
92
|
+
lines << "<section><div class=\"foldable startFolded\">#{marker} Blocked by issue: " \
|
|
93
|
+
"#{make_issue_label issue: blocking_issue, done: blocking_issue.done?}</div>"
|
|
94
|
+
lines << blocking_issue
|
|
95
|
+
lines << '</section>'
|
|
96
|
+
else
|
|
97
|
+
lines << ["#{marker} Blocked by issue: #{key} (no description found)"]
|
|
98
|
+
end
|
|
92
99
|
end
|
|
93
100
|
elsif blocked_stalled.stalled_by_status?
|
|
94
101
|
lines << ["#{color_block '--stalled-color'} Stalled by status: #{blocked_stalled.status}"]
|
|
@@ -146,7 +153,18 @@ class DailyView < ChartBase
|
|
|
146
153
|
line << "Assignee: <img src='#{issue.assigned_to_icon_url}' class='icon' /> <b>#{issue.assigned_to}</b>"
|
|
147
154
|
end
|
|
148
155
|
|
|
149
|
-
|
|
156
|
+
if issue.due_date
|
|
157
|
+
today = date_range.end
|
|
158
|
+
days = (issue.due_date - today).to_i
|
|
159
|
+
relative =
|
|
160
|
+
if days.zero? then 'today'
|
|
161
|
+
elsif days.positive? then "in #{label_days days}"
|
|
162
|
+
else "#{label_days(-days)} ago"
|
|
163
|
+
end
|
|
164
|
+
content = "#{issue.due_date} (#{relative})"
|
|
165
|
+
content = "<span style='background: var(--warning-banner)'>#{content}</span>" if days.negative?
|
|
166
|
+
line << "Due: <b>#{content}</b>"
|
|
167
|
+
end
|
|
150
168
|
|
|
151
169
|
block = lambda do |collection, label|
|
|
152
170
|
unless collection.empty?
|
|
@@ -166,7 +184,7 @@ class DailyView < ChartBase
|
|
|
166
184
|
|
|
167
185
|
return lines if subtasks.empty?
|
|
168
186
|
|
|
169
|
-
lines <<
|
|
187
|
+
lines << "<section><div class=\"foldable startFolded\">Child issues (#{subtasks.count})</div>"
|
|
170
188
|
lines += subtasks
|
|
171
189
|
lines << '</section>'
|
|
172
190
|
|
|
@@ -237,9 +255,11 @@ class DailyView < ChartBase
|
|
|
237
255
|
|
|
238
256
|
def make_description_lines issue
|
|
239
257
|
description = issue.raw['fields']['description']
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
258
|
+
return [] unless description
|
|
259
|
+
|
|
260
|
+
text = "<div class='foldable startFolded'>Description</div>" \
|
|
261
|
+
"<div>#{atlassian_document_format.to_html(description)}</div>"
|
|
262
|
+
[[text]]
|
|
243
263
|
end
|
|
244
264
|
|
|
245
265
|
def assemble_issue_lines issue, child:
|
|
@@ -247,6 +267,7 @@ class DailyView < ChartBase
|
|
|
247
267
|
|
|
248
268
|
lines = []
|
|
249
269
|
lines << [make_title_line(issue: issue, done: done)]
|
|
270
|
+
lines << make_not_visible_line(issue)
|
|
250
271
|
lines += make_parent_lines(issue) unless child
|
|
251
272
|
lines += make_stats_lines(issue: issue, done: done)
|
|
252
273
|
unless done
|
|
@@ -256,7 +277,7 @@ class DailyView < ChartBase
|
|
|
256
277
|
lines += make_child_lines(issue)
|
|
257
278
|
lines += make_history_lines(issue)
|
|
258
279
|
end
|
|
259
|
-
lines
|
|
280
|
+
lines.compact
|
|
260
281
|
end
|
|
261
282
|
|
|
262
283
|
def render_issue issue, child:
|
|
@@ -278,4 +299,8 @@ class DailyView < ChartBase
|
|
|
278
299
|
end
|
|
279
300
|
result << '</div>'
|
|
280
301
|
end
|
|
302
|
+
|
|
303
|
+
def make_not_visible_line issue
|
|
304
|
+
not_visible_text issue
|
|
305
|
+
end
|
|
281
306
|
end
|
|
@@ -49,9 +49,7 @@ class DailyWipByAgeChart < DailyWipChart
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def default_grouping_rules issue:, rules:
|
|
52
|
-
started, stopped = issue.
|
|
53
|
-
|
|
54
|
-
rules.issue_hint = "(age: #{label_days (rules.current_date - started + 1).to_i})" if started
|
|
52
|
+
started, stopped = issue.started_stopped_dates
|
|
55
53
|
|
|
56
54
|
if stopped && started.nil? # We can't tell when it started
|
|
57
55
|
@has_completed_but_not_started = true
|
|
@@ -72,7 +70,7 @@ class DailyWipByAgeChart < DailyWipChart
|
|
|
72
70
|
rules.label = 'Start date unknown'
|
|
73
71
|
rules.color = '--body-background'
|
|
74
72
|
rules.group_priority = 11
|
|
75
|
-
created_days = rules.current_date - created
|
|
73
|
+
created_days = rules.current_date - created
|
|
76
74
|
rules.issue_hint = "(created: #{label_days created_days.to_i} earlier, stopped on #{stopped})"
|
|
77
75
|
end
|
|
78
76
|
end
|
|
@@ -84,7 +82,8 @@ class DailyWipByAgeChart < DailyWipChart
|
|
|
84
82
|
end
|
|
85
83
|
|
|
86
84
|
def group_by_age started:, rules:
|
|
87
|
-
age = rules.current_date - started + 1
|
|
85
|
+
age = (rules.current_date - started).to_i + 1
|
|
86
|
+
rules.issue_hint = "(age: #{label_days age})"
|
|
88
87
|
|
|
89
88
|
case age
|
|
90
89
|
when 1
|
|
@@ -39,23 +39,32 @@ class DailyWipByBlockedStalledChart < DailyWipChart
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def default_grouping_rules issue:, rules:
|
|
42
|
-
started, stopped = issue.
|
|
42
|
+
started, stopped = issue.started_stopped_times
|
|
43
43
|
stopped_date = stopped&.to_date
|
|
44
|
+
started_date = started&.to_date
|
|
44
45
|
|
|
45
46
|
date = rules.current_date
|
|
46
47
|
change = issue.blocked_stalled_by_date(date_range: date..date, chart_end_time: time_range.end)[date]
|
|
47
|
-
|
|
48
48
|
stopped_today = stopped_date == rules.current_date
|
|
49
49
|
|
|
50
|
+
days = nil
|
|
51
|
+
if started_date && stopped_date
|
|
52
|
+
days = (stopped_date - started_date).to_i + 1 # cycletime
|
|
53
|
+
elsif started_date
|
|
54
|
+
days = (time_range.end.to_date - started_date).to_i + 1 # age
|
|
55
|
+
end
|
|
56
|
+
|
|
50
57
|
if stopped_today && started.nil?
|
|
51
58
|
@has_completed_but_not_started = true
|
|
52
59
|
rules.label = 'Completed but not started'
|
|
53
60
|
rules.color = '--wip-chart-completed-but-not-started-color'
|
|
54
61
|
rules.group_priority = -1
|
|
62
|
+
rules.issue_hint = '(Cycle time: Unknown)'
|
|
55
63
|
elsif stopped_today
|
|
56
64
|
rules.label = 'Completed'
|
|
57
65
|
rules.color = '--wip-chart-completed-color'
|
|
58
66
|
rules.group_priority = -2
|
|
67
|
+
rules.issue_hint = "(Cycle time: #{label_days days})"
|
|
59
68
|
elsif started.nil?
|
|
60
69
|
rules.label = 'Start date unknown'
|
|
61
70
|
rules.color = '--body-background'
|
|
@@ -64,16 +73,17 @@ class DailyWipByBlockedStalledChart < DailyWipChart
|
|
|
64
73
|
rules.label = 'Blocked'
|
|
65
74
|
rules.color = '--blocked-color'
|
|
66
75
|
rules.group_priority = 1
|
|
67
|
-
rules.issue_hint = "(#{change.reasons})"
|
|
76
|
+
rules.issue_hint = "(Age: #{label_days days}, #{change.reasons})"
|
|
68
77
|
elsif change&.stalled?
|
|
69
78
|
rules.label = 'Stalled'
|
|
70
79
|
rules.color = '--stalled-color'
|
|
71
80
|
rules.group_priority = 2
|
|
72
|
-
rules.issue_hint = "(#{change.reasons})"
|
|
81
|
+
rules.issue_hint = "(Age: #{label_days days}, #{change.reasons})"
|
|
73
82
|
else
|
|
74
83
|
rules.label = 'Active'
|
|
75
84
|
rules.color = '--wip-chart-active-color'
|
|
76
85
|
rules.group_priority = 3
|
|
86
|
+
rules.issue_hint = "(Age: #{label_days days})"
|
|
77
87
|
end
|
|
78
88
|
end
|
|
79
89
|
end
|
|
@@ -26,11 +26,13 @@ class DailyWipByParentChart < DailyWipChart
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def default_grouping_rules issue:, rules:
|
|
29
|
-
parent = issue.parent
|
|
29
|
+
parent = issue.parent
|
|
30
30
|
if parent
|
|
31
|
-
rules.label = parent
|
|
31
|
+
rules.label = parent.key
|
|
32
|
+
rules.label_hint = "#{parent.key} : #{parent.summary}"
|
|
32
33
|
else
|
|
33
34
|
rules.label = 'No parent'
|
|
35
|
+
rules.label_hint = 'No parent'
|
|
34
36
|
rules.group_priority = 1000
|
|
35
37
|
rules.color = '--body-background'
|
|
36
38
|
end
|
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
require 'jirametrics/chart_base'
|
|
4
4
|
|
|
5
5
|
class DailyGroupingRules < GroupingRules
|
|
6
|
-
attr_accessor :current_date, :group_priority, :issue_hint
|
|
6
|
+
attr_accessor :current_date, :group_priority, :issue_hint, :highlight
|
|
7
7
|
|
|
8
8
|
def initialize
|
|
9
9
|
super
|
|
10
10
|
@group_priority = 0
|
|
11
11
|
end
|
|
12
|
+
|
|
13
|
+
def group
|
|
14
|
+
[@label, @color, @highlight ? true : false]
|
|
15
|
+
end
|
|
12
16
|
end
|
|
13
17
|
|
|
14
18
|
class DailyWipChart < ChartBase
|
|
@@ -19,6 +23,8 @@ class DailyWipChart < ChartBase
|
|
|
19
23
|
|
|
20
24
|
header_text default_header_text
|
|
21
25
|
description_text default_description_text
|
|
26
|
+
@x_axis_title = nil
|
|
27
|
+
@y_axis_title = 'Count of items'
|
|
22
28
|
|
|
23
29
|
instance_eval(&block) if block
|
|
24
30
|
|
|
@@ -33,8 +39,15 @@ class DailyWipChart < ChartBase
|
|
|
33
39
|
issue_rules_by_active_date = group_issues_by_active_dates
|
|
34
40
|
possible_rules = select_possible_rules issue_rules_by_active_date
|
|
35
41
|
|
|
42
|
+
conflicting_labels = possible_rules
|
|
43
|
+
.group_by(&:label)
|
|
44
|
+
.select { |_label, rules| rules.any?(&:highlight) && rules.any? { |r| !r.highlight } }
|
|
45
|
+
.keys
|
|
46
|
+
|
|
36
47
|
data_sets = possible_rules.collect do |grouping_rule|
|
|
37
|
-
|
|
48
|
+
suffix = conflicting_labels.include?(grouping_rule.label) && grouping_rule.highlight ? '*' : ''
|
|
49
|
+
make_data_set grouping_rule: grouping_rule, issue_rules_by_active_date: issue_rules_by_active_date,
|
|
50
|
+
label_suffix: suffix
|
|
38
51
|
end
|
|
39
52
|
if @trend_lines
|
|
40
53
|
data_sets = @trend_lines.filter_map do |group_labels, line_color|
|
|
@@ -66,7 +79,7 @@ class DailyWipChart < ChartBase
|
|
|
66
79
|
hash = {}
|
|
67
80
|
|
|
68
81
|
@issues.each do |issue|
|
|
69
|
-
start, stop = issue.
|
|
82
|
+
start, stop = cycletime_for_issue(issue).started_stopped_dates(issue)
|
|
70
83
|
next if start.nil? && stop.nil?
|
|
71
84
|
|
|
72
85
|
# If it stopped but never started then assume it started at creation so the data points
|
|
@@ -82,16 +95,17 @@ class DailyWipChart < ChartBase
|
|
|
82
95
|
hash
|
|
83
96
|
end
|
|
84
97
|
|
|
85
|
-
def make_data_set grouping_rule:, issue_rules_by_active_date:
|
|
98
|
+
def make_data_set grouping_rule:, issue_rules_by_active_date:, label_suffix: ''
|
|
86
99
|
positive = grouping_rule.group_priority >= 0
|
|
100
|
+
display_label = "#{grouping_rule.label}#{label_suffix}"
|
|
87
101
|
|
|
88
102
|
data = issue_rules_by_active_date.collect do |date, issue_rules|
|
|
89
|
-
# issues = []
|
|
90
103
|
issue_strings = issue_rules
|
|
91
104
|
.select { |_issue, rules| rules.group == grouping_rule.group }
|
|
92
105
|
.sort_by { |issue, _rules| issue.key_as_i }
|
|
93
106
|
.collect { |issue, rules| "#{issue.key} : #{issue.summary.strip} #{rules.issue_hint}" }
|
|
94
|
-
|
|
107
|
+
title_label = grouping_rule.label_hint || display_label
|
|
108
|
+
title = ["#{title_label} (#{label_issues issue_strings.size})"] + issue_strings
|
|
95
109
|
|
|
96
110
|
{
|
|
97
111
|
x: date,
|
|
@@ -100,11 +114,19 @@ class DailyWipChart < ChartBase
|
|
|
100
114
|
}
|
|
101
115
|
end
|
|
102
116
|
|
|
117
|
+
color = grouping_rule.color || random_color
|
|
118
|
+
background_color = if grouping_rule.highlight
|
|
119
|
+
RawJavascript.new("createDiagonalPattern(#{color.to_json})")
|
|
120
|
+
else
|
|
121
|
+
color
|
|
122
|
+
end
|
|
123
|
+
|
|
103
124
|
{
|
|
104
125
|
type: 'bar',
|
|
105
|
-
label:
|
|
126
|
+
label: display_label,
|
|
127
|
+
label_hint: grouping_rule.label_hint,
|
|
106
128
|
data: data,
|
|
107
|
-
backgroundColor:
|
|
129
|
+
backgroundColor: background_color,
|
|
108
130
|
borderColor: CssVariable['--wip-chart-border-color'],
|
|
109
131
|
borderWidth: grouping_rule.color.to_s == 'var(--body-background)' ? 1 : 0,
|
|
110
132
|
borderRadius: positive ? 0 : 5
|
|
@@ -45,6 +45,7 @@ class DataQualityReport < ChartBase
|
|
|
45
45
|
scan_for_completed_issues_without_a_start_time entry: entry
|
|
46
46
|
scan_for_status_change_after_done entry: entry
|
|
47
47
|
scan_for_backwards_movement entry: entry, backlog_statuses: backlog_statuses
|
|
48
|
+
scan_for_issue_not_in_active_sprint entry: entry
|
|
48
49
|
scan_for_issues_not_created_in_a_backlog_status entry: entry, backlog_statuses: backlog_statuses
|
|
49
50
|
scan_for_stopped_before_started entry: entry
|
|
50
51
|
scan_for_issues_not_started_with_subtasks_that_have entry: entry
|
|
@@ -68,7 +69,7 @@ class DataQualityReport < ChartBase
|
|
|
68
69
|
result << render_problem_type(:status_changes_after_done)
|
|
69
70
|
result << render_problem_type(:backwards_through_status_categories)
|
|
70
71
|
result << render_problem_type(:backwords_through_statuses)
|
|
71
|
-
result << render_problem_type(:
|
|
72
|
+
result << render_problem_type(:issue_not_visible_on_board)
|
|
72
73
|
result << render_problem_type(:created_in_wrong_status)
|
|
73
74
|
result << render_problem_type(:stopped_before_started)
|
|
74
75
|
result << render_problem_type(:issue_not_started_but_subtasks_have)
|
|
@@ -120,7 +121,7 @@ class DataQualityReport < ChartBase
|
|
|
120
121
|
|
|
121
122
|
def initialize_entries
|
|
122
123
|
@entries = @issues.filter_map do |issue|
|
|
123
|
-
started, stopped = issue.
|
|
124
|
+
started, stopped = issue.started_stopped_times
|
|
124
125
|
next if stopped && stopped < time_range.begin
|
|
125
126
|
next if started && started > time_range.end
|
|
126
127
|
|
|
@@ -194,7 +195,7 @@ class DataQualityReport < ChartBase
|
|
|
194
195
|
# If it's been moved back to backlog then it's on a different report. Ignore it here.
|
|
195
196
|
detail = nil if backlog_statuses.any? { |s| s.name == change.value }
|
|
196
197
|
|
|
197
|
-
entry.report(problem_key: :
|
|
198
|
+
entry.report(problem_key: :issue_not_visible_on_board, detail: detail) unless detail.nil?
|
|
198
199
|
elsif change.old_value.nil?
|
|
199
200
|
# Do nothing
|
|
200
201
|
elsif index < last_index
|
|
@@ -223,6 +224,29 @@ class DataQualityReport < ChartBase
|
|
|
223
224
|
end
|
|
224
225
|
end
|
|
225
226
|
|
|
227
|
+
def scan_for_issue_not_in_active_sprint entry:
|
|
228
|
+
issue = entry.issue
|
|
229
|
+
return unless issue.board.scrum?
|
|
230
|
+
return if issue.sprints.any?(&:active?)
|
|
231
|
+
|
|
232
|
+
entry.report(problem_key: :issue_not_visible_on_board, detail: 'Issue is not in an active sprint')
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def scan_for_issue_never_visible_on_board entry:
|
|
236
|
+
issue = entry.issue
|
|
237
|
+
ever_visible = issue.changes.any? do |change|
|
|
238
|
+
next unless change.status?
|
|
239
|
+
|
|
240
|
+
issue.board.visible_columns.any? { |col| col.status_ids.include?(change.value_id) }
|
|
241
|
+
end
|
|
242
|
+
return if ever_visible
|
|
243
|
+
|
|
244
|
+
entry.report(
|
|
245
|
+
problem_key: :issue_not_visible_on_board,
|
|
246
|
+
detail: 'Issue has never been in a status mapped to a visible column on the board'
|
|
247
|
+
)
|
|
248
|
+
end
|
|
249
|
+
|
|
226
250
|
def scan_for_issues_not_created_in_a_backlog_status entry:, backlog_statuses:
|
|
227
251
|
creation_change = entry.issue.changes.find { |issue| issue.status? }
|
|
228
252
|
|
|
@@ -250,7 +274,7 @@ class DataQualityReport < ChartBase
|
|
|
250
274
|
|
|
251
275
|
started_subtasks = []
|
|
252
276
|
entry.issue.subtasks.each do |subtask|
|
|
253
|
-
started_subtasks << subtask if subtask.
|
|
277
|
+
started_subtasks << subtask if subtask.started_stopped_times.first
|
|
254
278
|
end
|
|
255
279
|
|
|
256
280
|
return if started_subtasks.empty?
|
|
@@ -266,8 +290,10 @@ class DataQualityReport < ChartBase
|
|
|
266
290
|
|
|
267
291
|
def scan_for_items_blocked_on_closed_tickets entry:
|
|
268
292
|
entry.issue.issue_links.each do |link|
|
|
293
|
+
next unless settings['blocked_link_text'].include?(link.label)
|
|
294
|
+
|
|
269
295
|
this_active = !entry.stopped
|
|
270
|
-
other_active = !link.other_issue.
|
|
296
|
+
other_active = !link.other_issue.started_stopped_times.last
|
|
271
297
|
next unless this_active && !other_active
|
|
272
298
|
|
|
273
299
|
entry.report(
|
|
@@ -293,14 +319,14 @@ class DataQualityReport < ChartBase
|
|
|
293
319
|
return "#{delta} hours" if delta < 24
|
|
294
320
|
|
|
295
321
|
delta /= 24
|
|
296
|
-
|
|
322
|
+
label_days delta
|
|
297
323
|
end
|
|
298
324
|
|
|
299
325
|
def scan_for_incomplete_subtasks_when_issue_done entry:
|
|
300
326
|
return unless entry.stopped
|
|
301
327
|
|
|
302
328
|
subtask_labels = entry.issue.subtasks.filter_map do |subtask|
|
|
303
|
-
subtask_started, subtask_stopped = subtask.
|
|
329
|
+
subtask_started, subtask_stopped = subtask.started_stopped_times
|
|
304
330
|
|
|
305
331
|
if !subtask_started && !subtask_stopped
|
|
306
332
|
"#{subtask_label subtask} (Not even started)"
|
|
@@ -407,12 +433,14 @@ class DataQualityReport < ChartBase
|
|
|
407
433
|
HTML
|
|
408
434
|
end
|
|
409
435
|
|
|
410
|
-
def
|
|
436
|
+
def render_issue_not_visible_on_board problems
|
|
437
|
+
unique_issue_count = problems.map(&:first).uniq.size
|
|
411
438
|
<<-HTML
|
|
412
|
-
#{
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
439
|
+
#{problems.size} #{'time'.then { |w| problems.size == 1 ? w : "#{w}s" }} across #{label_issues unique_issue_count},
|
|
440
|
+
an item was not visible on the board. This may impact
|
|
441
|
+
timings as the work was likely to have been forgotten if it wasn't visible. An issue can be not visible
|
|
442
|
+
for two reasons: the issue was in a status that is not mapped to any visible column on the board
|
|
443
|
+
(look in "unmapped statuses" on your board), or for scrum boards, the issue was not in an active sprint.
|
|
416
444
|
HTML
|
|
417
445
|
end
|
|
418
446
|
|
|
@@ -57,7 +57,7 @@ class DependencyChart < ChartBase
|
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
svg = execute_graphviz(dot_graph.join("\n"))
|
|
60
|
-
"<h1>#{@header_text}</h1><div>#{@description_text}
|
|
60
|
+
"<h1 class='foldable'>#{@header_text}</h1><div>#{@description_text}#{shrink_svg svg}</div>"
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def link_rules &block
|
|
@@ -231,7 +231,7 @@ class DependencyChart < ChartBase
|
|
|
231
231
|
elsif is_done
|
|
232
232
|
line2 << 'Done'
|
|
233
233
|
else
|
|
234
|
-
started_at = issue.
|
|
234
|
+
started_at = issue.started_stopped_times.first
|
|
235
235
|
if started_at.nil?
|
|
236
236
|
line2 << 'Not started'
|
|
237
237
|
else
|
|
@@ -25,10 +25,25 @@ class DownloadConfig
|
|
|
25
25
|
@no_earlier_than
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
def github_repos
|
|
29
|
+
@github_repos ||= []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def github_repo *repos
|
|
33
|
+
github_repos.concat(repos.map { |r| normalize_github_repo(r) })
|
|
34
|
+
end
|
|
35
|
+
|
|
28
36
|
def start_date today:
|
|
29
37
|
date = today.to_date - @rolling_date_count if @rolling_date_count
|
|
30
38
|
date = [date, @no_earlier_than].max if date && @no_earlier_than
|
|
31
39
|
date = @no_earlier_than if date.nil? && @no_earlier_than
|
|
32
40
|
date
|
|
33
41
|
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def normalize_github_repo repo
|
|
46
|
+
match = repo.match(%r{github\.com/([^/]+/[^/]+?)/?$})
|
|
47
|
+
match ? match[1] : repo
|
|
48
|
+
end
|
|
34
49
|
end
|