jirametrics 2.12.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 +81 -6
- data/lib/jirametrics/atlassian_document_format.rb +160 -0
- 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 +4 -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 +15 -6
- data/lib/jirametrics/chart_base.rb +141 -3
- 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} +21 -4
- data/lib/jirametrics/cycletime_histogram.rb +15 -101
- data/lib/jirametrics/cycletime_scatterplot.rb +17 -83
- data/lib/jirametrics/daily_view.rb +90 -61
- 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 +43 -12
- data/lib/jirametrics/dependency_chart.rb +6 -3
- data/lib/jirametrics/download_config.rb +15 -0
- data/lib/jirametrics/downloader.rb +117 -71
- data/lib/jirametrics/downloader_for_cloud.rb +287 -0
- data/lib/jirametrics/downloader_for_data_center.rb +95 -0
- 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 +41 -28
- data/lib/jirametrics/expedited_chart.rb +3 -1
- data/lib/jirametrics/exporter.rb +26 -6
- data/lib/jirametrics/file_config.rb +9 -11
- data/lib/jirametrics/file_system.rb +59 -3
- 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 +3 -1
- data/lib/jirametrics/html/aging_work_table.erb +5 -0
- data/lib/jirametrics/html/collapsible_issues_panel.erb +2 -2
- 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 +249 -69
- data/lib/jirametrics/html/index.erb +11 -37
- data/lib/jirametrics/html/index.js +164 -0
- data/lib/jirametrics/html/legacy_colors.css +174 -0
- data/lib/jirametrics/html/sprint_burndown.erb +17 -15
- 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 +52 -57
- data/lib/jirametrics/issue.rb +305 -102
- data/lib/jirametrics/issue_printer.rb +97 -0
- data/lib/jirametrics/jira_gateway.rb +81 -17
- data/lib/jirametrics/mcp_server.rb +531 -0
- data/lib/jirametrics/project_config.rb +128 -12
- 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 +5 -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/status_collection.rb +1 -0
- 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 +83 -68
- metadata +61 -6
|
@@ -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>#{@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,13 +73,13 @@ 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(
|
|
79
80
|
date_range: today..today, chart_end_time: time_range.end, settings: settings
|
|
80
81
|
)[today]
|
|
81
|
-
return []
|
|
82
|
+
return [] if blocked_stalled.active?
|
|
82
83
|
|
|
83
84
|
lines = []
|
|
84
85
|
if blocked_stalled.blocked?
|
|
@@ -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}"]
|
|
@@ -98,15 +105,18 @@ class DailyView < ChartBase
|
|
|
98
105
|
lines
|
|
99
106
|
end
|
|
100
107
|
|
|
101
|
-
def make_issue_label issue
|
|
102
|
-
"<img src='#{issue.type_icon_url}' title='#{issue.type}' class='icon' /> "
|
|
103
|
-
|
|
108
|
+
def make_issue_label issue:, done:
|
|
109
|
+
label = "<img src='#{issue.type_icon_url}' title='#{issue.type}' class='icon' /> "
|
|
110
|
+
label << '<s>' if done
|
|
111
|
+
label << "<b><a href='#{issue.url}'>#{issue.key}</a></b> <i>#{issue.summary}</i>"
|
|
112
|
+
label << '</s>' if done
|
|
113
|
+
label
|
|
104
114
|
end
|
|
105
115
|
|
|
106
|
-
def make_title_line issue
|
|
116
|
+
def make_title_line issue:, done:
|
|
107
117
|
title_line = +''
|
|
108
118
|
title_line << color_block('--expedited-color', title: 'Expedited') if issue.expedited?
|
|
109
|
-
title_line << make_issue_label(issue)
|
|
119
|
+
title_line << make_issue_label(issue: issue, done: done)
|
|
110
120
|
title_line
|
|
111
121
|
end
|
|
112
122
|
|
|
@@ -115,20 +125,25 @@ class DailyView < ChartBase
|
|
|
115
125
|
parent_key = issue.parent_key
|
|
116
126
|
if parent_key
|
|
117
127
|
parent = issues.find_by_key key: parent_key, include_hidden: true
|
|
118
|
-
text = parent ? make_issue_label(parent) : parent_key
|
|
128
|
+
text = parent ? make_issue_label(issue: parent, done: parent.done?) : parent_key
|
|
119
129
|
lines << ["Parent: #{text}"]
|
|
120
130
|
end
|
|
121
131
|
lines
|
|
122
132
|
end
|
|
123
133
|
|
|
124
|
-
def make_stats_lines issue
|
|
134
|
+
def make_stats_lines issue:, done:
|
|
125
135
|
line = []
|
|
126
136
|
|
|
127
137
|
line << "<img src='#{issue.priority_url}' class='icon' /> <b>#{issue.priority_name}</b>"
|
|
128
138
|
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
if done
|
|
140
|
+
cycletime = issue.board.cycletime.cycletime(issue)
|
|
131
141
|
|
|
142
|
+
line << "Cycletime: <b>#{label_days cycletime}</b>"
|
|
143
|
+
else
|
|
144
|
+
age = issue.board.cycletime.age(issue, today: date_range.end)
|
|
145
|
+
line << "Age: <b>#{age ? label_days(age) : '(Not Started)'}</b>"
|
|
146
|
+
end
|
|
132
147
|
line << "Status: <b>#{format_status issue.status, board: issue.board}</b>"
|
|
133
148
|
|
|
134
149
|
column = issue.board.visible_columns.find { |c| c.status_ids.include?(issue.status.id) }
|
|
@@ -138,7 +153,18 @@ class DailyView < ChartBase
|
|
|
138
153
|
line << "Assignee: <img src='#{issue.assigned_to_icon_url}' class='icon' /> <b>#{issue.assigned_to}</b>"
|
|
139
154
|
end
|
|
140
155
|
|
|
141
|
-
|
|
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
|
|
142
168
|
|
|
143
169
|
block = lambda do |collection, label|
|
|
144
170
|
unless collection.empty?
|
|
@@ -154,45 +180,26 @@ class DailyView < ChartBase
|
|
|
154
180
|
|
|
155
181
|
def make_child_lines issue
|
|
156
182
|
lines = []
|
|
157
|
-
subtasks = issue.subtasks
|
|
183
|
+
subtasks = issue.subtasks
|
|
158
184
|
|
|
159
|
-
|
|
160
|
-
icon_urls = subtasks.collect(&:type_icon_url).uniq.collect { |url| "<img src='#{url}' class='icon' />" }
|
|
161
|
-
lines << (icon_urls << 'Incomplete child issues')
|
|
162
|
-
lines += subtasks
|
|
163
|
-
end
|
|
164
|
-
lines
|
|
165
|
-
end
|
|
185
|
+
return lines if subtasks.empty?
|
|
166
186
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
.gsub(/\[~accountid:([^\]]+)\]/) { expand_account_id $1 } # Tagged people
|
|
171
|
-
.gsub(/\[([^\|]+)\|(https?[^\]]+)\]/, '<a href="\2">\1</a>') # URLs
|
|
172
|
-
.gsub("\n", '<br />')
|
|
173
|
-
end
|
|
187
|
+
lines << "<section><div class=\"foldable startFolded\">Child issues (#{subtasks.count})</div>"
|
|
188
|
+
lines += subtasks
|
|
189
|
+
lines << '</section>'
|
|
174
190
|
|
|
175
|
-
|
|
176
|
-
user = @users.find { |u| u.account_id == account_id }
|
|
177
|
-
text = account_id
|
|
178
|
-
text = "@#{user.display_name}" if user
|
|
179
|
-
"<span class='account_id'>#{text}</span>"
|
|
191
|
+
lines
|
|
180
192
|
end
|
|
181
193
|
|
|
182
194
|
def make_history_lines issue
|
|
183
195
|
history = issue.changes.reverse
|
|
184
196
|
lines = []
|
|
185
197
|
|
|
186
|
-
|
|
187
|
-
lines << [
|
|
188
|
-
"<a href=\"javascript:toggle_visibility('open#{id}', 'close#{id}', 'table#{id}');\">" \
|
|
189
|
-
"<span id='open#{id}'>▶ Issue History</span>" \
|
|
190
|
-
"<span id='close#{id}' style='display: none'>▼ Issue History</span></a>"
|
|
191
|
-
]
|
|
198
|
+
lines << '<section><div class="foldable startFolded">Issue history</div>'
|
|
192
199
|
table = +''
|
|
193
|
-
table <<
|
|
200
|
+
table << '<table>'
|
|
194
201
|
history.each do |c|
|
|
195
|
-
time = c.time.strftime '%b %d, %I:%M%P'
|
|
202
|
+
time = c.time.strftime '%b %d, %Y @ %I:%M%P'
|
|
196
203
|
|
|
197
204
|
table << '<tr>'
|
|
198
205
|
table << "<td><span class='time' title='Timestamp: #{c.time}'>#{time}</span></td>"
|
|
@@ -203,23 +210,24 @@ class DailyView < ChartBase
|
|
|
203
210
|
end
|
|
204
211
|
table << '</table>'
|
|
205
212
|
lines << [table]
|
|
213
|
+
lines << '</section>'
|
|
206
214
|
lines
|
|
207
215
|
end
|
|
208
216
|
|
|
209
217
|
def history_text change:, board:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
218
|
+
convertor = ->(value, _id) { value.inspect }
|
|
219
|
+
convertor = ->(_value, id) { format_status(board.possible_statuses.find_by_id(id), board: board) } if change.status?
|
|
220
|
+
|
|
221
|
+
if change.comment? || change.description?
|
|
222
|
+
atlassian_document_format.to_html(change.value)
|
|
223
|
+
elsif %w[status priority assignee duedate issuetype].include?(change.field)
|
|
224
|
+
to = convertor.call(change.value, change.value_id)
|
|
215
225
|
if change.old_value
|
|
216
|
-
from = convertor.call(change.old_value_id)
|
|
226
|
+
from = convertor.call(change.old_value, change.old_value_id)
|
|
217
227
|
"Changed from #{from} to #{to}"
|
|
218
228
|
else
|
|
219
229
|
"Set to #{to}"
|
|
220
230
|
end
|
|
221
|
-
elsif %w[priority assignee duedate issuetype].include?(change.field)
|
|
222
|
-
"Changed from \"#{change.old_value}\" to \"#{change.value}\""
|
|
223
231
|
elsif change.flagged?
|
|
224
232
|
change.value == '' ? 'Off' : 'On'
|
|
225
233
|
else
|
|
@@ -245,16 +253,31 @@ class DailyView < ChartBase
|
|
|
245
253
|
.join(' ')]]
|
|
246
254
|
end
|
|
247
255
|
|
|
256
|
+
def make_description_lines issue
|
|
257
|
+
description = issue.raw['fields']['description']
|
|
258
|
+
return [] unless description
|
|
259
|
+
|
|
260
|
+
text = "<div class='foldable startFolded'>Description</div>" \
|
|
261
|
+
"<div>#{atlassian_document_format.to_html(description)}</div>"
|
|
262
|
+
[[text]]
|
|
263
|
+
end
|
|
264
|
+
|
|
248
265
|
def assemble_issue_lines issue, child:
|
|
266
|
+
done = issue.done?
|
|
267
|
+
|
|
249
268
|
lines = []
|
|
250
|
-
lines << [make_title_line(issue)]
|
|
269
|
+
lines << [make_title_line(issue: issue, done: done)]
|
|
270
|
+
lines << make_not_visible_line(issue)
|
|
251
271
|
lines += make_parent_lines(issue) unless child
|
|
252
|
-
lines += make_stats_lines(issue)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
272
|
+
lines += make_stats_lines(issue: issue, done: done)
|
|
273
|
+
unless done
|
|
274
|
+
lines += make_description_lines(issue)
|
|
275
|
+
lines += make_sprints_lines(issue)
|
|
276
|
+
lines += make_blocked_stalled_lines(issue)
|
|
277
|
+
lines += make_child_lines(issue)
|
|
278
|
+
lines += make_history_lines(issue)
|
|
279
|
+
end
|
|
280
|
+
lines.compact
|
|
258
281
|
end
|
|
259
282
|
|
|
260
283
|
def render_issue issue, child:
|
|
@@ -264,6 +287,8 @@ class DailyView < ChartBase
|
|
|
264
287
|
assemble_issue_lines(issue, child: child).each do |row|
|
|
265
288
|
if row.is_a? Issue
|
|
266
289
|
result << render_issue(row, child: true)
|
|
290
|
+
elsif row.is_a?(String)
|
|
291
|
+
result << row
|
|
267
292
|
else
|
|
268
293
|
result << '<div class="heading">'
|
|
269
294
|
row.each do |chunk|
|
|
@@ -274,4 +299,8 @@ class DailyView < ChartBase
|
|
|
274
299
|
end
|
|
275
300
|
result << '</div>'
|
|
276
301
|
end
|
|
302
|
+
|
|
303
|
+
def make_not_visible_line issue
|
|
304
|
+
not_visible_text issue
|
|
305
|
+
end
|
|
277
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
|