sidekiq 8.1.1 → 8.1.6

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +49 -2
  3. data/README.md +1 -1
  4. data/bin/kiq +17 -0
  5. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +8 -8
  6. data/lib/sidekiq/api.rb +110 -35
  7. data/lib/sidekiq/capsule.rb +0 -1
  8. data/lib/sidekiq/cli.rb +2 -3
  9. data/lib/sidekiq/client.rb +8 -1
  10. data/lib/sidekiq/component.rb +3 -0
  11. data/lib/sidekiq/config.rb +4 -8
  12. data/lib/sidekiq/job.rb +14 -1
  13. data/lib/sidekiq/job_retry.rb +1 -1
  14. data/lib/sidekiq/launcher.rb +1 -1
  15. data/lib/sidekiq/manager.rb +2 -1
  16. data/lib/sidekiq/paginator.rb +8 -2
  17. data/lib/sidekiq/profiler.rb +1 -1
  18. data/lib/sidekiq/redis_client_adapter.rb +6 -2
  19. data/lib/sidekiq/scheduled.rb +2 -5
  20. data/lib/sidekiq/testing.rb +1 -0
  21. data/lib/sidekiq/tui/controls.rb +53 -0
  22. data/lib/sidekiq/tui/filtering.rb +53 -0
  23. data/lib/sidekiq/tui/tabs/base_tab.rb +204 -0
  24. data/lib/sidekiq/tui/tabs/busy.rb +118 -0
  25. data/lib/sidekiq/tui/tabs/dead.rb +19 -0
  26. data/lib/sidekiq/tui/tabs/home.rb +144 -0
  27. data/lib/sidekiq/tui/tabs/metrics.rb +131 -0
  28. data/lib/sidekiq/tui/tabs/queues.rb +95 -0
  29. data/lib/sidekiq/tui/tabs/retries.rb +19 -0
  30. data/lib/sidekiq/tui/tabs/scheduled.rb +19 -0
  31. data/lib/sidekiq/tui/tabs/set_tab.rb +96 -0
  32. data/lib/sidekiq/tui/tabs.rb +15 -0
  33. data/lib/sidekiq/tui.rb +276 -913
  34. data/lib/sidekiq/version.rb +1 -1
  35. data/lib/sidekiq/web/action.rb +2 -2
  36. data/lib/sidekiq/web/application.rb +14 -1
  37. data/lib/sidekiq/web/helpers.rb +34 -9
  38. data/lib/sidekiq/web/router.rb +2 -2
  39. data/lib/sidekiq.rb +1 -1
  40. data/sidekiq.gemspec +2 -2
  41. data/web/assets/stylesheets/style.css +2 -0
  42. data/web/locales/ar.yml +1 -1
  43. data/web/locales/fa.yml +1 -1
  44. data/web/locales/gd.yml +12 -2
  45. data/web/locales/he.yml +1 -1
  46. data/web/locales/pt-BR.yml +1 -1
  47. data/web/locales/ur.yml +1 -1
  48. data/web/locales/zh-TW.yml +1 -1
  49. data/web/views/_job_info.html.erb +8 -0
  50. data/web/views/_paging.html.erb +1 -1
  51. data/web/views/busy.html.erb +49 -40
  52. data/web/views/retries.html.erb +3 -0
  53. metadata +17 -4
  54. data/bin/tui +0 -5
data/lib/sidekiq/tui.rb CHANGED
@@ -1,458 +1,253 @@
1
- require "bundler/inline"
2
-
3
- gemfile do
4
- source "https://gem.coop"
5
- gem "ratatui_ruby", "1.3.0"
6
- gem "sidekiq"
7
- end
8
-
9
- RatatuiRuby.debug_mode!
10
-
11
1
  # https://sr.ht/~kerrick/ratatui_ruby/
12
2
  # https://git.sr.ht/~kerrick/ratatui_ruby/tree/stable/item/examples/
3
+ gem "ratatui_ruby", ">=1.4.0"
13
4
  require "ratatui_ruby"
5
+
6
+ RatatuiRuby.debug_mode! if !!ENV["DEBUG"]
7
+
14
8
  require "sidekiq/api"
15
9
  require "sidekiq/paginator"
16
10
 
17
- # Suppress Sidekiq logger output to prevent interference with TUI rendering
18
- require "logger"
19
- Sidekiq.default_configuration.logger = Logger.new(IO::NULL)
20
-
21
- DebugLogger = Logger.new("tui.log")
22
- def log(*x)
23
- x.each { |item| DebugLogger.info { item } }
24
- end
11
+ require_relative "tui/filtering"
12
+ require_relative "tui/controls"
13
+ require_relative "tui/tabs"
25
14
 
26
15
  module Sidekiq
27
16
  class TUI
28
- include Sidekiq::Paginator
17
+ include Sidekiq::Component
29
18
 
30
19
  PageOptions = Data.define(:page, :size)
31
20
 
32
21
  REFRESH_INTERVAL_SECONDS = 2
22
+ LOCALE_DIRECTORIES = [File.expand_path("#{File.dirname(__FILE__)}/../../web/locales")]
23
+
24
+ attr_reader :lang
33
25
 
34
- TABS = %w[Home Busy Queues Scheduled Retries Dead Metrics].freeze
35
- # CONTROLS defines data for input handling and for displaying controls.
36
- # :code is the key code for input handling.
37
- # :display and :description are shown in the controls area, with different
38
- # styling between them. If :display is omitted, :code is displayed instead.
39
- # Duplicate :display and :description values are ignored, shown only once.
40
- # :tabs is an array of tab names where the control is active.
41
- # :action is a lambda to execute when the control is triggered.
42
- #
43
- # Conventions: dangerous/irreversible actions should use UPPERCASE codes.
44
- # The Shift button means "I'm sure".
45
- CONTROLS = [
46
- {code: "?", display: "?", description: "Help", tabs: TABS,
47
- action: ->(tui) { tui.show_help }},
48
- {code: "left", display: "←/→", description: "Select Tab", tabs: TABS,
49
- action: ->(tui) { tui.navigate_tab(:left) }, refresh: true},
50
- {code: "right", display: "←/→", description: "Select Tab", tabs: TABS,
51
- action: ->(tui) { tui.navigate_tab(:right) }, refresh: true},
52
- {code: "q", display: "q", description: "Quit", tabs: TABS,
53
- action: ->(tui) { :quit }},
54
- {code: "c", modifiers: ["ctrl"], display: "q", description: "Quit", tabs: TABS,
55
- action: ->(tui) { :quit }},
56
- {code: "h", display: "h/l", description: "Prev/Next Page", tabs: TABS - ["Home"],
57
- action: ->(tui) { tui.prev_page }, refresh: true},
58
- {code: "l", display: "h/l", description: "Prev/Next Page", tabs: TABS - ["Home"],
59
- action: ->(tui) { tui.next_page }, refresh: true},
60
- {code: "k", display: "j/k", description: "Prev/Next Row", tabs: TABS - ["Home"],
61
- action: ->(tui) { tui.navigate_row(:up) }},
62
- {code: "j", display: "j/k", description: "Prev/Next Row", tabs: TABS - ["Home"],
63
- action: ->(tui) { tui.navigate_row(:down) }},
64
- {code: "x", display: "x", description: "Select", tabs: TABS - ["Home"],
65
- action: ->(tui) { tui.toggle_select }},
66
- {code: "A", modifiers: ["shift"], display: "A", description: "Select All", tabs: TABS - ["Home"],
67
- action: ->(tui) { tui.toggle_select(:all) }},
68
- {code: "D", modifiers: ["shift"], display: "D", description: "Delete", tabs: %w[Scheduled Retries Dead],
69
- action: ->(tui) { tui.alter_rows!(:delete) }, refresh: true},
70
- {code: "R", modifiers: ["shift"], display: "R", description: "Retry", tabs: %w[Retries],
71
- action: ->(tui) { tui.alter_rows!(:retry) }, refresh: true},
72
- {code: "E", modifiers: ["shift"], display: "E", description: "Enqueue", tabs: %w[Scheduled Dead],
73
- action: ->(tui) { tui.alter_rows!(:add_to_queue) }, refresh: true},
74
- {code: "K", modifiers: ["shift"], display: "K", description: "Kill", tabs: %w[Scheduled Retries],
75
- action: ->(tui) { tui.alter_rows!(:kill) }, refresh: true},
76
- {code: "D", modifiers: ["shift"], display: "D", description: "Delete", tabs: %w[Queues],
77
- action: ->(tui) { tui.delete_queue! }, refresh: true},
78
- {code: "p", description: "Pause/Unpause Queue", tabs: ["Queues"],
79
- action: ->(tui) { tui.toggle_pause_queue! }},
80
- {code: "T", modifiers: ["shift"], description: "Terminate", tabs: ["Busy"],
81
- action: ->(tui) { tui.terminate! }},
82
- {code: "Q", modifiers: ["shift"], description: "Quiet", tabs: ["Busy"],
83
- action: ->(tui) { tui.quiet! }},
84
- {code: "/", display: "/", description: "Filter", tabs: %w[Scheduled Retries Dead],
85
- action: ->(tui) { tui.start_filtering }}
86
- ].freeze
87
-
88
- def initialize
89
- @current_tab = "Home"
90
- @selected_row_index = 0
26
+ # language is meant to be a locale code, e.g.
27
+ # LANG=en_US.utf-8
28
+ def initialize(cfg, language: ENV["LANG"] || "en")
29
+ @lang = language
30
+ @config = cfg
91
31
  @base_style = nil
92
- @data = {}
93
- @last_refresh = Time.now
32
+ @last_refresh = Time.at(0)
33
+ @fps = Array.new(2) { 0 }
34
+ @previous_fps = 0
94
35
  @showing = :main
95
36
  end
96
37
 
97
- def run
98
- RatatuiRuby.run do |tui|
99
- @tui = tui
100
- @highlight_style = @tui.style(fg: :red, modifiers: [:underlined])
101
- @hotkey_style = @tui.style(modifiers: [:bold, :underlined])
38
+ def prepare(tui)
39
+ load_locale
102
40
 
103
- refresh_data
41
+ @tui = tui
42
+ @highlight_style = @tui.style(fg: :light_red, modifiers: [:underlined])
43
+ @hotkey_style = @tui.style(modifiers: [:bold, :underlined])
44
+ # eager load tabs
45
+ all
46
+ end
104
47
 
105
- loop do
106
- refresh_data if should_refresh?
107
- render
108
- break if handle_input == :quit
109
- end
48
+ def run_loop
49
+ # Must log to a file, terminal is now controlled by Ratatui
50
+ config.logger = Logger.new("tui.log")
51
+
52
+ loop do
53
+ refresh_data if should_refresh?
54
+ render
55
+ break if handle_input == :quit
110
56
  end
111
57
  end
112
58
 
113
59
  def render
114
- if @showing == :main
115
- @tui.draw do |frame|
116
- main_area, controls_area = @tui.layout_split(
117
- frame.area,
118
- direction: :vertical,
119
- constraints: [
120
- @tui.constraint_fill(1),
121
- @tui.constraint_length(4)
122
- ]
123
- )
124
-
125
- # Split main area into tabs and content
126
- tabs_area, content_area = @tui.layout_split(
127
- main_area,
128
- direction: :vertical,
129
- constraints: [
130
- @tui.constraint_length(3),
131
- @tui.constraint_fill(1)
132
- ]
133
- )
134
-
135
- tabs = @tui.tabs(
136
- titles: TABS,
137
- selected_index: TABS.index(@current_tab),
138
- block: @tui.block(title: Sidekiq::NAME, borders: [:all], title_style: @tui.style(fg: :red, modifiers: [:bold])),
139
- divider: " | ",
140
- highlight_style: @highlight_style,
141
- style: @base_style
142
- )
143
- frame.render_widget(tabs, tabs_area)
144
-
145
- render_content_area(frame, content_area)
146
- render_controls(frame, controls_area)
60
+ track_fps do
61
+ if @showing == :main
62
+ @tui.draw do |frame|
63
+ main_area, controls_area = @tui.layout_split(
64
+ frame.area,
65
+ direction: :vertical,
66
+ constraints: [
67
+ @tui.constraint_fill(1),
68
+ @tui.constraint_length(5)
69
+ ]
70
+ )
71
+
72
+ # Split main area into tabs and content
73
+ tabs_area, content_area = @tui.layout_split(
74
+ main_area,
75
+ direction: :vertical,
76
+ constraints: [
77
+ @tui.constraint_length(3),
78
+ @tui.constraint_fill(1)
79
+ ]
80
+ )
81
+
82
+ all_tabs = all
83
+ tabs = @tui.tabs(
84
+ titles: all_tabs.map { |tab| t(tab.name) },
85
+ selected_index: all_tabs.index(current_tab),
86
+ block: @tui.block(title: " #{Sidekiq::NAME}", borders: [:all], title_style: @tui.style(fg: :light_red, modifiers: [:bold])),
87
+ divider: " | ",
88
+ highlight_style: @highlight_style,
89
+ style: @base_style
90
+ )
91
+ frame.render_widget(tabs, tabs_area)
92
+
93
+ render_content_area(frame, content_area)
94
+ render_controls(frame, controls_area)
95
+ end
147
96
  end
148
- end
149
97
 
150
- if @showing == :help
151
- @tui.draw do |frame|
152
- main_area, controls_area = @tui.layout_split(
153
- frame.area,
154
- direction: :vertical,
155
- constraints: [
156
- @tui.constraint_fill(1),
157
- @tui.constraint_length(4)
158
- ]
159
- )
160
- content = @tui.block(
161
- title: Sidekiq::NAME,
162
- borders: [:all],
163
- title_style: @tui.style(fg: :red, modifiers: [:bold]),
164
- children: [
165
- # TODO convert to table
166
- @tui.paragraph(
167
- text: [
168
- @tui.text_line(spans: ["Welcome to the Sidekiq Terminal UI"], alignment: :center),
169
- @tui.text_line(spans: [
170
- @tui.text_span(content: "Esc", style: @hotkey_style),
171
- @tui.text_span(content: ": Close")
172
- ]),
173
- @tui.text_line(spans: [
174
- @tui.text_span(content: "←/→", style: @hotkey_style),
175
- @tui.text_span(content: ": Move between tabs")
176
- ]),
177
- @tui.text_line(spans: [
178
- @tui.text_span(content: "j/k", style: @hotkey_style),
179
- @tui.text_span(content: ": Use vim keys to move to prev/next row")
180
- ]),
181
- @tui.text_line(spans: [
182
- @tui.text_span(content: "x", style: @hotkey_style),
183
- @tui.text_span(content: ": Select/deselect current row")
184
- ]),
185
- @tui.text_line(spans: [
186
- @tui.text_span(content: "A", style: @hotkey_style),
187
- @tui.text_span(content: ": Select/deselect All visible rows")
188
- ]),
189
- @tui.text_line(spans: [
190
- @tui.text_span(content: "h/l", style: @hotkey_style),
191
- @tui.text_span(content: ": Use vim keys to move to prev/next page")
192
- ]),
193
- @tui.text_line(spans: [
194
- @tui.text_span(content: "q", style: @hotkey_style),
195
- @tui.text_span(content: ": Quit")
196
- ])
197
- ]
198
- )
199
- ]
200
- )
201
- frame.render_widget(content, main_area)
202
- controls = @tui.block(
203
- title: "Controls",
204
- borders: [:all],
205
- children: [
206
- @tui.paragraph(
207
- text: [
208
- @tui.text_line(spans: [
209
- @tui.text_span(content: "Esc", style: @hotkey_style),
210
- @tui.text_span(content: ": Close ")
211
- ])
212
- ]
213
- )
214
- ]
215
- )
216
- frame.render_widget(controls, controls_area)
98
+ if @showing == :help
99
+ @tui.draw do |frame|
100
+ main_area, controls_area = @tui.layout_split(
101
+ frame.area,
102
+ direction: :vertical,
103
+ constraints: [
104
+ @tui.constraint_fill(1),
105
+ @tui.constraint_length(4)
106
+ ]
107
+ )
108
+ content = @tui.block(
109
+ title: " #{Sidekiq::NAME} ",
110
+ borders: [:all],
111
+ title_style: @tui.style(fg: :light_red, modifiers: [:bold]),
112
+ children: [
113
+ # TODO convert to table
114
+ @tui.paragraph(
115
+ text: [
116
+ @tui.text_line(spans: ["Welcome to the Sidekiq Terminal UI"], alignment: :center),
117
+ @tui.text_line(spans: [
118
+ @tui.text_span(content: "Global hotkeys")
119
+ ]),
120
+ @tui.text_line(spans: []),
121
+ @tui.text_line(spans: [
122
+ @tui.text_span(content: "Esc", style: @hotkey_style),
123
+ @tui.text_span(content: ": Close this window")
124
+ ]),
125
+ @tui.text_line(spans: [
126
+ @tui.text_span(content: "←/→", style: @hotkey_style),
127
+ @tui.text_span(content: ": Move between tabs")
128
+ ]),
129
+ @tui.text_line(spans: [
130
+ @tui.text_span(content: "h/l", style: @hotkey_style),
131
+ @tui.text_span(content: ": Move to prev/next page of data")
132
+ ]),
133
+ @tui.text_line(spans: [
134
+ @tui.text_span(content: "j/k", style: @hotkey_style),
135
+ @tui.text_span(content: ": Move to prev/next row in current page")
136
+ ]),
137
+ @tui.text_line(spans: [
138
+ @tui.text_span(content: "x", style: @hotkey_style),
139
+ @tui.text_span(content: ": Select/deselect current row")
140
+ ]),
141
+ @tui.text_line(spans: [
142
+ @tui.text_span(content: "A", style: @hotkey_style),
143
+ @tui.text_span(content: ": Select/deselect All rows in current page")
144
+ ]),
145
+ @tui.text_line(spans: [
146
+ @tui.text_span(content: "q", style: @hotkey_style),
147
+ @tui.text_span(content: ": Quit")
148
+ ])
149
+ ]
150
+ )
151
+ ]
152
+ )
153
+ frame.render_widget(content, main_area)
154
+ controls = @tui.block(
155
+ title: t("Controls"),
156
+ borders: [:all],
157
+ children: [
158
+ @tui.paragraph(
159
+ text: [
160
+ @tui.text_line(spans: [
161
+ @tui.text_span(content: "Esc", style: @hotkey_style),
162
+ @tui.text_span(content: ": Close ")
163
+ ])
164
+ ]
165
+ )
166
+ ]
167
+ )
168
+ frame.render_widget(controls, controls_area)
169
+ end
217
170
  end
218
171
  end
219
172
  end
220
173
 
221
174
  def render_content_area(frame, content_area)
222
- return render_error(frame, content_area, @data[:error]) if @data[:error]
223
-
224
- case @current_tab
225
- when "Home"
226
- render_home(frame, content_area)
227
- when "Busy"
228
- render_busy(frame, content_area)
229
- when "Queues"
230
- render_queues(frame, content_area)
231
- when "Scheduled", "Retries", "Dead"
232
- render_set(frame, content_area)
233
- when "Metrics"
234
- render_metrics(frame, content_area)
235
- else
236
- frame.render_widget(
237
- @tui.paragraph(
238
- text: "Tab '#{@current_tab}' - Coming soon",
239
- alignment: :center,
240
- block: @tui.block(title: @current_tab, borders: [:all])
241
- ),
242
- content_area
243
- )
244
- end
175
+ return render_error(frame, content_area, current_tab.error) if current_tab.error
176
+
177
+ current_tab.render(@tui, frame, content_area)
245
178
  end
246
179
 
247
180
  def render_controls(frame, area)
248
- keys_and_descriptions = CONTROLS
249
- .select { |ctrl|
250
- ctrl[:tabs].include?(@current_tab)
251
- }.map { |ctrl|
252
- [ctrl[:display] || ctrl[:code], ctrl[:description]]
253
- }.to_h
254
-
255
- controls = @tui.block(
256
- title: "Controls",
257
- borders: [:all],
258
- children: [
259
- @tui.paragraph(
260
- text: [
261
- @tui.text_line(spans: keys_and_descriptions.map { |key, desc|
262
- [
263
- @tui.text_span(content: key, style: @hotkey_style),
264
- @tui.text_span(content: ": #{desc} ")
265
- ]
266
- }.flatten),
267
- # @tui.text_line(spans: [
268
- # @tui.text_span(content: "d", style: @hotkey_style),
269
- # @tui.text_span(content: ": Divider (#{@dividers[@divider_index]}) "),
270
- # @tui.text_span(content: "s", style: @hotkey_style),
271
- # @tui.text_span(content: ": Highlight (#{@highlight_styles[@highlight_style_index][:name]}) "),
272
- # @tui.text_span(content: "b", style: @hotkey_style),
273
- # @tui.text_span(content: ": Base Style (#{@base_styles[@base_style_index][:name]}) "),
274
- # ]),
275
- @tui.text_line(spans: [
276
- @tui.text_span(content: "Redis: #{redis_url} "),
277
- @tui.text_span(content: "Current Time: #{Time.now.utc}")
278
- ])
279
- ]
280
- )
181
+ active_keys = current_tab.controls.filter { |hash| hash[:description] }
182
+
183
+ # Split controls into two lines, 8 is arbitrary
184
+ # TODO Dynamically split based on term width?
185
+ first = active_keys[...8]
186
+ lines = []
187
+ lines << @tui.text_line(spans: first.map { |hash|
188
+ [
189
+ @tui.text_span(content: hash[:display] || hash[:code], style: @hotkey_style),
190
+ @tui.text_span(content: ": #{t(hash[:description])} ")
281
191
  ]
282
- )
192
+ }.flatten)
193
+
194
+ last = active_keys[8...]
195
+ lines << if last && last.size > 0
196
+ @tui.text_line(spans: last.map { |hash|
197
+ [
198
+ @tui.text_span(content: hash[:display] || hash[:code], style: @hotkey_style),
199
+ @tui.text_span(content: ": #{t(hash[:description])} ")
200
+ ]
201
+ }.flatten)
202
+ else
203
+ @tui.text_line(spans: [])
204
+ end
205
+
206
+ footer = [
207
+ @tui.text_span(content: "Redis: #{redis_url} "),
208
+ @tui.text_span(content: "#{t("Now")}: #{Time.now.utc} "),
209
+ @tui.text_span(content: "#{t("Locale")}: #{@lang}")
210
+ ]
211
+
212
+ if current_tab.data[:filter]
213
+ @filter_style = @tui.style(fg: :white, bg: :dark_gray)
214
+ footer += [
215
+ @tui.text_span(content: " #{t("Filter")}: ", style: @filter_style),
216
+ @tui.text_span(content: current_tab.data[:filter], style: @filter_style),
217
+ @tui.text_span(content: "_", style: @tui.style(fg: :white, bg: :dark_gray, modifiers: [:slow_blink]))
218
+ ]
219
+ end
220
+ footer << @tui.text_span(content: " FPS: #{previous_fps}") if debugging?
221
+ lines << @tui.text_line(spans: footer)
222
+
223
+ controls = @tui.block(title: t("Controls"), borders: [:all],
224
+ children: [@tui.paragraph(text: lines)])
283
225
  frame.render_widget(controls, area)
284
226
  end
285
227
 
286
228
  def handle_input
287
- case @tui.poll_event
288
- in {type: :key, code: "backspace"} if @data[:filtering]
289
- @data[:filter] = @data[:filter].empty? ? "" : @data[:filter][0..-2]
290
- in {type: :key, code: "enter"} if @data[:filtering]
291
- @data[:filtering] = nil
292
- @data[:selected] = []
229
+ # We shouldn't need more than 10 FPS for a data-oriented app.
230
+ # This throttles down our CPU usage. Default is 60 FPS.
231
+ case @tui.poll_event(timeout: 0.1)
293
232
  in {type: :key, code: "esc"} if @showing == :help
294
233
  @showing = :main
295
- in {type: :key, code: "esc"} if @data[:filtering]
296
- @data[:filtering] = nil
297
- @data[:filter] = nil
298
- @data[:selected] = []
299
- in {type: :key, code: code} if @data[:filtering] && code.length == 1
300
- @data[:filter] += code
301
- @data[:selected] = []
234
+ in {type: :key, code: code} if current_tab.filtering? && code.length == 1
235
+ current_tab.append_to_filter(code)
236
+ current_tab.refresh_data
302
237
  in {type: :key, code:, modifiers:}
303
- control = CONTROLS.find { |ctrl|
238
+ control = current_tab.controls.find { |ctrl|
304
239
  ctrl[:code] == code &&
305
- (ctrl[:modifiers] || []) == (modifiers || []) &&
306
- ctrl[:tabs].include?(@current_tab)
240
+ (ctrl[:modifiers] || []) == (modifiers || [])
307
241
  }
308
242
  return unless control
309
- control[:action].call(self).tap {
243
+ control[:action].call(self, current_tab).tap {
310
244
  refresh_data if control[:refresh]
311
245
  }
312
246
  else
313
247
  # Ignore other events
314
248
  end
315
249
  rescue => ex
316
- log(ex.message, ex.backtrace)
317
- end
318
-
319
- def show_help
320
- @showing = :help
321
- end
322
-
323
- # Navigate tabs to the left or right.
324
- # @param direction [Symbol] :left or :right
325
- def navigate_tab(direction)
326
- index_change = (direction == :right) ? 1 : -1
327
- @current_tab = TABS[(TABS.index(@current_tab) + index_change) % TABS.size]
328
- @selected_row_index = 0
329
- @data = {
330
- selected: [],
331
- filter: nil
332
- }
333
- end
334
-
335
- # Navigate the row selection up or down in the current tab's table.
336
- # @param direction [Symbol] :up or :down
337
- def navigate_row(direction)
338
- ids = @data.dig(:table, :row_ids)
339
- return if !ids || ids.empty?
340
-
341
- index_change = (direction == :down) ? 1 : -1
342
- @selected_row_index = (@selected_row_index + index_change) % ids.count
343
- end
344
-
345
- def start_filtering
346
- @data[:filtering] = true
347
- @data[:filter] = ""
348
- end
349
-
350
- def stop_filtering
351
- @data[:filtering] = false
352
- end
353
-
354
- def quiet!
355
- each_selection do |id|
356
- Sidekiq::Process.new("identity" => id).quiet!
357
- end
358
- end
359
-
360
- def terminate!
361
- each_selection do |id|
362
- Sidekiq::Process.new("identity" => id).stop!
363
- end
364
- end
365
-
366
- def each_selection(unselect: true, &)
367
- sel = @data[:selected]
368
- finished = []
369
- if !sel.empty?
370
- sel.each do |id|
371
- yield id
372
- # When processing multiple items in bulk, we want to unselect
373
- # each row if its operation succeeds so our UI will not
374
- # re-process rows 1-3 if row 4 fails.
375
- finished << id
376
- end
377
- else
378
- ids = @data.dig(:table, :row_ids)
379
- return if !ids || ids.empty?
380
- yield ids[@selected_row_index]
381
- end
382
- ensure
383
- @data[:selected] = sel - finished if unselect
384
- end
385
-
386
- def delete_queue!
387
- each_selection do |qname|
388
- Sidekiq::Queue.new(qname).clear
389
- end
390
- end
391
-
392
- def alter_rows!(action = :add_to_queue)
393
- log(@current_tab, @data[:selected])
394
- set = case @current_tab
395
- when "Scheduled"
396
- Sidekiq::ScheduledSet.new
397
- when "Retries"
398
- Sidekiq::RetrySet.new
399
- when "Dead"
400
- Sidekiq::DeadSet.new
401
- end
402
- return unless set
403
- each_selection do |id|
404
- score, jid = id.split("|")
405
- item = set.fetch(score, jid)&.first
406
- item&.send(action)
407
- end
408
- end
409
-
410
- def prev_page
411
- opts = @data.dig(:table, :pager)
412
- return unless opts
413
- return if opts.page < 2
414
-
415
- @data[:table][:pager] = Sidekiq::TUI::PageOptions.new(opts.page - 1, opts.size)
416
- end
417
-
418
- def next_page
419
- np = @data.dig(:table, :next_page)
420
- return unless np
421
- opts = @data.dig(:table, :pager)
422
- return unless opts
423
-
424
- @data[:table][:pager] = Sidekiq::TUI::PageOptions.new(np, opts.size)
425
- end
426
-
427
- def toggle_select(which = :current)
428
- sel = @data[:selected]
429
- log(which, sel)
430
- if which == :current
431
- x = @data[:table][:row_ids][@selected_row_index]
432
- if sel.index(x)
433
- # already checked, uncheck it
434
- sel.delete(x)
435
- else
436
- sel << x
437
- end
438
- elsif sel.empty?
439
- @data[:selected] = @data[:table][:row_ids]
440
- else
441
- sel.clear
442
- end
443
- end
444
-
445
- def toggle_pause_queue!
446
- return unless Sidekiq.pro?
447
-
448
- each_selection do |qname|
449
- queue = Sidekiq::Queue.new(qname)
450
- if queue.paused?
451
- queue.unpause!
452
- else
453
- queue.pause!
454
- end
455
- end
250
+ logger.error { [ex.message, ex.backtrace] }
456
251
  end
457
252
 
458
253
  def redis_url
@@ -468,552 +263,120 @@ module Sidekiq
468
263
  end
469
264
 
470
265
  def refresh_data
471
- stats = Sidekiq::Stats.new
472
- @data[:stats] = {
473
- processed: stats.processed,
474
- failed: stats.failed,
475
- busy: stats.workers_size,
476
- enqueued: stats.enqueued,
477
- retries: stats.retry_size,
478
- scheduled: stats.scheduled_size,
479
- dead: stats.dead_size
480
- }
481
-
482
- case @current_tab
483
- when "Home"
484
- @data[:chart] ||= {
485
- previous_stats: {
486
- processed: stats.processed,
487
- failed: stats.failed
488
- },
489
- deltas: {
490
- processed: Array.new(50, 0),
491
- failed: Array.new(50, 0)
492
- }
493
- }
494
-
495
- processed_delta = stats.processed - @data[:chart][:previous_stats][:processed]
496
- failed_delta = stats.failed - @data[:chart][:previous_stats][:failed]
497
-
498
- @data[:chart][:deltas][:processed].shift
499
- @data[:chart][:deltas][:processed].push(processed_delta)
500
- @data[:chart][:deltas][:failed].shift
501
- @data[:chart][:deltas][:failed].push(failed_delta)
502
-
503
- @data[:chart][:previous_stats] = {
504
- processed: stats.processed,
505
- failed: stats.failed
506
- }
507
-
508
- redis_info = Sidekiq.default_configuration.redis_info
509
-
510
- @data[:redis_info] = {
511
- version: redis_info["redis_version"] || "N/A",
512
- uptime_days: redis_info["uptime_in_days"] || "N/A",
513
- connected_clients: redis_info["connected_clients"] || "N/A",
514
- used_memory: redis_info["used_memory_human"] || "N/A",
515
- peak_memory: redis_info["used_memory_peak_human"] || "N/A"
516
- }
517
- when "Busy"
518
- busy = []
519
- table_row_ids = []
520
-
521
- Sidekiq::ProcessSet.new.each do |p|
522
- name = "#{p["hostname"]}:#{p["pid"]}"
523
- name += " ⭐️" if p.leader?
524
- name += " 🛑" if p.stopping?
525
- busy << [
526
- selected?(p) ? "✅" : "",
527
- name,
528
- Time.at(p["started_at"]).utc,
529
- format_memory(p["rss"].to_i),
530
- number_with_delimiter(p["concurrency"]),
531
- number_with_delimiter(p["busy"])
532
- ]
533
- table_row_ids << p.identity
534
- end
535
-
536
- @data[:busy] = busy
537
- @data[:table] = {row_ids: table_row_ids}
538
- when "Queues"
539
- queue_summaries = Sidekiq::Stats.new.queue_summaries.sort_by(&:name)
540
-
541
- selected = Array(@data[:selected])
542
- queues = queue_summaries.map { |queue_summary|
543
- row_cells = [
544
- selected.index(queue_summary.name) ? "✅" : "",
545
- queue_summary.name,
546
- queue_summary.size.to_s,
547
- number_with_delimiter(queue_summary.latency, {precision: 2})
548
- ]
549
- row_cells << (queue_summary.paused? ? "✅" : "") if Sidekiq.pro?
550
- row_cells
551
- }
552
-
553
- table_row_ids = queue_summaries.map(&:name)
554
-
555
- @data[:queues] = queues
556
- @data[:table] = {row_ids: table_row_ids}
557
- when "Scheduled"
558
- data_for_set(Sidekiq::ScheduledSet.new)
559
- when "Retries"
560
- data_for_set(Sidekiq::RetrySet.new)
561
- when "Dead"
562
- data_for_set(Sidekiq::DeadSet.new)
563
- when "Metrics"
564
- # only need to refresh every 60 seconds
565
- if !@data[:metrics_refresh] || @data[:metrics_refresh] < Time.now
566
- q = Sidekiq::Metrics::Query.new
567
- query_result = q.top_jobs(minutes: 60)
568
- @data[:metrics] = query_result
569
- @data[:metrics_refresh] = Time.now + 60
570
- end
571
- end
572
-
266
+ # logger.info GC.stat
267
+ current_tab.refresh_data
573
268
  @last_refresh = Time.now
574
269
  rescue => e
575
- @data = {error: e}
576
- end
577
-
578
- def data_for_set(set)
579
- f = @data[:filter]
580
- pager, rows, current, total = if f && f.size > 2
581
- rows = set.scan(f).to_a
582
- sz = rows.size
583
- [Sidekiq::TUI::PageOptions.new(1, sz), rows, 1, sz]
584
- else
585
- pager = @data.dig(:table, :pager) || Sidekiq::TUI::PageOptions.new(1, 25)
586
- current, total, items = page(set.name, pager.page, pager.size)
587
- rows = items.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
588
- [pager, rows, current, total]
589
- end
590
-
591
- @data.merge!(
592
- table: {pager:, rows:, current_page: current, total:,
593
- next_page: (current * pager.size < total) ? pager.page + 1 : nil,
594
- row_ids: rows.map { |job| [job.score, job["jid"]].join("|") }}
595
- )
596
- end
597
-
598
- def render_busy(frame, area)
599
- chunks = @tui.layout_split(
600
- area,
601
- direction: :vertical,
602
- constraints: [
603
- @tui.constraint_length(4), # Stats
604
- @tui.constraint_length(4), # Status
605
- @tui.constraint_fill(1) # Graph
606
- ]
607
- )
608
-
609
- render_stats_section(frame, chunks[0])
610
- render_status_section(frame, chunks[1])
611
- render_table(frame, chunks[2]) do
612
- {
613
- title: "Processes",
614
- header: ["☑️", "Name", "Started", "RSS", "Threads", "Busy"],
615
- widths: [
616
- @tui.constraint_length(5),
617
- @tui.constraint_fill(1),
618
- @tui.constraint_length(24),
619
- @tui.constraint_length(10),
620
- @tui.constraint_length(6),
621
- @tui.constraint_length(6)
622
- ],
623
- rows: @data[:busy].map.with_index { |cells, idx|
624
- @tui.table_row(
625
- cells:,
626
- style: idx.even? ? nil : @tui.style(bg: :dark_gray)
627
- )
628
- }
629
- }
630
- end
631
- end
632
-
633
- def render_set(frame, area)
634
- chunks = @tui.layout_split(
635
- area,
636
- direction: :vertical,
637
- constraints: [
638
- @tui.constraint_length(4), # Stats
639
- @tui.constraint_fill(1) # Table
640
- ]
641
- )
642
-
643
- render_stats_section(frame, chunks[0])
644
- render_table(frame, chunks[1]) do
645
- {
646
- title: @current_tab,
647
- header: ["☑️", "When", "Queue", "Job", "Arguments"],
648
- widths: [
649
- @tui.constraint_length(5),
650
- @tui.constraint_length(24),
651
- @tui.constraint_length(20),
652
- @tui.constraint_length(30),
653
- @tui.constraint_fill(1)
654
- ]
655
- }.tap do |h|
656
- rows = @data[:table][:rows].map.with_index { |entry, idx|
657
- @tui.table_row(
658
- cells: [
659
- selected?(entry) ? "✅" : "",
660
- entry.at,
661
- entry.queue,
662
- entry.display_class,
663
- entry.display_args
664
- ],
665
- style: idx.even? ? nil : @tui.style(bg: :dark_gray)
666
- )
667
- }
668
- h[:rows] = rows
669
- end
670
- end
671
- end
672
-
673
- def selected?(entry)
674
- @data[:selected].index(entry.id)
675
- end
676
-
677
- def render_home(frame, area)
678
- chunks = @tui.layout_split(
679
- area,
680
- direction: :vertical,
681
- constraints: [
682
- @tui.constraint_length(4), # Stats
683
- @tui.constraint_fill(1), # Graph
684
- @tui.constraint_length(4) # Redis
685
- ]
686
- )
687
-
688
- render_stats_section(frame, chunks[0])
689
- render_chart_section(frame, chunks[1])
690
- render_redis_info_section(frame, chunks[2])
270
+ handle_exception(e)
271
+ current_tab.error = e
691
272
  end
692
273
 
693
- def render_status_section(frame, area)
694
- keys = ["Processes", "Threads", "Busy", "Utilization", "RSS"]
695
- values = []
696
- processes = Sidekiq::ProcessSet.new
697
- workset = Sidekiq::WorkSet.new
698
- ws = workset.size
699
- values << (s = processes.size
700
- number_with_delimiter(s))
701
- values << (x = processes.total_concurrency
702
- number_with_delimiter(x))
703
- values << number_with_delimiter(ws)
704
- values << "#{(x == 0) ? 0 : ((ws / x.to_f) * 100).round(0)}%"
705
- values << format_memory(processes.total_rss)
706
-
707
- keys_line = keys.map { |k| k.to_s.ljust(12) }.join(" ")
708
- values_line = values.map { |v| v.to_s.ljust(12) }.join(" ")
274
+ def render_error(frame, area, err)
275
+ header = [@tui.text_line(
276
+ spans: [@tui.text_span(content: err.message, style: @tui.style(modifiers: [:bold]))],
277
+ alignment: :center
278
+ )]
279
+ lines = Array(err.backtrace).map { |line| @tui.text_line(spans: [@tui.text_span(content: line)]) }
709
280
 
710
281
  frame.render_widget(
711
282
  @tui.paragraph(
712
- text: [keys_line, values_line],
713
- block: @tui.block(title: "Status", borders: [:all])
283
+ text: header + lines,
284
+ alignment: :left,
285
+ block: @tui.block(title: t("Error"), borders: [:all], border_style: @tui.style(fg: :light_red))
714
286
  ),
715
287
  area
716
288
  )
717
289
  end
718
290
 
719
- def render_stats_section(frame, area)
720
- stats = @data[:stats]
721
-
722
- keys = ["Processed", "Failed", "Busy", "Enqueued", "Retries", "Scheduled", "Dead"]
723
- values = [
724
- stats[:processed],
725
- stats[:failed],
726
- stats[:busy],
727
- stats[:enqueued],
728
- stats[:retries],
729
- stats[:scheduled],
730
- stats[:dead]
731
- ]
732
-
733
- # Format keys and values with spacing
734
- keys_line = keys.map { |k| k.to_s.ljust(12) }.join(" ")
735
- values_line = values.map { |v| v.to_s.ljust(12) }.join(" ")
736
-
737
- frame.render_widget(
738
- @tui.paragraph(
739
- text: [keys_line, values_line],
740
- block: @tui.block(title: "Statistics", borders: [:all])
741
- ),
742
- area
743
- )
291
+ def show_help
292
+ @showing = :help
744
293
  end
745
294
 
746
- def render_chart_section(frame, area)
747
- max_value = [@data[:chart][:deltas][:processed].max, @data[:chart][:deltas][:failed].max, 1].max
748
- y_max = [max_value, 5].max
749
-
750
- processed_data = @data[:chart][:deltas][:processed].each_with_index.map { |value, idx| [idx.to_f, value.to_f] }
751
- failed_data = @data[:chart][:deltas][:failed].each_with_index.map { |value, idx| [idx.to_f, value.to_f] }
752
-
753
- datasets = [
754
- @tui.dataset(
755
- name: "",
756
- data: processed_data,
757
- style: @tui.style(fg: :green),
758
- marker: :dot,
759
- graph_type: :line
760
- ),
761
- @tui.dataset(
762
- name: "",
763
- data: failed_data,
764
- style: @tui.style(fg: :red),
765
- marker: :dot,
766
- graph_type: :line
767
- )
768
- ]
769
-
770
- num_labels = 5
771
- y_labels = (0...num_labels).map do |i|
772
- value = ((y_max * i) / (num_labels - 1)).round
773
- value.to_s
774
- end
775
-
776
- beacon_pulse = (Time.now.to_i % 2 == 0) ? "●" : " "
777
-
778
- chart = @tui.chart(
779
- datasets: datasets,
780
- x_axis: @tui.axis(
781
- bounds: [0.0, 49.0],
782
- labels: [],
783
- style: @tui.style(fg: :white)
784
- ),
785
- y_axis: @tui.axis(
786
- bounds: [0.0, y_max.to_f],
787
- labels: y_labels,
788
- style: @tui.style(fg: :white)
789
- ),
790
- block: @tui.block(
791
- title: "Dashboard #{beacon_pulse}",
792
- borders: [:all]
793
- )
794
- )
795
-
796
- frame.render_widget(chart, area)
295
+ def all
296
+ @all ||= Tabs::All.map { |kls| kls.new(self) }
797
297
  end
798
298
 
799
- def render_redis_info_section(frame, area)
800
- redis_info = @data[:redis_info]
801
-
802
- uptime_value = (redis_info[:uptime_days] == "N/A") ? "N/A" : "#{redis_info[:uptime_days]} days"
803
-
804
- keys = ["Version", "Uptime", "Connected Clients", "Memory Usage", "Peak Memory"]
805
- values = [
806
- redis_info[:version].to_s,
807
- uptime_value,
808
- redis_info[:connected_clients].to_s,
809
- redis_info[:used_memory].to_s,
810
- redis_info[:peak_memory].to_s
811
- ]
812
-
813
- # Format keys and values with spacing
814
- keys_line = keys.map { |k| k.ljust(18) }.join(" ")
815
- values_line = values.map { |v| v.ljust(18) }.join(" ")
816
-
817
- frame.render_widget(
818
- @tui.paragraph(
819
- text: [keys_line, values_line],
820
- block: @tui.block(title: "Redis Information", borders: [:all])
821
- ),
822
- area
823
- )
299
+ def current_tab
300
+ @current ||= @all.first
824
301
  end
825
302
 
826
- def render_queues(frame, area)
827
- header = ["☑️", "Queue", "Size", "Latency"]
828
- header << "Paused?" if Sidekiq.pro?
829
-
830
- chunks = @tui.layout_split(
831
- area,
832
- direction: :vertical,
833
- constraints: [
834
- @tui.constraint_length(4), # Stats
835
- @tui.constraint_fill(1) # Table
836
- ]
837
- )
838
-
839
- render_stats_section(frame, chunks[0])
840
- render_table(frame, chunks[1]) do
841
- {
842
- title: "Queues",
843
- header:,
844
- widths: header.map.with_index { |_, idx|
845
- @tui.constraint_length((idx == 1) ? 60 : 10)
846
- },
847
- rows: @data[:queues].map.with_index { |cells, idx|
848
- @tui.table_row(
849
- cells:,
850
- style: idx.even? ? nil : @tui.style(bg: :dark_gray)
851
- )
852
- }
853
- }
854
- end
303
+ # Navigate tabs to the left or right.
304
+ # @param direction [Symbol] :left or :right
305
+ def navigate(direction)
306
+ index_change = (direction == :right) ? 1 : -1
307
+ @current = @all[(@all.index(current_tab) + index_change) % @all.size]
308
+ @current.reset_data
855
309
  end
856
310
 
857
- def render_metrics(frame, area)
858
- chunks = @tui.layout_split(
859
- area,
860
- direction: :vertical,
861
- constraints: [
862
- @tui.constraint_length(4), # Stats
863
- @tui.constraint_fill(1) # Chart
864
- # TOOD Table
865
- ]
866
- )
867
-
868
- render_stats_section(frame, chunks[0])
869
- render_metrics_chart(frame, chunks[1])
311
+ public def t(msg, options = nil)
312
+ string = @strings[msg] || msg
313
+ if options.nil?
314
+ string
315
+ else
316
+ string % options
317
+ end
870
318
  end
871
319
 
872
- COLORS = %i[blue cyan yellow red green white gray]
873
-
874
- # Run to generate metrics data:
875
- # cd myapp && bundle install
876
- # bundle exec rake seed_jobs
877
- # bundle exec sidekiq
878
- def render_metrics_chart(frame, area)
879
- y_max = 5
880
- csize = COLORS.size
881
- q = @data[:metrics]
882
- job_results = q.job_results.sort_by { |(kls, jr)| jr.totals["s"] }.reverse.first(COLORS.size)
883
- # visible_kls = job_results.first(5).map(&:first)
884
- # chart_data = {
885
- # series: job_results.map { |(kls, jr)| [kls, jr.dig("series", "s")] }.to_h,
886
- # marks: query_result.marks.map { |m| [m.bucket, m.label] },
887
- # starts_at: query_result.starts_at.iso8601,
888
- # ends_at: query_result.ends_at.iso8601,
889
- # visibleKls: visible_kls,
890
- # yLabel: 'TotalExecutionTime',
891
- # units: 'seconds',
892
- # markLabel: '*',
893
- # }
894
-
895
- datasets = job_results.map.with_index do |(kls, data), idx|
896
- # log kls, data, idx
897
- hrdata = data.dig("series", "s")
898
- tm = Time.now
899
- tmi = tm.to_i
900
- tm = Time.at(tmi - (tmi % 60)).utc
901
- data = Array.new(60) { |idx| idx }.map do |bucket_idx|
902
- jumpback = bucket_idx * 60
903
- value = hrdata[(tm - jumpback).iso8601] || 0
904
- y_max = value if value > y_max
905
- # we have 60 data points, newest data should be
906
- # at highest indexes so we have to rejigger the index
907
- # here
908
- [59 - bucket_idx, value]
320
+ def load_strings(lang)
321
+ {}.tap do |all|
322
+ find_locale_files(lang).each do |file|
323
+ strs = YAML.safe_load_file(file)
324
+ all.merge! strs[lang]
909
325
  end
910
- # log data
911
-
912
- log(data)
913
- @tui.dataset(name: kls,
914
- data: data,
915
- style: @tui.style(fg: COLORS[idx % csize]),
916
- marker: :dot,
917
- graph_type: :line)
918
326
  end
919
-
920
- num_labels = 5
921
- y_labels = (0...num_labels).map do |i|
922
- value = ((y_max * i) / (num_labels - 1)).round
923
- value.to_s
924
- end
925
- xlabels = [
926
- q.starts_at.iso8601[11..15],
927
- q.ends_at.iso8601[11..15]
928
- ]
929
-
930
- # beacon_pulse = (Time.now.to_i % 2 == 0) ? "●" : " "
931
-
932
- chart = @tui.chart(
933
- datasets: datasets,
934
- x_axis: @tui.axis(
935
- bounds: [0.0, 60.0],
936
- labels: xlabels,
937
- style: @tui.style(fg: :white)
938
- ),
939
- y_axis: @tui.axis(
940
- bounds: [0.0, y_max.to_f],
941
- labels: y_labels,
942
- style: @tui.style(fg: :white)
943
- ),
944
- block: @tui.block(
945
- title: "Metrics",
946
- borders: [:all]
947
- )
948
- )
949
-
950
- frame.render_widget(chart, area)
951
327
  end
952
328
 
953
- def render_error(frame, area, err)
954
- log(err.message, err.backtrace)
955
- header = [@tui.text_line(
956
- spans: [@tui.text_span(content: err.message, style: @tui.style(modifiers: [:bold]))],
957
- alignment: :center
958
- )]
959
- lines = Array(err.backtrace).map { |line| @tui.text_line(spans: [@tui.text_span(content: line)]) }
960
-
961
- frame.render_widget(
962
- @tui.paragraph(
963
- text: header + lines,
964
- alignment: :left,
965
- block: @tui.block(title: "Error", borders: [:all], border_style: @tui.style(fg: :red))
966
- ),
967
- area
968
- )
329
+ def locale_files
330
+ @@locale_files ||= LOCALE_DIRECTORIES.flat_map { |path|
331
+ Dir["#{path}/*.yml"]
332
+ }
969
333
  end
970
334
 
971
- # TODO Implement I18n delimiter
972
- def number_with_delimiter(number, options = {})
973
- precision = options[:precision] || 0
974
- number.round(precision)
335
+ def available_locales
336
+ @@available_locales ||= Set.new(locale_files.map { |path| File.basename(path, ".yml") })
975
337
  end
976
338
 
977
- def format_memory(rss_kb)
978
- return "0" if rss_kb.nil? || rss_kb == 0
339
+ def find_locale_files(lang)
340
+ locale_files.select { |file| file =~ /\/#{lang}\.yml$/ }
341
+ end
979
342
 
980
- if rss_kb < 100_000
981
- "#{number_with_delimiter(rss_kb)} KB"
982
- elsif rss_kb < 10_000_000
983
- "#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB"
984
- else
985
- "#{number_with_delimiter(rss_kb / (1024.0 * 1024.0), precision: 1)} GB"
343
+ def load_locale
344
+ require "yaml"
345
+ lang = @lang.split(".").first # "en_US"
346
+ while lang.size > 0
347
+ hash = load_strings(lang)
348
+ if hash.size > 0
349
+ # found a working language dataset
350
+ @lang = lang
351
+ @strings = hash
352
+ Sidekiq.logger.debug { "using the #{lang} locale" }
353
+ break
354
+ end
355
+ # Try "en_US", "en_U", "en_", "en"
356
+ # It's ugly and bruteforce but it works
357
+ lang = lang[..-2]
986
358
  end
987
359
  end
988
360
 
989
- def render_table(frame, area)
990
- page = @data.dig(:table, :current_page) || 1
991
- rows = @data.dig(:table, :rows) || []
992
- total = @data.dig(:table, :total) || 0
993
- footer = ["", "Page: #{page}", "Count: #{rows.size}", "Total: #{total}"]
994
- footer << "Selected: #{@data[:selected].size}" unless @data[:selected].empty?
361
+ def track_fps
362
+ # We hold two fps buckets: one for current second, one for previous second
363
+ idx = Time.now.to_i % 2
364
+ @fps[idx] += 1
365
+ yield
366
+ end
995
367
 
996
- if @data[:filter]
997
- @filter_style = @tui.style(fg: :white, bg: :dark_gray)
998
- spans = [
999
- @tui.text_span(content: "Filter: ", style: @filter_style),
1000
- @tui.text_span(content: @data[:filter], style: @filter_style)
1001
- ]
1002
- spans << @tui.text_span(content: "_", style: @tui.style(fg: :white, bg: :dark_gray, modifiers: [:slow_blink])) if @data[:filtering]
1003
- footer << @tui.text_line(spans: spans)
368
+ def previous_fps
369
+ curidx = Time.now.to_i % 2
370
+ prev = (curidx == 1) ? 0 : 1
371
+ if (val = @fps[prev]) != 0
372
+ @previous_fps = val
373
+ @fps[prev] = 0
1004
374
  end
375
+ @previous_fps
376
+ end
1005
377
 
1006
- defaults = {
1007
- title: "TableName",
1008
- highlight_symbol: "➡️",
1009
- selected_row: @selected_row_index,
1010
- row_highlight_style: @tui.style(fg: :white, bg: :blue),
1011
- footer: footer
1012
- }
1013
- hash = defaults.merge(yield)
1014
- hash[:block] ||= @tui.block(title: hash.delete(:title), borders: :all)
1015
- table = @tui.table(**hash)
1016
- frame.render_widget(table, area)
378
+ def debugging?
379
+ !!ENV["DEBUG"]
1017
380
  end
1018
381
  end
1019
382
  end