jirametrics 2.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/jirametrics/aggregate_config.rb +1 -1
- data/lib/jirametrics/aging_work_bar_chart.rb +18 -13
- data/lib/jirametrics/aging_work_in_progress_chart.rb +8 -6
- data/lib/jirametrics/aging_work_table.rb +21 -16
- data/lib/jirametrics/anonymizer.rb +6 -5
- data/lib/jirametrics/chart_base.rb +35 -19
- data/lib/jirametrics/css_variable.rb +33 -0
- data/lib/jirametrics/cycletime_scatterplot.rb +8 -9
- data/lib/jirametrics/daily_wip_by_age_chart.rb +43 -17
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +32 -17
- data/lib/jirametrics/daily_wip_by_parent_chart.rb +42 -0
- data/lib/jirametrics/daily_wip_chart.rb +67 -6
- data/lib/jirametrics/data_quality_report.rb +1 -1
- data/lib/jirametrics/dependency_chart.rb +18 -14
- data/lib/jirametrics/downloader.rb +13 -11
- data/lib/jirametrics/examples/aggregated_project.rb +17 -20
- data/lib/jirametrics/examples/standard_project.rb +10 -18
- data/lib/jirametrics/expedited_chart.rb +17 -15
- data/lib/jirametrics/exporter.rb +26 -20
- data/lib/jirametrics/grouping_rules.rb +7 -1
- data/lib/jirametrics/html/aging_work_bar_chart.erb +12 -6
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +8 -2
- data/lib/jirametrics/html/aging_work_table.erb +11 -19
- data/lib/jirametrics/html/cycletime_histogram.erb +9 -3
- data/lib/jirametrics/html/cycletime_scatterplot.erb +10 -4
- data/lib/jirametrics/html/daily_wip_chart.erb +18 -5
- data/lib/jirametrics/html/expedited_chart.erb +11 -5
- data/lib/jirametrics/html/hierarchy_table.erb +1 -1
- data/lib/jirametrics/html/index.css +186 -0
- data/lib/jirametrics/html/index.erb +8 -36
- data/lib/jirametrics/html/sprint_burndown.erb +11 -6
- data/lib/jirametrics/html/story_point_accuracy_chart.erb +9 -4
- data/lib/jirametrics/html/throughput_chart.erb +11 -5
- data/lib/jirametrics/html_report_config.rb +28 -3
- data/lib/jirametrics/issue.rb +5 -3
- data/lib/jirametrics/jira_gateway.rb +5 -2
- data/lib/jirametrics/project_config.rb +14 -19
- data/lib/jirametrics/settings.json +7 -0
- data/lib/jirametrics/sprint_burndown.rb +10 -4
- data/lib/jirametrics/status_collection.rb +1 -1
- data/lib/jirametrics/story_point_accuracy_chart.rb +20 -10
- data/lib/jirametrics/throughput_chart.rb +10 -2
- data/lib/jirametrics.rb +2 -0
- metadata +7 -3
@@ -20,13 +20,16 @@ class DailyWipChart < ChartBase
|
|
20
20
|
header_text default_header_text
|
21
21
|
description_text default_description_text
|
22
22
|
|
23
|
-
if block
|
24
|
-
|
25
|
-
|
23
|
+
instance_eval(&block) if block
|
24
|
+
|
25
|
+
unless @group_by_block
|
26
26
|
grouping_rules do |issue, rules|
|
27
27
|
default_grouping_rules issue: issue, rules: rules
|
28
28
|
end
|
29
29
|
end
|
30
|
+
|
31
|
+
# Because this one will size itself as needed, we start with a smaller default size
|
32
|
+
# @canvas_height = 80
|
30
33
|
end
|
31
34
|
|
32
35
|
def run
|
@@ -36,6 +39,13 @@ class DailyWipChart < ChartBase
|
|
36
39
|
data_sets = possible_rules.collect do |grouping_rule|
|
37
40
|
make_data_set grouping_rule: grouping_rule, issue_rules_by_active_date: issue_rules_by_active_date
|
38
41
|
end
|
42
|
+
if @trend_lines
|
43
|
+
data_sets = @trend_lines.filter_map do |group_labels, line_color|
|
44
|
+
trend_line_data_set(data: data_sets, group_labels: group_labels, color: line_color)
|
45
|
+
end + data_sets
|
46
|
+
end
|
47
|
+
|
48
|
+
# grow_chart_height_if_too_many_data_sets data_sets.size
|
39
49
|
|
40
50
|
wrap_and_render(binding, __FILE__)
|
41
51
|
end
|
@@ -102,14 +112,14 @@ class DailyWipChart < ChartBase
|
|
102
112
|
label: grouping_rule.label,
|
103
113
|
data: data,
|
104
114
|
backgroundColor: grouping_rule.color || random_color,
|
105
|
-
borderColor: '
|
106
|
-
borderWidth: grouping_rule.color == '
|
115
|
+
borderColor: CssVariable['--wip-chart-border-color'],
|
116
|
+
borderWidth: grouping_rule.color.to_s == 'var(--body-background)' ? 1 : 0,
|
107
117
|
borderRadius: positive ? 0 : 5
|
108
118
|
}
|
109
119
|
end
|
110
120
|
|
111
121
|
def configure_rule issue:, date:
|
112
|
-
raise
|
122
|
+
raise "#{self.class}: grouping_rules must be set" if @group_by_block.nil?
|
113
123
|
|
114
124
|
rules = DailyGroupingRules.new
|
115
125
|
rules.current_date = date
|
@@ -120,4 +130,55 @@ class DailyWipChart < ChartBase
|
|
120
130
|
def grouping_rules &block
|
121
131
|
@group_by_block = block
|
122
132
|
end
|
133
|
+
|
134
|
+
def add_trend_line group_labels:, line_color:
|
135
|
+
(@trend_lines ||= []) << [group_labels, line_color]
|
136
|
+
end
|
137
|
+
|
138
|
+
def trend_line_data_set data:, group_labels:, color:
|
139
|
+
day_wip_hash = {}
|
140
|
+
data.each do |top_level|
|
141
|
+
next unless group_labels.include? top_level[:label]
|
142
|
+
|
143
|
+
top_level[:data].each do |datapoint|
|
144
|
+
date = datapoint[:x]
|
145
|
+
day_wip_hash[date] = (day_wip_hash[date] || 0) + datapoint[:y]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
points = day_wip_hash
|
150
|
+
.collect { |date, wip| [date.jd, wip] }
|
151
|
+
.sort_by(&:first)
|
152
|
+
|
153
|
+
calculator = TrendLineCalculator.new(points)
|
154
|
+
return nil unless calculator.valid?
|
155
|
+
|
156
|
+
data_points = calculator.chart_datapoints(
|
157
|
+
range: date_range.begin.jd..date_range.end.jd,
|
158
|
+
max_y: points.collect { |_date, wip| wip }.max
|
159
|
+
)
|
160
|
+
data_points.each do |point_hash|
|
161
|
+
point_hash[:x] = chart_format Date.jd(point_hash[:x])
|
162
|
+
end
|
163
|
+
|
164
|
+
{
|
165
|
+
type: 'line',
|
166
|
+
label: "Trendline",
|
167
|
+
data: data_points,
|
168
|
+
fill: false,
|
169
|
+
borderWidth: 1,
|
170
|
+
markerType: 'none',
|
171
|
+
borderColor: CssVariable[color],
|
172
|
+
borderDash: [6, 3],
|
173
|
+
pointStyle: 'dash',
|
174
|
+
hidden: false
|
175
|
+
}
|
176
|
+
end
|
177
|
+
|
178
|
+
def grow_chart_height_if_too_many_data_sets count
|
179
|
+
px_per_bar = 8
|
180
|
+
bars_per_issue = 0.5
|
181
|
+
preferred_height = count * px_per_bar * bars_per_issue
|
182
|
+
@canvas_height = preferred_height if @canvas_height.nil? || @canvas_height < preferred_height
|
183
|
+
end
|
123
184
|
end
|
@@ -271,7 +271,7 @@ class DataQualityReport < ChartBase
|
|
271
271
|
board_names = entry_list.collect { |entry| entry.issue.board.name.inspect }
|
272
272
|
entry_list.first.report(
|
273
273
|
problem_key: :issue_on_multiple_boards,
|
274
|
-
detail: "Found on boards: #{board_names.join(', ')}"
|
274
|
+
detail: "Found on boards: #{board_names.sort.join(', ')}"
|
275
275
|
)
|
276
276
|
end
|
277
277
|
end
|
@@ -84,7 +84,8 @@ class DependencyChart < ChartBase
|
|
84
84
|
result << issue_link.other_issue.key.inspect
|
85
85
|
result << '['
|
86
86
|
result << 'label=' << (link_rules.label || issue_link.label).inspect
|
87
|
-
result << ',color=' << (link_rules.line_color || '
|
87
|
+
result << ',color=' << (link_rules.line_color || 'gray').inspect
|
88
|
+
result << ',fontcolor=' << (link_rules.line_color || 'gray').inspect
|
88
89
|
result << ',dir=both' if link_rules.bidirectional_arrows?
|
89
90
|
result << '];'
|
90
91
|
result
|
@@ -101,12 +102,26 @@ class DependencyChart < ChartBase
|
|
101
102
|
tooltip = "#{issue.key}: #{issue.summary}"
|
102
103
|
result << ",tooltip=#{tooltip[0..80].inspect}"
|
103
104
|
unless issue_rules.color == :none
|
104
|
-
result << %(,style=filled,fillcolor="#{issue_rules.color || color_for(type: issue.type
|
105
|
+
result << %(,style=filled,fillcolor="#{issue_rules.color || color_for(type: issue.type)}")
|
105
106
|
end
|
106
107
|
result << ']'
|
107
108
|
result
|
108
109
|
end
|
109
110
|
|
111
|
+
# This used to pull colours from chart_base but the migration to CSS colours kept breaking
|
112
|
+
# this chart so we moved it here, until we're finished with the rest. TODO: Revisit whether
|
113
|
+
# this can also use customizable CSS colours
|
114
|
+
def color_for type:
|
115
|
+
@chart_colors = {
|
116
|
+
'Story' => '#90EE90',
|
117
|
+
'Task' => '#87CEFA',
|
118
|
+
'Bug' => '#ffdab9',
|
119
|
+
'Defect' => '#ffdab9',
|
120
|
+
'Epic' => '#fafad2',
|
121
|
+
'Spike' => '#DDA0DD' # light purple
|
122
|
+
}[type] ||= random_color
|
123
|
+
end
|
124
|
+
|
110
125
|
def build_dot_graph
|
111
126
|
issue_links = find_links
|
112
127
|
|
@@ -148,6 +163,7 @@ class DependencyChart < ChartBase
|
|
148
163
|
dot_graph = []
|
149
164
|
dot_graph << 'digraph mygraph {'
|
150
165
|
dot_graph << 'rankdir=LR'
|
166
|
+
dot_graph << 'bgcolor="transparent"'
|
151
167
|
|
152
168
|
# Sort the keys so they are proccessed in a deterministic order.
|
153
169
|
visible_issues.values.sort_by(&:key_as_i).each do |issue|
|
@@ -177,18 +193,6 @@ class DependencyChart < ChartBase
|
|
177
193
|
message
|
178
194
|
end
|
179
195
|
|
180
|
-
def default_color_for_issue issue
|
181
|
-
{
|
182
|
-
'Story' => '#90EE90',
|
183
|
-
'Task' => '#87CEFA',
|
184
|
-
'Bug' => '#f08080',
|
185
|
-
'Defect' => '#f08080',
|
186
|
-
'Epic' => '#fafad2',
|
187
|
-
'Spike' => '#7fffd4',
|
188
|
-
'Sub-task' => '#dcdcdc'
|
189
|
-
}[issue.type]
|
190
|
-
end
|
191
|
-
|
192
196
|
def shrink_svg svg
|
193
197
|
scale = 0.8
|
194
198
|
svg.sub(/width="([\d.]+)pt" height="([\d.]+)pt"/) do
|
@@ -10,7 +10,7 @@ class Downloader
|
|
10
10
|
attr_reader :file_system
|
11
11
|
|
12
12
|
# For testing only
|
13
|
-
attr_reader :start_date_in_query
|
13
|
+
attr_reader :start_date_in_query, :board_id_to_filter_id
|
14
14
|
|
15
15
|
def initialize download_config:, file_system:, jira_gateway:
|
16
16
|
@metadata = {}
|
@@ -55,7 +55,7 @@ class Downloader
|
|
55
55
|
|
56
56
|
def log text, both: false
|
57
57
|
@file_system.log text
|
58
|
-
puts text if both
|
58
|
+
puts text if both && !@quiet_mode
|
59
59
|
end
|
60
60
|
|
61
61
|
def find_board_ids
|
@@ -93,7 +93,7 @@ class Downloader
|
|
93
93
|
intercept_jql = @download_config.project_config.settings['intercept_jql']
|
94
94
|
jql = intercept_jql.call jql if intercept_jql
|
95
95
|
|
96
|
-
log " #{jql}"
|
96
|
+
log " JQL: #{jql}"
|
97
97
|
escaped_jql = CGI.escape jql
|
98
98
|
|
99
99
|
max_results = 100
|
@@ -148,19 +148,19 @@ class Downloader
|
|
148
148
|
|
149
149
|
def exit_if_call_failed json
|
150
150
|
# Sometimes Jira returns the singular form of errorMessage and sometimes the plural. Consistency FTW.
|
151
|
-
return unless json['errorMessages'] || json['errorMessage']
|
151
|
+
return unless json['error'] || json['errorMessages'] || json['errorMessage']
|
152
152
|
|
153
|
-
log "Download failed. See #{@logfile_name} for details.", both: true
|
153
|
+
log "Download failed. See #{@file_system.logfile_name} for details.", both: true
|
154
154
|
log " #{JSON.pretty_generate(json)}"
|
155
155
|
exit 1
|
156
156
|
end
|
157
157
|
|
158
158
|
def download_statuses
|
159
159
|
log ' Downloading all statuses', both: true
|
160
|
-
json = @jira_gateway.call_url relative_url:
|
160
|
+
json = @jira_gateway.call_url relative_url: '/rest/api/2/status'
|
161
161
|
|
162
162
|
@file_system.save_json(
|
163
|
-
json: json,
|
163
|
+
json: json,
|
164
164
|
filename: "#{@target_path}#{@download_config.project_config.file_prefix}_statuses.json"
|
165
165
|
)
|
166
166
|
end
|
@@ -168,13 +168,16 @@ class Downloader
|
|
168
168
|
def download_board_configuration board_id:
|
169
169
|
log " Downloading board configuration for board #{board_id}", both: true
|
170
170
|
json = @jira_gateway.call_url relative_url: "/rest/agile/1.0/board/#{board_id}/configuration"
|
171
|
-
exit_if_call_failed json
|
172
171
|
|
173
|
-
|
172
|
+
exit_if_call_failed json
|
174
173
|
|
175
174
|
file_prefix = @download_config.project_config.file_prefix
|
176
175
|
@file_system.save_json json: json, filename: "#{@target_path}#{file_prefix}_board_#{board_id}_configuration.json"
|
177
176
|
|
177
|
+
# We have a reported bug that blew up on this line. Moved it after the save so we can
|
178
|
+
# actually look at the returned json.
|
179
|
+
@board_id_to_filter_id[board_id] = json['filter']['id'].to_i
|
180
|
+
|
178
181
|
download_sprints board_id: board_id if json['type'] == 'scrum'
|
179
182
|
end
|
180
183
|
|
@@ -282,9 +285,8 @@ class Downloader
|
|
282
285
|
|
283
286
|
# Pick up any issues that had a status change in the range
|
284
287
|
start_date_text = @start_date_in_query.strftime '%Y-%m-%d'
|
285
|
-
end_date_text = today.strftime '%Y-%m-%d'
|
286
288
|
# find_in_range = %((status changed DURING ("#{start_date_text} 00:00","#{end_date_text} 23:59")))
|
287
|
-
find_in_range = %(
|
289
|
+
find_in_range = %(updated >= "#{start_date_text} 00:00")
|
288
290
|
|
289
291
|
segments << "(#{find_in_range} OR #{catch_all})"
|
290
292
|
end
|
@@ -10,9 +10,11 @@
|
|
10
10
|
# single team. For that reason, we look at slightly different things that we would on a single team board.
|
11
11
|
|
12
12
|
class Exporter
|
13
|
-
def aggregated_project name:, project_names:
|
13
|
+
def aggregated_project name:, project_names:, settings: {}
|
14
14
|
project name: name do
|
15
15
|
puts name
|
16
|
+
self.settings.merge! settings
|
17
|
+
|
16
18
|
aggregate do
|
17
19
|
project_names.each do |project_name|
|
18
20
|
include_issues_from project_name
|
@@ -28,32 +30,27 @@ class Exporter
|
|
28
30
|
end
|
29
31
|
|
30
32
|
html_report do
|
33
|
+
html '<h1>Boards included in this report</h1><ul>', type: :header
|
34
|
+
board_lines = []
|
35
|
+
included_projects.each do |project|
|
36
|
+
project.all_boards.values.each do |board|
|
37
|
+
board_lines << "<a href='#{project.file_prefix}.html'>#{board.name}</a> from project #{project.name}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
board_lines.sort.each { |line| html "<li>#{line}</li>", type: :header }
|
41
|
+
html '</ul>', type: :header
|
42
|
+
|
31
43
|
cycletime_scatterplot do
|
32
44
|
show_trend_lines
|
45
|
+
# For an aggregated report we group by board rather than by type
|
33
46
|
grouping_rules do |issue, rules|
|
34
47
|
rules.label = issue.board.name
|
35
48
|
end
|
36
49
|
end
|
37
50
|
# aging_work_in_progress_chart
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
<p>How much work is in progress, grouped by the parent of the issue. This will give us an
|
42
|
-
indication of how focused we are on higher level objectives. If there are many parent
|
43
|
-
tickets in progress at the same time, either this team has their focus scattered or we
|
44
|
-
aren't doing a good job of
|
45
|
-
<a href="https://improvingflow.com/2024/02/21/slicing-epics.html">splitting those parent
|
46
|
-
tickets</a>. Neither of those is desirable.</p>
|
47
|
-
<p>If you're expecting all work items to have parents and there are a lot that don't,
|
48
|
-
that's also something to look at. Consider whether there is even value in aggregating
|
49
|
-
these projects if they don't share parent dependencies. Aggregation helps us when we're
|
50
|
-
looking at related work and if there aren't parent dependencies then the work may not
|
51
|
-
be related.</p>
|
52
|
-
TEXT
|
53
|
-
grouping_rules do |issue, rules|
|
54
|
-
rules.label = issue.parent&.key || 'No parent'
|
55
|
-
rules.color = 'white' if rules.label == 'No parent'
|
56
|
-
end
|
51
|
+
daily_wip_by_parent_chart do
|
52
|
+
# When aggregating, the chart tends to need more vertical space
|
53
|
+
canvas height: 400, width: 800
|
57
54
|
end
|
58
55
|
aging_work_table do
|
59
56
|
# In an aggregated report, we likely only care about items that are old so exclude anything
|
@@ -6,13 +6,19 @@
|
|
6
6
|
# See https://github.com/mikebowler/jirametrics/wiki/Examples-folder for more
|
7
7
|
class Exporter
|
8
8
|
def standard_project name:, file_prefix:, ignore_issues: nil, starting_status: nil, boards: {},
|
9
|
-
default_board: nil, anonymize: false
|
9
|
+
default_board: nil, anonymize: false, settings: {}, status_category_mappings: {}
|
10
10
|
|
11
11
|
project name: name do
|
12
12
|
puts name
|
13
13
|
self.anonymize if anonymize
|
14
14
|
|
15
15
|
settings['blocked_link_text'] = ['is blocked by']
|
16
|
+
self.settings.merge! settings
|
17
|
+
|
18
|
+
status_category_mappings.each do |status, category|
|
19
|
+
status_category_mapping status: status, category: category
|
20
|
+
end
|
21
|
+
|
16
22
|
file_prefix file_prefix
|
17
23
|
download do
|
18
24
|
rolling_date_count 90
|
@@ -50,7 +56,7 @@ class Exporter
|
|
50
56
|
type: :header
|
51
57
|
end
|
52
58
|
|
53
|
-
discard_changes_before status_becomes: (starting_status || :backlog)
|
59
|
+
discard_changes_before status_becomes: (starting_status || :backlog) # rubocop:disable Style/RedundantParentheses
|
54
60
|
|
55
61
|
cycletime_scatterplot do
|
56
62
|
show_trend_lines
|
@@ -77,21 +83,7 @@ class Exporter
|
|
77
83
|
aging_work_table
|
78
84
|
daily_wip_by_age_chart
|
79
85
|
daily_wip_by_blocked_stalled_chart
|
80
|
-
|
81
|
-
header_text 'Daily WIP by Parent'
|
82
|
-
description_text <<-TEXT
|
83
|
-
How much work is in progress, grouped by the parent of the issue. This will give us an
|
84
|
-
indication of how focused we are on higher level objectives. If there are many parent
|
85
|
-
tickets in progress at the same time, either this team has their focus scattered or we
|
86
|
-
aren't doing a good job of
|
87
|
-
<a href="https://improvingflow.com/2024/02/21/slicing-epics.html">splitting those parent
|
88
|
-
tickets</a>. Neither of those is desirable.
|
89
|
-
TEXT
|
90
|
-
grouping_rules do |issue, rules|
|
91
|
-
rules.label = issue.parent&.key || 'No parent'
|
92
|
-
rules.color = 'white' if rules.label == 'No parent'
|
93
|
-
end
|
94
|
-
end
|
86
|
+
daily_wip_by_parent_chart
|
95
87
|
expedited_chart
|
96
88
|
sprint_burndown
|
97
89
|
story_point_accuracy_chart
|
@@ -107,7 +99,7 @@ class Exporter
|
|
107
99
|
when 'Sync'
|
108
100
|
rules.use_bidirectional_arrows
|
109
101
|
else
|
110
|
-
# This is a link type that we don't
|
102
|
+
# This is a link type that we don't recognize. Dump it to standard out to draw attention
|
111
103
|
# to it.
|
112
104
|
puts "name=#{link.name}, label=#{link.label}"
|
113
105
|
end
|
@@ -3,11 +3,14 @@
|
|
3
3
|
require 'jirametrics/chart_base'
|
4
4
|
|
5
5
|
class ExpeditedChart < ChartBase
|
6
|
-
EXPEDITED_SEGMENT =
|
6
|
+
EXPEDITED_SEGMENT = ChartBase.new.tap do |segment|
|
7
7
|
def segment.to_json *_args
|
8
|
+
expedited = CssVariable.new('--expedited-color').to_json
|
9
|
+
not_expedited = CssVariable.new('--expedited-chart-no-longer-expedited').to_json
|
10
|
+
|
8
11
|
<<~SNIPPET
|
9
12
|
{
|
10
|
-
borderColor: ctx => expedited(ctx,
|
13
|
+
borderColor: ctx => expedited(ctx, #{expedited}) || notExpedited(ctx, #{not_expedited}),
|
11
14
|
borderDash: ctx => notExpedited(ctx, [6, 6])
|
12
15
|
}
|
13
16
|
SNIPPET
|
@@ -22,19 +25,18 @@ class ExpeditedChart < ChartBase
|
|
22
25
|
|
23
26
|
header_text 'Expedited work'
|
24
27
|
description_text <<-HTML
|
25
|
-
<p>
|
28
|
+
<div class="p">
|
26
29
|
This chart only shows issues that have been expedited at some point. We care about these as
|
27
30
|
any form of expedited work will affect the entire system and will slow down non-expedited work.
|
28
31
|
Refer to this article on
|
29
32
|
<a href="https://improvingflow.com/2021/06/16/classes-of-service.html">classes of service</a>
|
30
33
|
for a longer explanation on why we want to avoid expedited work.
|
31
|
-
</
|
32
|
-
<p>
|
33
|
-
The
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
</p>
|
34
|
+
</div>
|
35
|
+
<div class="p">
|
36
|
+
The colour of the line indicates time that this issue was #{color_block '--expedited-color'} expedited
|
37
|
+
or #{color_block '--expedited-chart-no-longer-expedited'} not expedited.
|
38
|
+
</div>
|
39
|
+
#{describe_non_working_days}
|
38
40
|
HTML
|
39
41
|
end
|
40
42
|
|
@@ -124,20 +126,20 @@ class ExpeditedChart < ChartBase
|
|
124
126
|
case action
|
125
127
|
when :issue_started
|
126
128
|
data << make_point(issue: issue, time: time, label: 'Started', expedited: expedited)
|
127
|
-
dot_colors << '
|
129
|
+
dot_colors << CssVariable['--expedited-chart-dot-issue-started-color']
|
128
130
|
point_styles << 'rect'
|
129
131
|
when :issue_stopped
|
130
132
|
data << make_point(issue: issue, time: time, label: 'Completed', expedited: expedited)
|
131
|
-
dot_colors << '
|
133
|
+
dot_colors << CssVariable['--expedited-chart-dot-issue-stopped-color']
|
132
134
|
point_styles << 'rect'
|
133
135
|
when :expedite_start
|
134
136
|
data << make_point(issue: issue, time: time, label: 'Expedited', expedited: true)
|
135
|
-
dot_colors << '
|
137
|
+
dot_colors << CssVariable['--expedited-chart-dot-expedite-started-color']
|
136
138
|
point_styles << 'circle'
|
137
139
|
expedited = true
|
138
140
|
when :expedite_stop
|
139
141
|
data << make_point(issue: issue, time: time, label: 'Not expedited', expedited: false)
|
140
|
-
dot_colors << '
|
142
|
+
dot_colors << CssVariable['--expedited-chart-dot-expedite-stopped-color']
|
141
143
|
point_styles << 'circle'
|
142
144
|
expedited = false
|
143
145
|
else
|
@@ -149,7 +151,7 @@ class ExpeditedChart < ChartBase
|
|
149
151
|
last_change_time = expedite_data[-1][0].to_date
|
150
152
|
if last_change_time && last_change_time <= date_range.end && stopped_time.nil?
|
151
153
|
data << make_point(issue: issue, time: date_range.end, label: 'Still ongoing', expedited: expedited)
|
152
|
-
dot_colors << '
|
154
|
+
dot_colors << '' # It won't be visible so it doesn't matter
|
153
155
|
point_styles << 'dash'
|
154
156
|
end
|
155
157
|
end
|
data/lib/jirametrics/exporter.rb
CHANGED
@@ -3,9 +3,9 @@
|
|
3
3
|
require 'fileutils'
|
4
4
|
|
5
5
|
class Object
|
6
|
-
def deprecated message:
|
6
|
+
def deprecated message:, date:
|
7
7
|
text = +''
|
8
|
-
text <<
|
8
|
+
text << "Deprecated(#{date}):"
|
9
9
|
text << message
|
10
10
|
text << "\n-> Called from #{caller(1..1).first}"
|
11
11
|
warn text
|
@@ -16,7 +16,14 @@ class Exporter
|
|
16
16
|
attr_reader :project_configs, :file_system
|
17
17
|
|
18
18
|
def self.configure &block
|
19
|
-
|
19
|
+
logfile_name = 'jirametrics.log'
|
20
|
+
logfile = File.open logfile_name, 'w'
|
21
|
+
file_system = FileSystem.new
|
22
|
+
file_system.logfile = logfile
|
23
|
+
file_system.logfile_name = logfile_name
|
24
|
+
|
25
|
+
exporter = Exporter.new file_system: file_system
|
26
|
+
|
20
27
|
exporter.instance_eval(&block)
|
21
28
|
@@instance = exporter
|
22
29
|
end
|
@@ -41,25 +48,24 @@ class Exporter
|
|
41
48
|
|
42
49
|
def download name_filter:
|
43
50
|
@downloading = true
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
next if project.aggregated_project?
|
52
|
-
|
53
|
-
project.download_config.run
|
54
|
-
downloader = Downloader.new(
|
55
|
-
download_config: project.download_config,
|
56
|
-
file_system: file_system,
|
57
|
-
jira_gateway: JiraGateway.new(file_system: file_system)
|
58
|
-
)
|
59
|
-
downloader.run
|
51
|
+
each_project_config(name_filter: name_filter) do |project|
|
52
|
+
project.evaluate_next_level
|
53
|
+
next if project.aggregated_project?
|
54
|
+
|
55
|
+
unless project.download_config
|
56
|
+
raise "Project #{project.name.inspect} is missing a download section in the config. " \
|
57
|
+
'That is required in order to download'
|
60
58
|
end
|
59
|
+
|
60
|
+
project.download_config.run
|
61
|
+
downloader = Downloader.new(
|
62
|
+
download_config: project.download_config,
|
63
|
+
file_system: file_system,
|
64
|
+
jira_gateway: JiraGateway.new(file_system: file_system)
|
65
|
+
)
|
66
|
+
downloader.run
|
61
67
|
end
|
62
|
-
puts "Full output from downloader in #{logfile_name}"
|
68
|
+
puts "Full output from downloader in #{file_system.logfile_name}"
|
63
69
|
end
|
64
70
|
|
65
71
|
def each_project_config name_filter:
|
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class GroupingRules < Rules
|
4
|
-
attr_accessor :label
|
4
|
+
attr_accessor :label
|
5
|
+
attr_reader :color
|
5
6
|
|
6
7
|
def eql? other
|
7
8
|
other.label == @label && other.color == @color
|
@@ -10,4 +11,9 @@ class GroupingRules < Rules
|
|
10
11
|
def group
|
11
12
|
[@label, @color]
|
12
13
|
end
|
14
|
+
|
15
|
+
def color= color
|
16
|
+
color = CssVariable[color] unless color.is_a?(CssVariable)
|
17
|
+
@color = color
|
18
|
+
end
|
13
19
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
<div>
|
1
|
+
<div class="chart">
|
2
2
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
3
3
|
</div>
|
4
4
|
<script>
|
@@ -19,14 +19,20 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
19
19
|
stacked: false,
|
20
20
|
title: {
|
21
21
|
display: false
|
22
|
-
}
|
22
|
+
},
|
23
|
+
grid: {
|
24
|
+
color: <%= CssVariable['--grid-line-color'].to_json %>
|
25
|
+
},
|
23
26
|
},
|
24
27
|
y: {
|
25
28
|
stacked: true,
|
26
29
|
position: 'right',
|
27
30
|
ticks: {
|
28
31
|
display: true
|
29
|
-
}
|
32
|
+
},
|
33
|
+
grid: {
|
34
|
+
color: <%= CssVariable['--grid-line-color'].to_json %>
|
35
|
+
},
|
30
36
|
}
|
31
37
|
},
|
32
38
|
plugins: {
|
@@ -38,8 +44,8 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
38
44
|
type: 'box',
|
39
45
|
xMin: '<%= range.begin %>T00:00:00',
|
40
46
|
xMax: '<%= range.end %>T23:59:59',
|
41
|
-
backgroundColor: '
|
42
|
-
borderColor: '
|
47
|
+
backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
|
48
|
+
borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
|
43
49
|
},
|
44
50
|
<% end %>
|
45
51
|
|
@@ -48,7 +54,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
48
54
|
type: 'line',
|
49
55
|
xMin: '<%= percentage_line_x %>',
|
50
56
|
xMax: '<%= percentage_line_x %>',
|
51
|
-
borderColor: '
|
57
|
+
borderColor: <%= CssVariable.new('--aging-work-bar-chart-percentage-line-color').to_json %>,
|
52
58
|
borderWidth: 1,
|
53
59
|
drawTime: 'afterDraw'
|
54
60
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
<div>
|
1
|
+
<div class="chart">
|
2
2
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
3
3
|
</div>
|
4
4
|
<script>
|
@@ -20,7 +20,10 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
|
|
20
20
|
scaleLabel: {
|
21
21
|
display: true,
|
22
22
|
labelString: 'Date Completed'
|
23
|
-
}
|
23
|
+
},
|
24
|
+
grid: {
|
25
|
+
color: <%= CssVariable['--grid-line-color'].to_json %>
|
26
|
+
},
|
24
27
|
},
|
25
28
|
y: {
|
26
29
|
scaleLabel: {
|
@@ -31,6 +34,9 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
|
|
31
34
|
display: true,
|
32
35
|
text: 'Age in days'
|
33
36
|
},
|
37
|
+
grid: {
|
38
|
+
color: <%= CssVariable['--grid-line-color'].to_json %>
|
39
|
+
},
|
34
40
|
}
|
35
41
|
},
|
36
42
|
plugins: {
|