jirametrics 2.14 → 2.22
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 +176 -134
- data/lib/jirametrics/anonymizer.rb +8 -6
- data/lib/jirametrics/atlassian_document_format.rb +3 -3
- data/lib/jirametrics/bar_chart_range.rb +17 -0
- data/lib/jirametrics/board.rb +4 -0
- data/lib/jirametrics/board_config.rb +3 -1
- data/lib/jirametrics/change_item.rb +11 -4
- data/lib/jirametrics/chart_base.rb +34 -2
- data/lib/jirametrics/cycletime_config.rb +22 -4
- data/lib/jirametrics/cycletime_histogram.rb +3 -1
- data/lib/jirametrics/cycletime_scatterplot.rb +36 -17
- data/lib/jirametrics/daily_view.rb +6 -20
- data/lib/jirametrics/daily_wip_by_age_chart.rb +3 -4
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +13 -3
- data/lib/jirametrics/daily_wip_chart.rb +1 -1
- data/lib/jirametrics/data_quality_report.rb +8 -3
- data/lib/jirametrics/dependency_chart.rb +4 -1
- data/lib/jirametrics/downloader.rb +34 -99
- data/lib/jirametrics/downloader_for_cloud.rb +202 -0
- data/lib/jirametrics/downloader_for_data_center.rb +94 -0
- data/lib/jirametrics/examples/standard_project.rb +9 -9
- data/lib/jirametrics/expedited_chart.rb +1 -1
- data/lib/jirametrics/exporter.rb +12 -5
- data/lib/jirametrics/file_system.rb +24 -1
- data/lib/jirametrics/fix_version.rb +13 -0
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +1 -1
- data/lib/jirametrics/groupable_issue_chart.rb +7 -1
- data/lib/jirametrics/html/aging_work_bar_chart.erb +2 -1
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +3 -1
- data/lib/jirametrics/html/aging_work_table.erb +2 -0
- data/lib/jirametrics/html/collapsible_issues_panel.erb +2 -2
- data/lib/jirametrics/html/cycletime_histogram.erb +4 -2
- data/lib/jirametrics/html/cycletime_scatterplot.erb +6 -6
- data/lib/jirametrics/html/daily_wip_chart.erb +2 -0
- data/lib/jirametrics/html/estimate_accuracy_chart.erb +2 -0
- data/lib/jirametrics/html/expedited_chart.erb +3 -1
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +2 -0
- data/lib/jirametrics/html/index.css +16 -9
- data/lib/jirametrics/html/index.erb +3 -35
- data/lib/jirametrics/html/index.js +114 -0
- data/lib/jirametrics/html/sprint_burndown.erb +11 -3
- data/lib/jirametrics/html/throughput_chart.erb +2 -2
- data/lib/jirametrics/html_generator.rb +31 -0
- data/lib/jirametrics/html_report_config.rb +8 -25
- data/lib/jirametrics/issue.rb +125 -19
- data/lib/jirametrics/jira_gateway.rb +55 -17
- data/lib/jirametrics/project_config.rb +22 -2
- data/lib/jirametrics/raw_javascript.rb +13 -0
- data/lib/jirametrics/settings.json +3 -1
- data/lib/jirametrics/sprint.rb +12 -0
- data/lib/jirametrics/sprint_burndown.rb +6 -2
- data/lib/jirametrics/stitcher.rb +75 -0
- data/lib/jirametrics.rb +26 -70
- metadata +10 -3
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class DownloaderForDataCenter < Downloader
|
|
4
|
+
def jira_instance_type
|
|
5
|
+
'Jira DataCenter'
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def download_issues board:
|
|
9
|
+
log " Downloading primary issues for board #{board.id}", both: true
|
|
10
|
+
path = File.join(@target_path, "#{file_prefix}_issues/")
|
|
11
|
+
unless Dir.exist?(path)
|
|
12
|
+
log " Creating path #{path}"
|
|
13
|
+
Dir.mkdir(path)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
filter_id = board_id_to_filter_id[board.id]
|
|
17
|
+
jql = make_jql(filter_id: filter_id)
|
|
18
|
+
jira_search_by_jql(jql: jql, initial_query: true, board: board, path: path)
|
|
19
|
+
|
|
20
|
+
log " Downloading linked issues for board #{board.id}", both: true
|
|
21
|
+
loop do
|
|
22
|
+
@issue_keys_pending_download.reject! { |key| @issue_keys_downloaded_in_current_run.include? key }
|
|
23
|
+
break if @issue_keys_pending_download.empty?
|
|
24
|
+
|
|
25
|
+
keys_to_request = @issue_keys_pending_download[0..99]
|
|
26
|
+
@issue_keys_pending_download.reject! { |key| keys_to_request.include? key }
|
|
27
|
+
jql = "key in (#{keys_to_request.join(', ')})"
|
|
28
|
+
jira_search_by_jql(jql: jql, initial_query: false, board: board, path: path)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def jira_search_by_jql jql:, initial_query:, board:, path:
|
|
33
|
+
intercept_jql = @download_config.project_config.settings['intercept_jql']
|
|
34
|
+
jql = intercept_jql.call jql if intercept_jql
|
|
35
|
+
|
|
36
|
+
log " JQL: #{jql}"
|
|
37
|
+
escaped_jql = CGI.escape jql
|
|
38
|
+
|
|
39
|
+
max_results = 100
|
|
40
|
+
start_at = 0
|
|
41
|
+
total = 1
|
|
42
|
+
while start_at < total
|
|
43
|
+
json = @jira_gateway.call_url relative_url: '/rest/api/2/search' \
|
|
44
|
+
"?jql=#{escaped_jql}&maxResults=#{max_results}&startAt=#{start_at}&expand=changelog&fields=*all"
|
|
45
|
+
|
|
46
|
+
json['issues'].each do |issue_json|
|
|
47
|
+
issue_json['exporter'] = {
|
|
48
|
+
'in_initial_query' => initial_query
|
|
49
|
+
}
|
|
50
|
+
identify_other_issues_to_be_downloaded raw_issue: issue_json, board: board
|
|
51
|
+
file = "#{issue_json['key']}-#{board.id}.json"
|
|
52
|
+
|
|
53
|
+
@file_system.save_json(json: issue_json, filename: File.join(path, file))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
total = json['total'].to_i
|
|
57
|
+
max_results = json['maxResults']
|
|
58
|
+
|
|
59
|
+
message = " Downloaded #{start_at + 1}-#{[start_at + max_results, total].min} of #{total} issues to #{path} "
|
|
60
|
+
log message, both: true
|
|
61
|
+
|
|
62
|
+
start_at += json['issues'].size
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def make_jql filter_id:, today: Date.today
|
|
67
|
+
segments = []
|
|
68
|
+
segments << "filter=#{filter_id}"
|
|
69
|
+
|
|
70
|
+
start_date = @download_config.start_date today: today
|
|
71
|
+
|
|
72
|
+
if start_date
|
|
73
|
+
@download_date_range = start_date..today.to_date
|
|
74
|
+
|
|
75
|
+
# For an incremental download, we want to query from the end of the previous one, not from the
|
|
76
|
+
# beginning of the full range.
|
|
77
|
+
@start_date_in_query = metadata['date_end'] || @download_date_range.begin
|
|
78
|
+
log " Incremental download only. Pulling from #{@start_date_in_query}", both: true if metadata['date_end']
|
|
79
|
+
|
|
80
|
+
# Catch-all to pick up anything that's been around since before the range started but hasn't
|
|
81
|
+
# had an update during the range.
|
|
82
|
+
catch_all = '((status changed OR Sprint is not EMPTY) AND statusCategory != Done)'
|
|
83
|
+
|
|
84
|
+
# Pick up any issues that had a status change in the range
|
|
85
|
+
start_date_text = @start_date_in_query.strftime '%Y-%m-%d'
|
|
86
|
+
# find_in_range = %((status changed DURING ("#{start_date_text} 00:00","#{end_date_text} 23:59")))
|
|
87
|
+
find_in_range = %(updated >= "#{start_date_text} 00:00")
|
|
88
|
+
|
|
89
|
+
segments << "(#{find_in_range} OR #{catch_all})"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
segments.join ' AND '
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -15,15 +15,6 @@ class Exporter
|
|
|
15
15
|
self.anonymize if anonymize
|
|
16
16
|
self.settings.merge! settings
|
|
17
17
|
|
|
18
|
-
status_category_mappings.each do |status, category|
|
|
19
|
-
status_category_mapping status: status, category: category
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
download do
|
|
23
|
-
self.rolling_date_count(rolling_date_count) if rolling_date_count
|
|
24
|
-
self.no_earlier_than(no_earlier_than) if no_earlier_than
|
|
25
|
-
end
|
|
26
|
-
|
|
27
18
|
boards.each_key do |board_id|
|
|
28
19
|
block = boards[board_id]
|
|
29
20
|
if block == :default
|
|
@@ -37,6 +28,15 @@ class Exporter
|
|
|
37
28
|
end
|
|
38
29
|
end
|
|
39
30
|
|
|
31
|
+
status_category_mappings.each do |status, category|
|
|
32
|
+
status_category_mapping status: status, category: category
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
download do
|
|
36
|
+
self.rolling_date_count(rolling_date_count) if rolling_date_count
|
|
37
|
+
self.no_earlier_than(no_earlier_than) if no_earlier_than
|
|
38
|
+
end
|
|
39
|
+
|
|
40
40
|
issues.reject! do |issue|
|
|
41
41
|
ignore_types.include? issue.type
|
|
42
42
|
end
|
|
@@ -48,7 +48,7 @@ class ExpeditedChart < ChartBase
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
if data_sets.empty?
|
|
51
|
-
'<h1>Expedited work</h1>There is no expedited work in this time period
|
|
51
|
+
'<h1 class="foldable">Expedited work</h1><p>There is no expedited work in this time period.</p>'
|
|
52
52
|
else
|
|
53
53
|
wrap_and_render(binding, __FILE__)
|
|
54
54
|
end
|
data/lib/jirametrics/exporter.rb
CHANGED
|
@@ -50,24 +50,27 @@ class Exporter
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
project.download_config.run
|
|
53
|
-
|
|
53
|
+
gateway = JiraGateway.new(
|
|
54
|
+
file_system: file_system, jira_config: project.jira_config, settings: project.settings
|
|
55
|
+
)
|
|
56
|
+
downloader = Downloader.create(
|
|
54
57
|
download_config: project.download_config,
|
|
55
58
|
file_system: file_system,
|
|
56
|
-
jira_gateway:
|
|
59
|
+
jira_gateway: gateway
|
|
57
60
|
)
|
|
58
61
|
downloader.run
|
|
59
62
|
end
|
|
60
63
|
puts "Full output from downloader in #{file_system.logfile_name}"
|
|
61
64
|
end
|
|
62
65
|
|
|
63
|
-
def info
|
|
66
|
+
def info key, name_filter:
|
|
64
67
|
selected = []
|
|
65
68
|
each_project_config(name_filter: name_filter) do |project|
|
|
66
69
|
project.evaluate_next_level
|
|
67
70
|
|
|
68
71
|
project.run load_only: true
|
|
69
72
|
project.issues.each do |issue|
|
|
70
|
-
selected << [project, issue] if
|
|
73
|
+
selected << [project, issue] if key == issue.key
|
|
71
74
|
end
|
|
72
75
|
rescue => e # rubocop:disable Style/RescueStandardError
|
|
73
76
|
# This happens when we're attempting to load an aggregated project because it hasn't been
|
|
@@ -76,7 +79,7 @@ class Exporter
|
|
|
76
79
|
end
|
|
77
80
|
|
|
78
81
|
if selected.empty?
|
|
79
|
-
file_system.log "No issues found to match #{
|
|
82
|
+
file_system.log "No issues found to match #{key.inspect}"
|
|
80
83
|
else
|
|
81
84
|
selected.each do |project, issue|
|
|
82
85
|
file_system.log "\nProject #{project.name}", also_write_to_stderr: true
|
|
@@ -85,6 +88,10 @@ class Exporter
|
|
|
85
88
|
end
|
|
86
89
|
end
|
|
87
90
|
|
|
91
|
+
def stitch stitch_file
|
|
92
|
+
Stitcher.new(file_system: file_system).run(stitch_file: stitch_file)
|
|
93
|
+
end
|
|
94
|
+
|
|
88
95
|
def each_project_config name_filter:
|
|
89
96
|
@project_configs.each do |project|
|
|
90
97
|
yield project if project.name.nil? || File.fnmatch(name_filter, project.name)
|
|
@@ -5,6 +5,13 @@ require 'json'
|
|
|
5
5
|
class FileSystem
|
|
6
6
|
attr_accessor :logfile, :logfile_name
|
|
7
7
|
|
|
8
|
+
def initialize
|
|
9
|
+
# In almost all cases, this will be immediately replaced in the Exporter
|
|
10
|
+
# but if we fail before we get that far, this will at least let a useful
|
|
11
|
+
# error show up on the console.
|
|
12
|
+
@logfile = $stdout
|
|
13
|
+
end
|
|
14
|
+
|
|
8
15
|
# Effectively the same as File.read except it forces the encoding to UTF-8
|
|
9
16
|
def load filename, supress_deprecation: false
|
|
10
17
|
if filename.end_with?('.json') && !supress_deprecation
|
|
@@ -31,6 +38,14 @@ class FileSystem
|
|
|
31
38
|
File.write(filename, content)
|
|
32
39
|
end
|
|
33
40
|
|
|
41
|
+
def mkdir path
|
|
42
|
+
FileUtils.mkdir_p path
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def utime file:, time:
|
|
46
|
+
File.utime time, time, file
|
|
47
|
+
end
|
|
48
|
+
|
|
34
49
|
def warning message, more: nil
|
|
35
50
|
log "Warning: #{message}", more: more, also_write_to_stderr: true
|
|
36
51
|
end
|
|
@@ -66,7 +81,15 @@ class FileSystem
|
|
|
66
81
|
end
|
|
67
82
|
|
|
68
83
|
def file_exist? filename
|
|
69
|
-
File.exist? filename
|
|
84
|
+
File.exist?(filename) && File.file?(filename)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def dir_exist? path
|
|
88
|
+
File.exist?(path) && File.directory?(path)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def unlink filename
|
|
92
|
+
File.unlink filename
|
|
70
93
|
end
|
|
71
94
|
|
|
72
95
|
def deprecated message:, date:, depth: 2
|
|
@@ -11,11 +11,24 @@ class FixVersion
|
|
|
11
11
|
@raw['name']
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
def description
|
|
15
|
+
@raw['description']
|
|
16
|
+
end
|
|
17
|
+
|
|
14
18
|
def id
|
|
15
19
|
@raw['id'].to_i
|
|
16
20
|
end
|
|
17
21
|
|
|
22
|
+
def release_date
|
|
23
|
+
text = @raw['releaseDate']
|
|
24
|
+
text.nil? ? nil : Date.parse(text)
|
|
25
|
+
end
|
|
26
|
+
|
|
18
27
|
def released?
|
|
19
28
|
@raw['released']
|
|
20
29
|
end
|
|
30
|
+
|
|
31
|
+
def archived?
|
|
32
|
+
@raw['archived']
|
|
33
|
+
end
|
|
21
34
|
end
|
|
@@ -60,7 +60,7 @@ class FlowEfficiencyScatterplot < ChartBase
|
|
|
60
60
|
create_dataset(issues: issues, label: rules.label, color: rules.color)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if data_sets.empty?
|
|
63
|
+
return "<h1 class='foldable'>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if data_sets.empty?
|
|
64
64
|
|
|
65
65
|
wrap_and_render(binding, __FILE__)
|
|
66
66
|
end
|
|
@@ -15,14 +15,20 @@ module GroupableIssueChart
|
|
|
15
15
|
|
|
16
16
|
def group_issues completed_issues
|
|
17
17
|
result = {}
|
|
18
|
+
ignored_issues = []
|
|
18
19
|
completed_issues.each do |issue|
|
|
19
20
|
rules = GroupingRules.new
|
|
20
21
|
@group_by_block.call(issue, rules)
|
|
21
|
-
|
|
22
|
+
if rules.ignored?
|
|
23
|
+
ignored_issues << issue
|
|
24
|
+
next
|
|
25
|
+
end
|
|
22
26
|
|
|
23
27
|
(result[rules] ||= []) << issue
|
|
24
28
|
end
|
|
25
29
|
|
|
30
|
+
completed_issues.reject! { |issue| ignored_issues.include? issue }
|
|
31
|
+
|
|
26
32
|
result.each_key do |rules|
|
|
27
33
|
rules.color = random_color if rules.color.nil?
|
|
28
34
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<%= seam_start %>
|
|
1
2
|
<div class="chart">
|
|
2
3
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
3
4
|
</div>
|
|
@@ -66,4 +67,4 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
66
67
|
}
|
|
67
68
|
});
|
|
68
69
|
</script>
|
|
69
|
-
|
|
70
|
+
<%= seam_end %>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<%= seam_start %>
|
|
1
2
|
<div class="chart">
|
|
2
3
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
3
4
|
</div>
|
|
@@ -40,7 +41,7 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
|
|
|
40
41
|
color: <%= CssVariable['--grid-line-color'].to_json %>,
|
|
41
42
|
z: 1 // draw the grid lines on top of the bars
|
|
42
43
|
},
|
|
43
|
-
stacked:
|
|
44
|
+
stacked: false,
|
|
44
45
|
max: <%= (@max_age * 1.1).to_i %>
|
|
45
46
|
}
|
|
46
47
|
},
|
|
@@ -73,3 +74,4 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
|
|
|
73
74
|
}
|
|
74
75
|
});
|
|
75
76
|
</script>
|
|
77
|
+
<%= seam_end %>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
<table class='standard' id='<%= issues_id %>'
|
|
1
|
+
<div class='foldable startFolded'>Show details</div>
|
|
2
|
+
<table class='standard' id='<%= issues_id %>'>
|
|
3
3
|
<thead>
|
|
4
4
|
<tr>
|
|
5
5
|
<th>Issue</th>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<%= seam_start %>
|
|
1
2
|
<div class="chart">
|
|
2
3
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
3
4
|
</div>
|
|
@@ -6,8 +7,8 @@ if show_stats
|
|
|
6
7
|
link_id = next_id
|
|
7
8
|
issues_id = next_id
|
|
8
9
|
%>
|
|
9
|
-
|
|
10
|
-
<div id="<%= issues_id %>" style="
|
|
10
|
+
<div class='foldable' style="padding-left: 1em;">Statistics</div>
|
|
11
|
+
<div id="<%= issues_id %>" style="padding-left: 1em;">
|
|
11
12
|
<div>
|
|
12
13
|
<table class="standard">
|
|
13
14
|
<tr>
|
|
@@ -119,3 +120,4 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
119
120
|
}
|
|
120
121
|
});
|
|
121
122
|
</script>
|
|
123
|
+
<%= seam_end %>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<%= seam_start %>
|
|
1
2
|
<div class="chart">
|
|
2
3
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
3
4
|
</div>
|
|
@@ -10,15 +11,14 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
10
11
|
options: {
|
|
11
12
|
title: {
|
|
12
13
|
display: true,
|
|
13
|
-
text: "
|
|
14
|
+
text: "<%= @header_text %>"
|
|
14
15
|
},
|
|
15
16
|
responsive: <%= canvas_responsive? %>, // If responsive is true then it fills the screen
|
|
16
17
|
scales: {
|
|
17
18
|
x: {
|
|
18
19
|
type: "time",
|
|
19
20
|
scaleLabel: {
|
|
20
|
-
display: true
|
|
21
|
-
labelString: 'Date Completed'
|
|
21
|
+
display: true
|
|
22
22
|
},
|
|
23
23
|
grid: {
|
|
24
24
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
@@ -29,13 +29,12 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
29
29
|
y: {
|
|
30
30
|
scaleLabel: {
|
|
31
31
|
display: true,
|
|
32
|
-
labelString: 'Days',
|
|
33
32
|
min: 0,
|
|
34
|
-
max: <%= @
|
|
33
|
+
max: <%= @highest_y_value %>
|
|
35
34
|
},
|
|
36
35
|
title: {
|
|
37
36
|
display: true,
|
|
38
|
-
text: '
|
|
37
|
+
text: '<%= y_axis_heading %>'
|
|
39
38
|
},
|
|
40
39
|
grid: {
|
|
41
40
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
@@ -98,3 +97,4 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
98
97
|
}
|
|
99
98
|
});
|
|
100
99
|
</script>
|
|
100
|
+
<%= seam_end %>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<%= seam_start %>
|
|
1
2
|
<div class="chart">
|
|
2
3
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
3
4
|
</div>
|
|
@@ -65,3 +66,4 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
65
66
|
}
|
|
66
67
|
});
|
|
67
68
|
</script>
|
|
69
|
+
<%= seam_end %>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<%= seam_start %>
|
|
1
2
|
<div class="chart">
|
|
2
3
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
3
4
|
</div>
|
|
@@ -60,3 +61,4 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
60
61
|
}
|
|
61
62
|
});
|
|
62
63
|
</script>
|
|
64
|
+
<%= seam_end %>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<%= seam_start %>
|
|
1
2
|
<div class="chart">
|
|
2
3
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
3
4
|
</div>
|
|
@@ -61,4 +62,5 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
});
|
|
64
|
-
</script>
|
|
65
|
+
</script>
|
|
66
|
+
<%= seam_end %>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<%= seam_start %>
|
|
1
2
|
<div class="chart">
|
|
2
3
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
3
4
|
</div>
|
|
@@ -83,3 +84,4 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
83
84
|
}
|
|
84
85
|
});
|
|
85
86
|
</script>
|
|
87
|
+
<%= seam_start %>
|
|
@@ -67,10 +67,21 @@
|
|
|
67
67
|
--sprint-burndown-sprint-color-4: red;
|
|
68
68
|
--sprint-burndown-sprint-color-5: brown;
|
|
69
69
|
|
|
70
|
+
--sprint-color: lightblue;
|
|
71
|
+
|
|
70
72
|
--daily-view-selected-issue-background: lightgray;
|
|
71
73
|
--daily-view-issue-border: green;
|
|
72
74
|
--daily-view-selected-issue-border: red;
|
|
73
75
|
|
|
76
|
+
/* The first five are the standard priorities that Jira creates by default. */
|
|
77
|
+
--priority-color-highest: #dc2626; /* red-600 - urgent red */
|
|
78
|
+
--priority-color-high: #ea580c; /* orange-600 - warning orange */
|
|
79
|
+
--priority-color-medium: #9ca3af; /* gray-400 - neutral light gray */
|
|
80
|
+
--priority-color-low: #0891b2; /* cyan-600 - calm blue */
|
|
81
|
+
--priority-color-lowest: #64748b; /* slate-500 - muted slate */
|
|
82
|
+
/* Then here are some values we've seen in multiple instances. */
|
|
83
|
+
--priority-color-notset: gray;
|
|
84
|
+
--priority-color-critical: red;
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
body {
|
|
@@ -78,11 +89,6 @@ body {
|
|
|
78
89
|
color: var(--default-text-color);
|
|
79
90
|
}
|
|
80
91
|
|
|
81
|
-
h1 {
|
|
82
|
-
border: 1px solid black;
|
|
83
|
-
background: lightgray;
|
|
84
|
-
padding-left: 0.2em;
|
|
85
|
-
}
|
|
86
92
|
dl, dd, dt {
|
|
87
93
|
padding: 0;
|
|
88
94
|
margin: 0;
|
|
@@ -242,11 +248,12 @@ div.child_issue {
|
|
|
242
248
|
--wip-chart-duration-more-than-four-weeks-color: #8e0000;
|
|
243
249
|
|
|
244
250
|
--daily-view-selected-issue-background: #474747;
|
|
245
|
-
}
|
|
246
251
|
|
|
247
|
-
|
|
248
|
-
color: #
|
|
249
|
-
|
|
252
|
+
--priority-color-highest: #ef4444; /* red-500 - bright urgent red */
|
|
253
|
+
--priority-color-high: #f97316; /* orange-500 - bright orange */
|
|
254
|
+
--priority-color-medium: #9ca3af; /* gray-400 - neutral light gray */
|
|
255
|
+
--priority-color-low: #06b6d4; /* cyan-500 - bright calm blue */
|
|
256
|
+
--priority-color-lowest: #94a3b8; /* slate-400 - muted light slate */
|
|
250
257
|
}
|
|
251
258
|
|
|
252
259
|
a[href] {
|
|
@@ -5,41 +5,9 @@
|
|
|
5
5
|
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.1/moment.js"></script>
|
|
6
6
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
7
7
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@^1"></script>
|
|
8
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-annotation/1.
|
|
8
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-annotation/3.1.0/chartjs-plugin-annotation.min.js"></script>
|
|
9
9
|
<script type="text/javascript">
|
|
10
|
-
|
|
11
|
-
link_text = document.getElementById(link_id).textContent
|
|
12
|
-
if( link_text == 'Show details') {
|
|
13
|
-
document.getElementById(link_id).textContent = 'Hide details'
|
|
14
|
-
document.getElementById(issues_id).style.display = 'block'
|
|
15
|
-
}
|
|
16
|
-
else {
|
|
17
|
-
document.getElementById(link_id).textContent = 'Show details'
|
|
18
|
-
document.getElementById(issues_id).style.display = 'none'
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function toggle_visibility(open_link_id, close_link_id, toggleable_id) {
|
|
23
|
-
let open_link = document.getElementById(open_link_id)
|
|
24
|
-
let close_link = document.getElementById(close_link_id)
|
|
25
|
-
let toggleable_element = document.getElementById(toggleable_id)
|
|
26
|
-
|
|
27
|
-
if(open_link.style.display == 'none') {
|
|
28
|
-
open_link.style.display = 'block'
|
|
29
|
-
close_link.style.display = 'none'
|
|
30
|
-
toggleable_element.style.display = 'none'
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
open_link.style.display = 'none'
|
|
34
|
-
close_link.style.display = 'block'
|
|
35
|
-
toggleable_element.style.display = 'block'
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
// If we switch between light/dark mode then force a refresh so all charts will redraw correctly
|
|
39
|
-
// in the other colour scheme.
|
|
40
|
-
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
|
|
41
|
-
location.reload()
|
|
42
|
-
})
|
|
10
|
+
<%= javascript %>
|
|
43
11
|
</script>
|
|
44
12
|
<style>
|
|
45
13
|
<%= css %>
|
|
@@ -55,6 +23,6 @@
|
|
|
55
23
|
</div>
|
|
56
24
|
</noscript>
|
|
57
25
|
<%= "\n" + @sections.collect { |text, type| text if type == :header }.compact.join("\n\n") %>
|
|
58
|
-
<%= "\n" + @sections.collect { |text, type| text if type
|
|
26
|
+
<%= "\n" + @sections.collect { |text, type| text if type != :header }.compact.join("\n\n") %>
|
|
59
27
|
</body>
|
|
60
28
|
</html>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
function makeFoldable() {
|
|
2
|
+
// Get all elements with the "foldable" class
|
|
3
|
+
const foldableElements = document.querySelectorAll('.foldable');
|
|
4
|
+
|
|
5
|
+
if (foldableElements.length === 0) {
|
|
6
|
+
return; // No foldable elements found
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Process each foldable element
|
|
10
|
+
foldableElements.forEach((element, index) => {
|
|
11
|
+
// Skip if this is the footer element
|
|
12
|
+
if (element.id === 'footer') {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Create a unique ID for this section
|
|
17
|
+
const sectionId = `foldable-section-${index}`;
|
|
18
|
+
const toggleId = `foldable-toggle-${index}`;
|
|
19
|
+
|
|
20
|
+
// Create a container div for the foldable element and its content
|
|
21
|
+
const container = document.createElement('div');
|
|
22
|
+
container.className = 'foldable-section';
|
|
23
|
+
container.id = sectionId;
|
|
24
|
+
|
|
25
|
+
// Create a toggle button
|
|
26
|
+
const toggleButton = document.createElement(element.tagName); //'button');
|
|
27
|
+
toggleButton.id = toggleId;
|
|
28
|
+
toggleButton.className = 'foldable-toggle-btn';
|
|
29
|
+
toggleButton.innerHTML = '▼ ' + element.textContent;
|
|
30
|
+
|
|
31
|
+
// Create a content container
|
|
32
|
+
const contentContainer = document.createElement('div');
|
|
33
|
+
contentContainer.className = 'foldable-content';
|
|
34
|
+
contentContainer.style.cssText = `
|
|
35
|
+
border-left: 2px solid #ccc;
|
|
36
|
+
padding-left: 15px;
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
// Move the foldable element into the container and replace it with the toggle button
|
|
40
|
+
element.parentNode.insertBefore(container, element);
|
|
41
|
+
container.appendChild(toggleButton);
|
|
42
|
+
container.appendChild(contentContainer);
|
|
43
|
+
|
|
44
|
+
// Move all elements between this foldable element and the next foldable element (or end of document) into the content container
|
|
45
|
+
let nextElement = element.nextElementSibling;
|
|
46
|
+
while (nextElement && !nextElement.classList.contains('foldable')) {
|
|
47
|
+
// Skip the footer element
|
|
48
|
+
if (nextElement.id === 'footer') {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const temp = nextElement.nextElementSibling;
|
|
53
|
+
contentContainer.appendChild(nextElement);
|
|
54
|
+
nextElement = temp;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Remove the original foldable element
|
|
58
|
+
element.remove();
|
|
59
|
+
|
|
60
|
+
// Add click event to toggle visibility
|
|
61
|
+
toggleButton.addEventListener('click', function() {
|
|
62
|
+
const content = this.nextElementSibling;
|
|
63
|
+
if (content.style.display === 'none') {
|
|
64
|
+
content.style.display = 'block';
|
|
65
|
+
this.innerHTML = '▼ ' + this.innerHTML.substring(2);
|
|
66
|
+
} else {
|
|
67
|
+
content.style.display = 'none';
|
|
68
|
+
this.innerHTML = '▶ ' + this.innerHTML.substring(2);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Initially show the content (you can change this to 'none' if you want sections collapsed by default)
|
|
73
|
+
contentContainer.style.display = 'block';
|
|
74
|
+
if(element.classList.contains('startFolded')) {
|
|
75
|
+
toggleButton.click();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Auto-initialize when DOM is loaded
|
|
81
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
82
|
+
makeFoldable();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
// If we switch between light/dark mode then force a refresh so all charts will redraw correctly
|
|
87
|
+
// in the other colour scheme.
|
|
88
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
|
|
89
|
+
location.reload()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// Draw a diagonal pattern to highlight sections of a bar chart. Based on code found at:
|
|
93
|
+
// https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns
|
|
94
|
+
function createDiagonalPattern(color = 'black') {
|
|
95
|
+
// create a 5x5 px canvas for the pattern's base shape
|
|
96
|
+
let shape = document.createElement('canvas')
|
|
97
|
+
shape.width = 5
|
|
98
|
+
shape.height = 5
|
|
99
|
+
// get the context for drawing
|
|
100
|
+
let c = shape.getContext('2d')
|
|
101
|
+
// draw 1st line of the shape
|
|
102
|
+
c.strokeStyle = color
|
|
103
|
+
c.beginPath()
|
|
104
|
+
c.moveTo(1, 0)
|
|
105
|
+
c.lineTo(5, 4)
|
|
106
|
+
c.stroke()
|
|
107
|
+
// draw 2nd line of the shape
|
|
108
|
+
c.beginPath()
|
|
109
|
+
c.moveTo(0, 4)
|
|
110
|
+
c.lineTo(1, 5)
|
|
111
|
+
c.stroke()
|
|
112
|
+
// create the pattern from the shape
|
|
113
|
+
return c.createPattern(shape, 'repeat')
|
|
114
|
+
}
|