jirametrics 2.12pre9 → 2.12pre12

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2e99113dff452fc39af119d35fe1b480ef991dba7ef3ef8c3335bf471fed9d8
4
- data.tar.gz: e05ad4ed94cb690856293244e23f28d1007fdb1b608820bcfee40aa082232722
3
+ metadata.gz: c113cc459acf750ccafa11aa97aaeaa799f3a23b4867df0e21503af9fdb18038
4
+ data.tar.gz: 4a204e05b0e5c61d011c8005d5a82011ce280afa45e76cef5cea9844bde71915
5
5
  SHA512:
6
- metadata.gz: 0ece3dea23ac0f9141342ae2be782dfcbf034b39db6fb6ec7cbe28d8afe85865e41906083e9fbf587ed2de4aba9f6efa586a26f6899faa00a890505f5602e125
7
- data.tar.gz: 7fac5a0b4fdbdda9808febf6b6f3043dcdf0965a3613145458cc3fb8c88cb3c0872e370fac12c0b0c50642172caebcb9c1b932252eb78de313c1fbbe14656de8
6
+ metadata.gz: 2a1d16a963187788e906a6010642a4361e968ab127b12421a31753cf68f6399d99c418152e9ead9d4bd2887b40a6333f2a049fcd4c164734319d275f78d8e5e2
7
+ data.tar.gz: a376ba351aac4f70383a6401df5b48c8637a6b68cfe26b35a3eca2016abb7b7c3cea813c2b246b59574efc2b44a7da97bc05e7402e9250fe47027bcfd263bf52
@@ -181,4 +181,8 @@ class AgingWorkTable < ChartBase
181
181
 
182
182
  result.reverse
183
183
  end
184
+
185
+ def priority_text issue
186
+ "<img src='#{issue.priority_url}' title='Priority: #{issue.priority_name}' />"
187
+ end
184
188
  end
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ChangeItem
4
- attr_reader :field, :value_id, :old_value_id, :raw, :author, :time
4
+ attr_reader :field, :value_id, :old_value_id, :raw, :time
5
5
  attr_accessor :value, :old_value
6
6
 
7
- def initialize raw:, time:, author:, artificial: false
7
+ def initialize raw:, author_raw:, time:, artificial: false
8
8
  @raw = raw
9
+ @author_raw = author_raw
9
10
  @time = time
10
11
  raise 'ChangeItem.new() time cannot be nil' if time.nil?
11
12
  raise "Time must be an object of type Time in the correct timezone: #{@time.inspect}" unless @time.is_a? Time
@@ -16,7 +17,14 @@ class ChangeItem
16
17
  @old_value = @raw['fromString']
17
18
  @old_value_id = @raw['from']&.to_i
18
19
  @artificial = artificial
19
- @author = author
20
+ end
21
+
22
+ def author
23
+ @author_raw&.[]('displayName') || @author_raw&.[]('name') || 'Unknown author'
24
+ end
25
+
26
+ def author_icon_url
27
+ @author_raw&.[]('avatarUrls')&.[]('16x16')
20
28
  end
21
29
 
22
30
  def status? = (field == 'status')
@@ -35,6 +43,8 @@ class ChangeItem
35
43
 
36
44
  def labels? = (field == 'labels')
37
45
 
46
+ def comment? = (field == 'comment')
47
+
38
48
  # An alias for time so that logic accepting a Time, Date, or ChangeItem can all respond to :to_time
39
49
  def to_time = @time
40
50
 
@@ -59,7 +59,7 @@ class CycleTimeConfig
59
59
  'from' => '0',
60
60
  'fromString' => ''
61
61
  }
62
- ChangeItem.new raw: raw, time: time, author: 'unknown', artificial: true
62
+ ChangeItem.new raw: raw, time: time, artificial: true, author_raw: nil
63
63
  end
64
64
 
65
65
  def started_stopped_changes issue
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DailyView < ChartBase
4
+ attr_accessor :possible_statuses
5
+
6
+ def initialize _block
7
+ super()
8
+
9
+ header_text 'Daily View'
10
+ description_text <<-HTML
11
+ <div class="p">
12
+ This view shows all the items you'll want to discuss during your daily coordination meeting
13
+ (aka daily scrum, standup), in the order that you should be discussing them. The most important
14
+ items are at the top, and the least at the bottom.
15
+ </div>
16
+ <div class="p">
17
+ By default, we sort by priority first and then by age within each of those priorities.
18
+ Hover over the issue to make it stand out more.
19
+ </div>
20
+ HTML
21
+ end
22
+
23
+ def run
24
+ aging_issues = select_aging_issues
25
+
26
+ return "<h1>#{@header_text}</h1>There are no items currently in progress" if aging_issues.empty?
27
+
28
+ result = +''
29
+ result << render_top_text(binding)
30
+ aging_issues.each do |issue|
31
+ result << render_issue(issue)
32
+ end
33
+ result
34
+ end
35
+
36
+ def select_aging_issues
37
+ aging_issues = issues.select do |issue|
38
+ started_at, stopped_at = issue.board.cycletime.started_stopped_times(issue)
39
+ started_at && !stopped_at
40
+ end
41
+
42
+ today = date_range.end
43
+ aging_issues.collect do |issue|
44
+ [issue, issue.priority_name, issue.board.cycletime.age(issue, today: today)]
45
+ end.sort(&issue_sorter).collect(&:first)
46
+ end
47
+
48
+ def issue_sorter
49
+ priority_names = settings['priority_order']
50
+ lambda do |a, b|
51
+ a_issue, a_priority, a_age = *a
52
+ b_issue, b_priority, b_age = *b
53
+
54
+ a_priority_index = priority_names.index(a_priority)
55
+ b_priority_index = priority_names.index(b_priority)
56
+
57
+ if a_priority_index.nil? && b_priority_index.nil?
58
+ result = a_priority <=> b_priority
59
+ elsif a_priority_index.nil?
60
+ result = 1
61
+ elsif b_priority_index.nil?
62
+ result = -1
63
+ else
64
+ result = b_priority_index <=> a_priority_index
65
+ end
66
+
67
+ result = b_age <=> a_age if result.zero?
68
+ result = a_issue <=> b_issue if result.zero?
69
+ result
70
+ end
71
+ end
72
+
73
+ def make_blocked_stalled_lines issue
74
+ today = date_range.end
75
+
76
+ blocked_stalled = issue.blocked_stalled_by_date(
77
+ date_range: today..today, chart_end_time: time_range.end, settings: settings
78
+ )[today]
79
+
80
+ lines = []
81
+ if blocked_stalled.blocked?
82
+ marker = color_block '--blocked-color'
83
+ lines << ["#{marker} Blocked by flag"] if blocked_stalled.flag
84
+ lines << ["#{marker} Blocked by status: #{blocked_stalled.status}"] if blocked_stalled.blocked_by_status?
85
+ blocked_stalled.blocking_issue_keys&.each do |key|
86
+ lines << ["#{marker} Blocked by issue: #{key}"]
87
+ blocking_issue = issues.find { |i| i.key == key }
88
+ lines << blocking_issue if blocking_issue
89
+ end
90
+ elsif blocked_stalled.stalled_by_status?
91
+ lines << ["#{color_block '--stalled-color'} Stalled by status: #{blocked_stalled.status}"]
92
+ else
93
+ lines << ["#{color_block '--stalled-color'} Stalled by inactivity: #{blocked_stalled.stalled_days} days"]
94
+ end
95
+ lines
96
+ end
97
+
98
+ def make_stats_lines issue
99
+ lines = []
100
+
101
+ title_line = +''
102
+ title_line << color_block('--expedited-color', title: 'Expedited') if issue.expedited?
103
+ title_line << "<img src='#{issue.type_icon_url}' title='#{issue.type}' class='icon' /> "
104
+ title_line << "<b><a href='#{issue.url}'>#{issue.key}</a></b> <i>#{issue.summary}</i>"
105
+ lines << [title_line]
106
+
107
+ chunks = []
108
+
109
+ chunks << "<img src='#{issue.priority_url}' class='icon' /> <b>#{issue.priority_name}</b>"
110
+
111
+ age = issue.board.cycletime.age(issue, today: date_range.end)
112
+ chunks << "Age: <b>#{label_days age}</b>" if age
113
+
114
+ chunks << "Status: <b>#{issue.status.name}</b>"
115
+
116
+ column = issue.board.visible_columns.find { |c| c.status_ids.include?(issue.status.id) }
117
+ chunks << "Column: <b>#{column&.name || '(not visible on board)'}</b>"
118
+
119
+ if issue.assigned_to
120
+ chunks << "Who: <img src='#{issue.assigned_to_icon_url}' class='icon' /> <b>#{issue.assigned_to}</b>"
121
+ end
122
+
123
+ chunks << "Due: <b>#{issue.due_date}</b>" if issue.due_date
124
+ lines << chunks
125
+
126
+ lines
127
+ end
128
+
129
+ def make_child_lines issue
130
+ lines = []
131
+ subtasks = issue.subtasks.reject { |i| i.done? }
132
+
133
+ unless subtasks.empty?
134
+ icon_urls = subtasks.collect(&:type_icon_url).uniq.collect { |url| "<img src='#{url}' class='icon' />" }
135
+ lines << (icon_urls << 'Incomplete child issues')
136
+ lines += subtasks
137
+ end
138
+ lines
139
+ end
140
+
141
+ def jira_rich_text_to_html text
142
+ text.gsub(/{color:(#\w{6})}([^{]+){color}/, '<span style="color: \1">\2</span>')
143
+ end
144
+
145
+ def make_comment_lines issue
146
+ comments = issue.changes.select { |i| i.comment? }.reverse
147
+ lines = []
148
+ return lines if comments.empty?
149
+
150
+ lines << ['Comments']
151
+ comments.each do |c|
152
+ text = jira_rich_text_to_html c.value
153
+ time = c.time.strftime '%b %d, %I:%M%P'
154
+
155
+ lines << [
156
+ "<div class='comment'>" \
157
+ "<img src='#{c.author_icon_url}' class='icon' title='#{c.author}' /> " \
158
+ "<span class='header' title='Timestamp: #{c.time}'>#{time}</span> " \
159
+ " &rarr; #{text}</div>"
160
+ ]
161
+ end
162
+ lines
163
+ end
164
+
165
+ def assemble_issue_lines issue
166
+ lines = []
167
+ lines += make_stats_lines(issue)
168
+ lines += make_blocked_stalled_lines(issue)
169
+ lines += make_child_lines(issue)
170
+ lines += make_comment_lines(issue)
171
+ lines
172
+ end
173
+
174
+ def render_issue issue, css_class: 'daily_issue'
175
+ result = +''
176
+ result << "<div class='#{css_class}'>"
177
+ assemble_issue_lines(issue).each do |row|
178
+ if row.is_a? Issue
179
+ result << render_issue(row, css_class: 'child_issue')
180
+ else
181
+ result << '<div class="heading">'
182
+ row.each do |chunk|
183
+ result << "<div>#{chunk}</div>"
184
+ end
185
+ result << '</div>'
186
+ end
187
+ end
188
+ result << '</div>'
189
+ end
190
+ end
@@ -58,6 +58,8 @@ class Exporter
58
58
  type: :header
59
59
  end
60
60
 
61
+ daily_view if show_experimental_charts
62
+
61
63
  cycletime_scatterplot do
62
64
  show_trend_lines
63
65
  end
@@ -52,8 +52,6 @@ class FileSystem
52
52
  # In some Jira instances, a sizeable portion of the JSON is made up of empty fields. I've seen
53
53
  # cases where this simple compression will drop the filesize by half.
54
54
  def compress node
55
- return node
56
-
57
55
  if node.is_a? Hash
58
56
  node.reject! { |_key, value| value.nil? || (value.is_a?(Array) && value.empty?) }
59
57
  node.each_value { |value| compress value }
@@ -4,6 +4,7 @@
4
4
  <th title="Age in days">Age</th>
5
5
  <th title="Expedited">E</th>
6
6
  <th title="Blocked / Stalled">B/S</th>
7
+ <th title="Priority">P</th>
7
8
  <th>Issue</th>
8
9
  <th>Status</th>
9
10
  <th>Forecast</th>
@@ -29,6 +30,7 @@
29
30
  <td style="text-align: right;"><%= issue_age || 'Not started' %></td>
30
31
  <td><%= expedited_text(issue) %></td>
31
32
  <td><%= blocked_text(issue) %></td>
33
+ <td><%= priority_text(issue) %></td>
32
34
  <td>
33
35
  <% parent_hierarchy(issue).each_with_index do |parent, index| %>
34
36
  <% color = parent != issue ? "var(--hierarchy-table-inactive-item-text-color)" : 'var(--default-text-color)' %>
@@ -67,6 +67,9 @@
67
67
  --sprint-burndown-sprint-color-4: red;
68
68
  --sprint-burndown-sprint-color-5: brown;
69
69
 
70
+ --daily-view-selected-issue-background: lightgray;
71
+ --daily-view-issue-border: green;
72
+ --daily-view-selected-issue-border: red;
70
73
 
71
74
  }
72
75
 
@@ -142,6 +145,44 @@ ul.quality_report {
142
145
  border-top: 1px solid gray;
143
146
  }
144
147
 
148
+ div.daily_issue:hover {
149
+ background: var(--daily-view-selected-issue-background);
150
+ border-color: var(--daily-view-selected-issue-border);
151
+ }
152
+
153
+ div.daily_issue {
154
+ border: 1px solid var(--daily-view-issue-border);
155
+ padding: 0.5em;
156
+ .heading {
157
+ vertical-align: middle;
158
+ display: flex;
159
+ gap: 0.5em;
160
+ align-items: center;
161
+ }
162
+ .comment {
163
+ margin-left: 1em;
164
+ padding-left: 2em;
165
+ text-indent: -2em;
166
+ .time {
167
+ font-size: 0.8em;
168
+ }
169
+ }
170
+ .icon {
171
+ width: 1em;
172
+ height: 1em;
173
+ }
174
+ margin-bottom: 0.5em;
175
+ }
176
+ div.child_issue:hover {
177
+ background: var(--body-background);
178
+ }
179
+ div.child_issue {
180
+ border: 1px dashed green;
181
+ margin: 0.2em;
182
+ margin-left: 1.5em;
183
+ padding: 0.5em;
184
+ }
185
+
145
186
  @media screen and (prefers-color-scheme: dark) {
146
187
  :root {
147
188
  --warning-banner: #9F2B00;
@@ -174,6 +215,8 @@ ul.quality_report {
174
215
  --wip-chart-duration-two-weeks-or-less-color: #cf9400;
175
216
  --wip-chart-duration-four-weeks-or-less-color: #c25e00;
176
217
  --wip-chart-duration-more-than-four-weeks-color: #8e0000;
218
+
219
+ --daily-view-selected-issue-background: #474747;
177
220
  }
178
221
 
179
222
  h1 {
@@ -33,6 +33,7 @@ class HtmlReportConfig
33
33
  define_chart name: 'estimate_accuracy_chart', classname: 'EstimateAccuracyChart'
34
34
  define_chart name: 'hierarchy_table', classname: 'HierarchyTable'
35
35
  define_chart name: 'flow_efficiency_scatterplot', classname: 'FlowEfficiencyScatterplot'
36
+ define_chart name: 'daily_view', classname: 'DailyView'
36
37
 
37
38
  define_chart name: 'daily_wip_by_type', classname: 'DailyWipChart',
38
39
  deprecated_warning: 'This is the same as daily_wip_chart. Please use that one', deprecated_date: '2024-05-23'
@@ -44,9 +44,11 @@ class Issue
44
44
  def key = @raw['key']
45
45
 
46
46
  def type = @raw['fields']['issuetype']['name']
47
-
48
47
  def type_icon_url = @raw['fields']['issuetype']['iconUrl']
49
48
 
49
+ def priority_name = @raw['fields']['priority']['name']
50
+ def priority_url = @raw['fields']['priority']['iconUrl']
51
+
50
52
  def summary = @raw['fields']['summary']
51
53
 
52
54
  def labels = @raw['fields']['labels'] || []
@@ -205,6 +207,10 @@ class Issue
205
207
  nil
206
208
  end
207
209
 
210
+ def first_time_visible_on_board
211
+ first_time_in_status(*board.visible_columns.collect(&:status_ids).flatten)
212
+ end
213
+
208
214
  def parse_time text
209
215
  Time.parse(text).getlocal(@timezone_offset)
210
216
  end
@@ -230,6 +236,10 @@ class Issue
230
236
  @raw['fields']&.[]('assignee')&.[]('displayName')
231
237
  end
232
238
 
239
+ def assigned_to_icon_url
240
+ @raw['fields']&.[]('assignee')&.[]('avatarUrls')&.[]('16x16')
241
+ end
242
+
233
243
  # Many test failures are simply unreadable because the default inspect on this class goes
234
244
  # on for pages. Shorten it up.
235
245
  def inspect
@@ -315,7 +325,7 @@ class Issue
315
325
 
316
326
  # This mock change is to force the writing of one last entry at the end of the time range.
317
327
  # By doing this, we're able to eliminate a lot of duplicated code in charts.
318
- mock_change = ChangeItem.new time: end_time, author: '', artificial: true, raw: { 'field' => '' }
328
+ mock_change = ChangeItem.new time: end_time, artificial: true, raw: { 'field' => '' }, author_raw: nil
319
329
 
320
330
  (changes + [mock_change]).each do |change|
321
331
  previous_was_active = false if check_for_stalled(
@@ -580,7 +590,7 @@ class Issue
580
590
  /(?<project_code1>[^-]+)-(?<id1>.+)/ =~ key
581
591
  /(?<project_code2>[^-]+)-(?<id2>.+)/ =~ other.key
582
592
  comparison = project_code1 <=> project_code2
583
- comparison = id1 <=> id2 if comparison.zero?
593
+ comparison = id1.to_i <=> id2.to_i if comparison.zero?
584
594
  comparison
585
595
  end
586
596
 
@@ -687,32 +697,25 @@ class Issue
687
697
 
688
698
  private
689
699
 
690
- def assemble_author raw
691
- raw['author']&.[]('displayName') || raw['author']&.[]('name') || 'Unknown author'
692
- end
693
-
694
700
  def load_history_into_changes
695
701
  @raw['changelog']['histories']&.each do |history|
696
702
  created = parse_time(history['created'])
697
703
 
698
- # It should be impossible to not have an author but we've seen it in production
699
- author = assemble_author history
700
704
  history['items']&.each do |item|
701
- @changes << ChangeItem.new(raw: item, time: created, author: author)
705
+ @changes << ChangeItem.new(raw: item, time: created, author_raw: history['author'])
702
706
  end
703
707
  end
704
708
  end
705
709
 
706
710
  def load_comments_into_changes
707
711
  @raw['fields']['comment']['comments']&.each do |comment|
708
- raw = {
712
+ raw = comment.merge({
709
713
  'field' => 'comment',
710
714
  'to' => comment['id'],
711
715
  'toString' => comment['body']
712
- }
713
- author = assemble_author comment
716
+ })
714
717
  created = parse_time(comment['created'])
715
- @changes << ChangeItem.new(raw: raw, time: created, author: author, artificial: true)
718
+ @changes << ChangeItem.new(raw: raw, time: created, artificial: true, author_raw: comment['author'])
716
719
  end
717
720
  end
718
721
 
@@ -754,7 +757,9 @@ class Issue
754
757
  first_status = first_change.old_value
755
758
  first_status_id = first_change.old_value_id
756
759
  end
757
- ChangeItem.new time: created_time, artificial: true, author: author, raw: {
760
+
761
+ creator = raw['fields']['creator']
762
+ ChangeItem.new time: created_time, artificial: true, author_raw: creator, raw: {
758
763
  'field' => field_name,
759
764
  'to' => first_status_id,
760
765
  'toString' => first_status
@@ -26,7 +26,10 @@ class JiraGateway
26
26
  end
27
27
 
28
28
  def call_command command
29
- @file_system.log " #{command.gsub(/\s+/, ' ')}"
29
+ log_entry = " #{command.gsub(/\s+/, ' ')}"
30
+ log_entry = log_entry.gsub(@jira_api_token, '[API_TOKEN]') if @jira_api_token
31
+ @file_system.log log_entry
32
+
30
33
  result = `#{command}`
31
34
  @file_system.log result unless $CHILD_STATUS.success?
32
35
  return result if $CHILD_STATUS.success?
@@ -6,5 +6,6 @@
6
6
  "blocked_statuses": [],
7
7
  "flagged_means_blocked": true,
8
8
 
9
- "expedited_priority_names": ["Critical", "Highest"]
9
+ "expedited_priority_names": ["Critical", "Highest"],
10
+ "priority_order": ["Lowest", "Low", "Medium", "High", "Highest"]
10
11
  }
data/lib/jirametrics.rb CHANGED
@@ -114,6 +114,7 @@ class JiraMetrics < Thor
114
114
  require 'jirametrics/hierarchy_table'
115
115
  require 'jirametrics/estimation_configuration'
116
116
  require 'jirametrics/board'
117
+ require 'jirametrics/daily_view'
117
118
  load config_file
118
119
  end
119
120
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jirametrics
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.12pre9
4
+ version: 2.12pre12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-16 00:00:00.000000000 Z
10
+ date: 2025-06-26 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: random-word
@@ -77,6 +77,7 @@ files:
77
77
  - lib/jirametrics/cycletime_config.rb
78
78
  - lib/jirametrics/cycletime_histogram.rb
79
79
  - lib/jirametrics/cycletime_scatterplot.rb
80
+ - lib/jirametrics/daily_view.rb
80
81
  - lib/jirametrics/daily_wip_by_age_chart.rb
81
82
  - lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb
82
83
  - lib/jirametrics/daily_wip_by_parent_chart.rb