jirametrics 3.3.1 → 3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/jirametrics/aging_work_bar_chart.rb +2 -4
- data/lib/jirametrics/aging_work_in_progress_chart.rb +26 -26
- data/lib/jirametrics/atlassian_document_format.rb +22 -14
- data/lib/jirametrics/board_movement_calculator.rb +0 -23
- data/lib/jirametrics/change_item.rb +4 -8
- data/lib/jirametrics/chart_base.rb +44 -15
- data/lib/jirametrics/chart_format.rb +15 -0
- data/lib/jirametrics/daily_view.rb +6 -7
- data/lib/jirametrics/daily_wip_by_parent_chart.rb +1 -1
- data/lib/jirametrics/data_quality_report.rb +3 -3
- data/lib/jirametrics/dependency_chart.rb +3 -5
- data/lib/jirametrics/estimate_accuracy_chart.rb +37 -8
- data/lib/jirametrics/expedited_chart.rb +5 -6
- data/lib/jirametrics/exporter.rb +24 -0
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +3 -5
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +10 -7
- data/lib/jirametrics/html/estimate_accuracy_chart.erb +34 -1
- data/lib/jirametrics/html/index.css +10 -9
- data/lib/jirametrics/html/index.js +13 -0
- data/lib/jirametrics/html/time_based_scatterplot.erb +2 -1
- data/lib/jirametrics/issue.rb +5 -7
- data/lib/jirametrics/issue_printer.rb +5 -3
- data/lib/jirametrics/jira_gateway.rb +5 -7
- data/lib/jirametrics/sprint_burndown.rb +2 -2
- data/lib/jirametrics/testing/mock_board.rb +34 -0
- data/lib/jirametrics/testing/mock_change_item.rb +112 -0
- data/lib/jirametrics/testing/mock_cycle_time_config.rb +73 -0
- data/lib/jirametrics/testing/mock_issue.rb +108 -0
- data/lib/jirametrics/testing.rb +88 -0
- data/lib/jirametrics/time_based_histogram.rb +2 -4
- data/lib/jirametrics/time_based_scatterplot.rb +2 -4
- data/lib/jirametrics/user.rb +16 -2
- data/lib/jirametrics/wip_by_column_chart.rb +1 -2
- data/lib/jirametrics.rb +6 -1
- metadata +7 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Support for writing tests against jirametrics from outside this repo, for anyone building their
|
|
4
|
+
# own charts or other extensions.
|
|
5
|
+
#
|
|
6
|
+
# Require this and you get the whole library loaded along with it, so a client test file needs
|
|
7
|
+
# nothing else:
|
|
8
|
+
#
|
|
9
|
+
# require 'jirametrics/testing'
|
|
10
|
+
#
|
|
11
|
+
# What is supported is exactly what this module holds: the Mock* classes below, plus the methods
|
|
12
|
+
# to_time, to_date and empty_config_block. The require above loads the rest of the library too,
|
|
13
|
+
# and that is a side effect of loading rather than a promise about any of it.
|
|
14
|
+
#
|
|
15
|
+
# The methods, and only the methods, arrive through include. Constants do not, because constant
|
|
16
|
+
# lookup in an example block runs through the block's lexical scope rather than the ancestors of
|
|
17
|
+
# the class it runs against:
|
|
18
|
+
#
|
|
19
|
+
# RSpec.configure { |config| config.include JiraMetrics::Testing }
|
|
20
|
+
#
|
|
21
|
+
# to_time '2024-01-01' # works, include supplies it
|
|
22
|
+
# JiraMetrics::Testing::MockIssue.empty(...) # classes are named in full
|
|
23
|
+
#
|
|
24
|
+
# Add your own alias if the full name grates. That is your namespace to spend, so we don't spend
|
|
25
|
+
# it for you:
|
|
26
|
+
#
|
|
27
|
+
# MockIssue = JiraMetrics::Testing::MockIssue
|
|
28
|
+
#
|
|
29
|
+
# The require of 'jirametrics' below is not optional. This file nests inside the JiraMetrics class,
|
|
30
|
+
# and reopening it before Thor has defined it raises a superclass mismatch.
|
|
31
|
+
require 'jirametrics'
|
|
32
|
+
require 'require_all'
|
|
33
|
+
require_rel '.'
|
|
34
|
+
|
|
35
|
+
class JiraMetrics
|
|
36
|
+
module Testing
|
|
37
|
+
# Accepts the date formats a test is likely to write, rather than only what Time.parse takes.
|
|
38
|
+
# A bare date means midnight, and a missing offset means UTC:
|
|
39
|
+
#
|
|
40
|
+
# to_time '2024-01-01' => 2024-01-01 00:00:00 +0000
|
|
41
|
+
# to_time '2024-01-01T12:34:56' => 2024-01-01 12:34:56 +0000
|
|
42
|
+
# to_time '2024-01-01T12:34:56.789' => 2024-01-01 12:34:56.789 +0000
|
|
43
|
+
# to_time '2024-01-01T12:34:56 +10:00' => 2024-01-01 12:34:56 +1000
|
|
44
|
+
TIME_PATTERN = /
|
|
45
|
+
^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})
|
|
46
|
+
(?<remainder>T(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?<fraction>\.\d+)?
|
|
47
|
+
\s*(?<offset>[+-]\d{2}:?\d{2})?)?$
|
|
48
|
+
/x
|
|
49
|
+
private_constant :TIME_PATTERN
|
|
50
|
+
|
|
51
|
+
extend self
|
|
52
|
+
|
|
53
|
+
def to_time input
|
|
54
|
+
return input unless input.is_a? String
|
|
55
|
+
|
|
56
|
+
matches = input.match TIME_PATTERN
|
|
57
|
+
raise "Can't parse string: #{input.inspect}" unless matches
|
|
58
|
+
|
|
59
|
+
Time.parse format_matched_time(matches)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The companion to to_time, for the many places a test wants a Date rather than a Time. Passes a
|
|
63
|
+
# Date straight through for the same reason to_time passes a Time through.
|
|
64
|
+
def to_date input
|
|
65
|
+
input.is_a?(Date) ? input : Date.parse(input)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Every chart is constructed with a configuration block, so a test that only wants to exercise
|
|
69
|
+
# the chart itself still has to supply one. This is that block, doing nothing:
|
|
70
|
+
#
|
|
71
|
+
# chart = MyCustomChart.new empty_config_block
|
|
72
|
+
# chart.issues = [issue]
|
|
73
|
+
def empty_config_block = ->(_) {}
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# Every optional part gets a default, which is what makes this long rather than interesting.
|
|
78
|
+
def format_matched_time matches
|
|
79
|
+
format(
|
|
80
|
+
'%<year>04d-%<month>02d-%<day>02dT-%<hour>02d:%<minute>02d:%<second>02d%<fraction>s%<offset>s',
|
|
81
|
+
year: matches[:year].to_i, month: matches[:month].to_i, day: matches[:day].to_i,
|
|
82
|
+
hour: (matches[:hour] || 0).to_i, minute: (matches[:minute] || 0).to_i,
|
|
83
|
+
second: (matches[:second] || 0).to_i,
|
|
84
|
+
fraction: matches[:fraction] || '', offset: matches[:offset] || '+0000'
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -13,6 +13,7 @@ class TimeBasedHistogram < TimeBasedChart
|
|
|
13
13
|
def initialize
|
|
14
14
|
super
|
|
15
15
|
|
|
16
|
+
no_data_text '<%= render_header %><div>No data matched the selected criteria. Nothing to show.</div>'
|
|
16
17
|
percentiles [50, 85, 98]
|
|
17
18
|
@show_stats = true
|
|
18
19
|
end
|
|
@@ -77,10 +78,7 @@ class TimeBasedHistogram < TimeBasedChart
|
|
|
77
78
|
)
|
|
78
79
|
end
|
|
79
80
|
|
|
80
|
-
if data_sets.empty?
|
|
81
|
-
return "<h1 class='foldable'>#{@header_text}</h1>" \
|
|
82
|
-
'<div>No data matched the selected criteria. Nothing to show.</div>'
|
|
83
|
-
end
|
|
81
|
+
return render_no_data if data_sets.empty?
|
|
84
82
|
|
|
85
83
|
wrap_and_render(binding, __FILE__)
|
|
86
84
|
end
|
|
@@ -22,6 +22,7 @@ class TimeBasedScatterplot < TimeBasedChart
|
|
|
22
22
|
def initialize
|
|
23
23
|
super
|
|
24
24
|
|
|
25
|
+
no_data_text '<%= render_header %><div>No data matched the selected criteria. Nothing to show.</div>'
|
|
25
26
|
@percentage_lines = []
|
|
26
27
|
@highest_y_value = 0
|
|
27
28
|
@percentiles = [85]
|
|
@@ -56,10 +57,7 @@ class TimeBasedScatterplot < TimeBasedChart
|
|
|
56
57
|
}
|
|
57
58
|
end
|
|
58
59
|
|
|
59
|
-
if data_sets.empty?
|
|
60
|
-
return "<h1 class='foldable'>#{@header_text}</h1>" \
|
|
61
|
-
'<div>No data matched the selected criteria. Nothing to show.</div>'
|
|
62
|
-
end
|
|
60
|
+
return render_no_data if data_sets.empty?
|
|
63
61
|
|
|
64
62
|
wrap_and_render(binding, __FILE__)
|
|
65
63
|
end
|
data/lib/jirametrics/user.rb
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class User
|
|
4
|
+
RENDERED_AVATAR_SIZE = '16x16'
|
|
5
|
+
|
|
6
|
+
# Jira omits the user entirely when there isn't one, so callers get nil rather than a User that
|
|
7
|
+
# raises on every question.
|
|
8
|
+
def self.from_raw raw
|
|
9
|
+
raw && new(raw: raw)
|
|
10
|
+
end
|
|
11
|
+
|
|
4
12
|
def initialize raw:
|
|
5
13
|
@raw = raw
|
|
6
14
|
end
|
|
7
15
|
|
|
8
16
|
def account_id = @raw['accountId']
|
|
9
|
-
def avatar_url = @raw['avatarUrls']['16x16']
|
|
10
17
|
def active? = @raw['active']
|
|
11
|
-
def
|
|
18
|
+
def email_address = @raw['emailAddress']
|
|
19
|
+
|
|
20
|
+
# Nil when there's no name to show, in either spelling. Callers want different things in that
|
|
21
|
+
# case, so the default belongs at the call site. 'name' is what older Jira called displayName,
|
|
22
|
+
# and newer versions of Cloud don't return it.
|
|
23
|
+
def display_name = @raw['displayName'] || @raw['name']
|
|
24
|
+
|
|
25
|
+
def avatar_url = @raw['avatarUrls']&.[](RENDERED_AVATAR_SIZE)
|
|
12
26
|
end
|
|
@@ -10,7 +10,7 @@ class WipByColumnChart < ChartBase
|
|
|
10
10
|
# Long only because of the inline description_text heredoc and one-time setup; splitting wouldn't help.
|
|
11
11
|
def initialize block # rubocop:disable Metrics/MethodLength
|
|
12
12
|
super()
|
|
13
|
-
header_text 'WIP by column'
|
|
13
|
+
header_text 'WIP by column on board: <%= current_board.name %>'
|
|
14
14
|
description_text <<-HTML
|
|
15
15
|
<p>
|
|
16
16
|
This chart shows how much time each board column has spent at different WIP (Work in Progress) levels.
|
|
@@ -58,7 +58,6 @@ class WipByColumnChart < ChartBase
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def run
|
|
61
|
-
@header_text += " on board: #{current_board.name}"
|
|
62
61
|
stats = column_stats
|
|
63
62
|
@column_names = stats.collect(&:name)
|
|
64
63
|
@wip_data = stats.collect do |stat|
|
data/lib/jirametrics.rb
CHANGED
|
@@ -157,7 +157,12 @@ class JiraMetrics < Thor
|
|
|
157
157
|
exit 1
|
|
158
158
|
end
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
# The testing surface has no place in a production run, and the examples are opt-in: a config
|
|
161
|
+
# that wants standard_project requires it by name. Matching on the prefix also excludes
|
|
162
|
+
# testing.rb, which has to go with its directory because it require_rel's the whole tree.
|
|
163
|
+
not_autoloaded = %w[testing examples].collect { |name| File.join __dir__, 'jirametrics', name }
|
|
164
|
+
require_all(Dir[File.join(__dir__, 'jirametrics', '**', '*.rb')]
|
|
165
|
+
.reject { |file| not_autoloaded.any? { |prefix| file.start_with? prefix } })
|
|
161
166
|
# Set only after the require above, so Exporter is defined. The config file we load below calls
|
|
162
167
|
# Exporter.configure, which opens this log; the MCP server passes its own name so it doesn't
|
|
163
168
|
# truncate jirametrics.log.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jirametrics
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: '3.4'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Bowler
|
|
@@ -122,6 +122,7 @@ files:
|
|
|
122
122
|
- lib/jirametrics/cfd_data_builder.rb
|
|
123
123
|
- lib/jirametrics/change_item.rb
|
|
124
124
|
- lib/jirametrics/chart_base.rb
|
|
125
|
+
- lib/jirametrics/chart_format.rb
|
|
125
126
|
- lib/jirametrics/color_palette.rb
|
|
126
127
|
- lib/jirametrics/columns_config.rb
|
|
127
128
|
- lib/jirametrics/css_variable.rb
|
|
@@ -201,6 +202,11 @@ files:
|
|
|
201
202
|
- lib/jirametrics/status.rb
|
|
202
203
|
- lib/jirametrics/status_collection.rb
|
|
203
204
|
- lib/jirametrics/stitcher.rb
|
|
205
|
+
- lib/jirametrics/testing.rb
|
|
206
|
+
- lib/jirametrics/testing/mock_board.rb
|
|
207
|
+
- lib/jirametrics/testing/mock_change_item.rb
|
|
208
|
+
- lib/jirametrics/testing/mock_cycle_time_config.rb
|
|
209
|
+
- lib/jirametrics/testing/mock_issue.rb
|
|
204
210
|
- lib/jirametrics/throughput_by_completed_resolution_chart.rb
|
|
205
211
|
- lib/jirametrics/throughput_chart.rb
|
|
206
212
|
- lib/jirametrics/time_based_chart.rb
|