sqdash 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +71 -0
- data/README.md +60 -3
- data/Rakefile +8 -1
- data/exe/sqdash +2 -0
- data/lib/sqdash/autocomplete.rb +164 -0
- data/lib/sqdash/cli.rb +68 -684
- data/lib/sqdash/config.rb +39 -0
- data/lib/sqdash/database.rb +3 -3
- data/lib/sqdash/input_handler.rb +281 -0
- data/lib/sqdash/renderer.rb +256 -0
- data/lib/sqdash/version.rb +1 -1
- data/lib/sqdash.rb +4 -0
- metadata +9 -3
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Sqdash
|
|
6
|
+
class Config
|
|
7
|
+
CONFIG_FILENAME = ".sqdash.yml"
|
|
8
|
+
|
|
9
|
+
def self.load(path = nil)
|
|
10
|
+
new(path)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(path = nil)
|
|
14
|
+
@data = {}
|
|
15
|
+
file = path || find_config_file
|
|
16
|
+
return unless file
|
|
17
|
+
|
|
18
|
+
@data = YAML.safe_load_file(file) || {}
|
|
19
|
+
rescue Errno::ENOENT
|
|
20
|
+
# Explicit path given but file not found
|
|
21
|
+
abort "\e[31mConfig file not found: #{path}\e[0m" if path
|
|
22
|
+
rescue Psych::SyntaxError => e
|
|
23
|
+
abort "\e[31mInvalid YAML in #{file}: #{e.message}\e[0m"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def database_url
|
|
27
|
+
@data["database_url"]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def find_config_file
|
|
33
|
+
[
|
|
34
|
+
File.join(Dir.pwd, CONFIG_FILENAME),
|
|
35
|
+
File.join(Dir.home, CONFIG_FILENAME)
|
|
36
|
+
].find { |p| File.exist?(p) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/sqdash/database.rb
CHANGED
|
@@ -15,7 +15,7 @@ module Sqdash
|
|
|
15
15
|
require_adapter!(url)
|
|
16
16
|
ActiveRecord::Base.establish_connection(url)
|
|
17
17
|
ActiveRecord::Base.connection
|
|
18
|
-
rescue => e
|
|
18
|
+
rescue StandardError => e
|
|
19
19
|
abort "\e[31mFailed to connect: #{e.message}\e[0m\n\n" \
|
|
20
20
|
"Usage: sqdash <database-url>\n" \
|
|
21
21
|
"Run sqdash --help for details."
|
|
@@ -32,8 +32,8 @@ module Sqdash
|
|
|
32
32
|
|
|
33
33
|
require config[:gem]
|
|
34
34
|
rescue LoadError
|
|
35
|
-
abort "\e[31mMissing database adapter gem '#{config[:gem]}'. Install it with:\n" \
|
|
36
|
-
"
|
|
35
|
+
abort "\e[31mMissing database adapter gem '#{config[:gem]}'. Install it with:\n " \
|
|
36
|
+
"gem install #{config[:gem]}\e[0m"
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
end
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/console"
|
|
4
|
+
require "io/wait"
|
|
5
|
+
|
|
6
|
+
module Sqdash
|
|
7
|
+
module InputHandler
|
|
8
|
+
def handle_input
|
|
9
|
+
@saved_stty = `stty -g`.chomp
|
|
10
|
+
system("stty", "-echo", "-icanon", "min", "1")
|
|
11
|
+
loop do
|
|
12
|
+
if @needs_redraw
|
|
13
|
+
@needs_redraw = false
|
|
14
|
+
adjust_scroll
|
|
15
|
+
full_draw
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
key = read_key
|
|
19
|
+
|
|
20
|
+
unless key
|
|
21
|
+
if @detail_job
|
|
22
|
+
@detail_job.reload
|
|
23
|
+
else
|
|
24
|
+
load_data
|
|
25
|
+
end
|
|
26
|
+
draw_screen
|
|
27
|
+
next
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if @detail_job
|
|
31
|
+
handle_detail_input(key)
|
|
32
|
+
elsif @command_mode
|
|
33
|
+
handle_command_input(key)
|
|
34
|
+
elsif @filter_mode
|
|
35
|
+
handle_filter_input(key)
|
|
36
|
+
else
|
|
37
|
+
handle_normal_input(key)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
draw_screen
|
|
41
|
+
end
|
|
42
|
+
ensure
|
|
43
|
+
system("stty", @saved_stty) if @saved_stty
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def read_key
|
|
47
|
+
ready = $stdin.wait_readable(1)
|
|
48
|
+
return nil unless ready
|
|
49
|
+
|
|
50
|
+
$stdin.getc
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def handle_normal_input(key)
|
|
54
|
+
case key
|
|
55
|
+
when "\e"
|
|
56
|
+
next_chars = begin
|
|
57
|
+
$stdin.read_nonblock(2)
|
|
58
|
+
rescue StandardError
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
case next_chars
|
|
62
|
+
when "[A" # up
|
|
63
|
+
@selected = [0, @selected - 1].max
|
|
64
|
+
adjust_scroll
|
|
65
|
+
when "[B" # down
|
|
66
|
+
max = [@jobs.length - 1, 0].max
|
|
67
|
+
@selected = [max, @selected + 1].min
|
|
68
|
+
adjust_scroll
|
|
69
|
+
when nil # bare Escape — clear active filter
|
|
70
|
+
if @filter_text.length.positive?
|
|
71
|
+
@filter_text = ""
|
|
72
|
+
load_data
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
when "q"
|
|
76
|
+
throw(:quit)
|
|
77
|
+
when "/"
|
|
78
|
+
@filter_mode = true
|
|
79
|
+
@filter_text = ""
|
|
80
|
+
when ":"
|
|
81
|
+
@command_mode = true
|
|
82
|
+
@command_text = ""
|
|
83
|
+
when "r"
|
|
84
|
+
retry_selected
|
|
85
|
+
when "d"
|
|
86
|
+
discard_selected
|
|
87
|
+
when "\r", "\n"
|
|
88
|
+
show_detail
|
|
89
|
+
when " "
|
|
90
|
+
load_data
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def handle_filter_input(key)
|
|
95
|
+
case key
|
|
96
|
+
when "\r", "\n"
|
|
97
|
+
@filter_mode = false
|
|
98
|
+
print "\e[?25l"
|
|
99
|
+
load_data
|
|
100
|
+
when "\e"
|
|
101
|
+
begin
|
|
102
|
+
$stdin.read_nonblock(2)
|
|
103
|
+
rescue StandardError
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
@filter_mode = false
|
|
107
|
+
@filter_text = ""
|
|
108
|
+
print "\e[?25l"
|
|
109
|
+
load_data
|
|
110
|
+
when "\t"
|
|
111
|
+
autocomplete_filter
|
|
112
|
+
when "\u007F", "\b"
|
|
113
|
+
@filter_text = @filter_text[0..-2]
|
|
114
|
+
load_data
|
|
115
|
+
else
|
|
116
|
+
if key.match?(/[[:print:]]/)
|
|
117
|
+
@filter_text += key
|
|
118
|
+
load_data
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def handle_command_input(key)
|
|
124
|
+
case key
|
|
125
|
+
when "\r", "\n"
|
|
126
|
+
execute_command
|
|
127
|
+
@command_mode = false
|
|
128
|
+
@command_text = ""
|
|
129
|
+
print "\e[?25l"
|
|
130
|
+
when "\e"
|
|
131
|
+
begin
|
|
132
|
+
$stdin.read_nonblock(2)
|
|
133
|
+
rescue StandardError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
@command_mode = false
|
|
137
|
+
@command_text = ""
|
|
138
|
+
print "\e[?25l"
|
|
139
|
+
when "\t"
|
|
140
|
+
autocomplete_command
|
|
141
|
+
when "\u007F", "\b"
|
|
142
|
+
@command_text = @command_text[0..-2]
|
|
143
|
+
else
|
|
144
|
+
@command_text += key if key.match?(/[[:print:]]/)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def handle_detail_input(key)
|
|
149
|
+
case key
|
|
150
|
+
when "\e"
|
|
151
|
+
next_chars = begin
|
|
152
|
+
$stdin.read_nonblock(2)
|
|
153
|
+
rescue StandardError
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
case next_chars
|
|
157
|
+
when "[A"
|
|
158
|
+
@detail_scroll = [@detail_scroll - 1, 0].max
|
|
159
|
+
when "[B"
|
|
160
|
+
@detail_scroll += 1
|
|
161
|
+
when nil
|
|
162
|
+
@detail_job = nil
|
|
163
|
+
full_draw
|
|
164
|
+
end
|
|
165
|
+
when "\u007F", "\b"
|
|
166
|
+
@detail_job = nil
|
|
167
|
+
full_draw
|
|
168
|
+
when "r"
|
|
169
|
+
failed = Models::FailedExecution.find_by(job_id: @detail_job.id)
|
|
170
|
+
if failed
|
|
171
|
+
failed.retry!
|
|
172
|
+
@message = "Retried job #{@detail_job.id} (#{@detail_job.class_name})"
|
|
173
|
+
@detail_job.reload
|
|
174
|
+
load_data
|
|
175
|
+
else
|
|
176
|
+
@message = "Job #{@detail_job.id} is not failed"
|
|
177
|
+
end
|
|
178
|
+
when "d"
|
|
179
|
+
failed = Models::FailedExecution.find_by(job_id: @detail_job.id)
|
|
180
|
+
if failed
|
|
181
|
+
failed.discard!
|
|
182
|
+
@message = "Discarded job #{@detail_job.id} (#{@detail_job.class_name})"
|
|
183
|
+
@detail_job = nil
|
|
184
|
+
load_data
|
|
185
|
+
full_draw
|
|
186
|
+
else
|
|
187
|
+
@message = "Job #{@detail_job.id} is not failed"
|
|
188
|
+
end
|
|
189
|
+
when "q"
|
|
190
|
+
throw(:quit)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def execute_command
|
|
195
|
+
parts = @command_text.strip.split(/\s+/)
|
|
196
|
+
return if parts.empty?
|
|
197
|
+
|
|
198
|
+
case parts[0]
|
|
199
|
+
when "sort"
|
|
200
|
+
field = parts[1] || "created"
|
|
201
|
+
direction = parts[2] || "desc"
|
|
202
|
+
case field
|
|
203
|
+
when "created"
|
|
204
|
+
@sort_column = :created_at
|
|
205
|
+
when "id"
|
|
206
|
+
@sort_column = :id
|
|
207
|
+
else
|
|
208
|
+
@message = "Unknown sort field: #{field}"
|
|
209
|
+
return
|
|
210
|
+
end
|
|
211
|
+
case direction
|
|
212
|
+
when "asc" then @sort_dir = :asc
|
|
213
|
+
when "desc" then @sort_dir = :desc
|
|
214
|
+
else
|
|
215
|
+
@message = "Unknown sort direction: #{direction}"
|
|
216
|
+
return
|
|
217
|
+
end
|
|
218
|
+
@selected = 0
|
|
219
|
+
@scroll_offset = 0
|
|
220
|
+
load_data
|
|
221
|
+
when "view"
|
|
222
|
+
target = parts[1] || "all"
|
|
223
|
+
case target
|
|
224
|
+
when "all" then switch_view(:all)
|
|
225
|
+
when "failed" then switch_view(:failed)
|
|
226
|
+
when "completed" then switch_view(:completed)
|
|
227
|
+
when "pending" then switch_view(:pending)
|
|
228
|
+
else
|
|
229
|
+
@message = "Unknown view: #{target}"
|
|
230
|
+
end
|
|
231
|
+
else
|
|
232
|
+
@message = "Unknown command: #{parts[0]}"
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def show_detail
|
|
237
|
+
return if @jobs.empty?
|
|
238
|
+
|
|
239
|
+
@detail_job = @jobs[@selected]
|
|
240
|
+
@detail_scroll = 0
|
|
241
|
+
full_draw
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def switch_view(view)
|
|
245
|
+
@view = view
|
|
246
|
+
@selected = 0
|
|
247
|
+
@scroll_offset = 0
|
|
248
|
+
load_data
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def retry_selected
|
|
252
|
+
job = @jobs[@selected]
|
|
253
|
+
return unless job
|
|
254
|
+
|
|
255
|
+
failed = Models::FailedExecution.find_by(job_id: job.id)
|
|
256
|
+
unless failed
|
|
257
|
+
@message = "Job #{job.id} is not failed"
|
|
258
|
+
return
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
failed.retry!
|
|
262
|
+
@message = "Retried job #{job.id} (#{job.class_name})"
|
|
263
|
+
load_data
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def discard_selected
|
|
267
|
+
job = @jobs[@selected]
|
|
268
|
+
return unless job
|
|
269
|
+
|
|
270
|
+
failed = Models::FailedExecution.find_by(job_id: job.id)
|
|
271
|
+
unless failed
|
|
272
|
+
@message = "Job #{job.id} is not failed"
|
|
273
|
+
return
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
failed.discard!
|
|
277
|
+
@message = "Discarded job #{job.id} (#{job.class_name})"
|
|
278
|
+
load_data
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Sqdash
|
|
6
|
+
module Renderer
|
|
7
|
+
STATUS_TEXT = {
|
|
8
|
+
failed: "\e[31m● failed\e[0m ",
|
|
9
|
+
completed: "\e[32m● completed\e[0m",
|
|
10
|
+
pending: "\e[33m● pending\e[0m "
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
VIEW_LABEL = {
|
|
14
|
+
all: "ALL",
|
|
15
|
+
failed: "\e[31mFAILED\e[0m",
|
|
16
|
+
completed: "\e[32mCOMPLETED\e[0m",
|
|
17
|
+
pending: "\e[33mPENDING\e[0m"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def truncate(str, max)
|
|
21
|
+
return str if str.length <= max
|
|
22
|
+
|
|
23
|
+
visible = str.gsub(/\e\[[0-9;]*m/, "")
|
|
24
|
+
return str if visible.length <= max
|
|
25
|
+
|
|
26
|
+
result = +""
|
|
27
|
+
visible_count = 0
|
|
28
|
+
i = 0
|
|
29
|
+
while i < str.length && visible_count < max
|
|
30
|
+
if str[i] == "\e" && str[i..] =~ /\A(\e\[[0-9;]*m)/
|
|
31
|
+
result << ::Regexp.last_match(1)
|
|
32
|
+
i += ::Regexp.last_match(1).length
|
|
33
|
+
else
|
|
34
|
+
result << str[i]
|
|
35
|
+
visible_count += 1
|
|
36
|
+
i += 1
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
result << "\e[0m"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def terminal_height
|
|
43
|
+
$stdout.winsize[0]
|
|
44
|
+
rescue StandardError
|
|
45
|
+
24
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def terminal_width
|
|
49
|
+
$stdout.winsize[1]
|
|
50
|
+
rescue StandardError
|
|
51
|
+
80
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def visible_rows
|
|
55
|
+
[terminal_height - 11, 5].max
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def status_text(status)
|
|
59
|
+
STATUS_TEXT[status]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def view_label
|
|
63
|
+
VIEW_LABEL[@view]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def full_draw
|
|
67
|
+
print "\e[?25l"
|
|
68
|
+
print "\e[2J\e[H"
|
|
69
|
+
draw_screen
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def cleanup
|
|
73
|
+
print "\e[?25h"
|
|
74
|
+
print "\e[2J\e[H"
|
|
75
|
+
puts "Goodbye!"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def column_widths
|
|
79
|
+
w = terminal_width
|
|
80
|
+
remaining = [w - 36, 10].max
|
|
81
|
+
job_w = [remaining * 65 / 100, 6].max
|
|
82
|
+
queue_w = [remaining - job_w, 4].max
|
|
83
|
+
{ id: 8, job: job_w, queue: queue_w, status: 14, created: 12 }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def draw_screen
|
|
87
|
+
if @detail_job
|
|
88
|
+
draw_detail_screen
|
|
89
|
+
else
|
|
90
|
+
draw_list_screen
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def draw_list_screen
|
|
95
|
+
print "\e[H"
|
|
96
|
+
w = terminal_width
|
|
97
|
+
rows = visible_rows
|
|
98
|
+
cols = column_widths
|
|
99
|
+
|
|
100
|
+
# Header
|
|
101
|
+
puts truncate("\e[1;36m sqdash \e[0m\e[36m Solid Queue Dashboard v#{Sqdash::VERSION}\e[0m", w) + "\e[K"
|
|
102
|
+
puts "\e[90m#{'─' * w}\e[0m\e[K"
|
|
103
|
+
|
|
104
|
+
# Stats bar
|
|
105
|
+
total = Models::Job.count
|
|
106
|
+
completed = Models::Job.where.not(finished_at: nil).count
|
|
107
|
+
failed = @failed_ids.length
|
|
108
|
+
pending = Models::ReadyExecution.count
|
|
109
|
+
sort_label = "#{@sort_column == :id ? 'ID' : 'Created'} #{@sort_dir == :asc ? '↑' : '↓'}"
|
|
110
|
+
showing = @all_loaded ? @jobs.length.to_s : "#{@jobs.length}/#{@total_count}"
|
|
111
|
+
stats = " \e[1mTotal:\e[0m #{total} \e[32m✓ #{completed}\e[0m \e[31m✗ #{failed}\e[0m \e[33m◌ #{pending}\e[0m │ View: #{view_label} │ Sort: #{sort_label} │ Showing: #{showing}"
|
|
112
|
+
puts truncate(stats, w) + "\e[K"
|
|
113
|
+
|
|
114
|
+
# Filter / Command bar
|
|
115
|
+
if @command_mode
|
|
116
|
+
print "\e[?25h"
|
|
117
|
+
hint = command_autocomplete_hint
|
|
118
|
+
puts truncate(
|
|
119
|
+
" \e[1;35m:\e[0m #{@command_text}\e[90m#{hint}\e[0m \e[90m<Tab> complete <Enter> run <Esc> cancel\e[0m", w
|
|
120
|
+
) + "\e[K"
|
|
121
|
+
elsif @filter_mode
|
|
122
|
+
print "\e[?25h"
|
|
123
|
+
hint = autocomplete_hint
|
|
124
|
+
puts truncate(" \e[1;33m/\e[0m #{@filter_text}\e[90m#{hint}\e[0m \e[90m<Tab> complete <Esc> cancel\e[0m",
|
|
125
|
+
w) + "\e[K"
|
|
126
|
+
elsif @filter_text.length.positive?
|
|
127
|
+
puts truncate(" \e[33m/#{@filter_text}\e[0m \e[90m(/ to edit, Esc to clear)\e[0m", w) + "\e[K"
|
|
128
|
+
else
|
|
129
|
+
puts "\e[K"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
puts "\e[90m#{'─' * w}\e[0m\e[K"
|
|
133
|
+
|
|
134
|
+
# Column headers
|
|
135
|
+
puts truncate(
|
|
136
|
+
"\e[1m #{'ID'.ljust(cols[:id])}#{'Job'.ljust(cols[:job])}#{'Queue'.ljust(cols[:queue])}#{'Status'.ljust(cols[:status])}Created\e[0m", w
|
|
137
|
+
) + "\e[K"
|
|
138
|
+
|
|
139
|
+
# Job list
|
|
140
|
+
visible_jobs = @jobs[@scroll_offset, rows] || []
|
|
141
|
+
|
|
142
|
+
visible_jobs.each_with_index do |job, i|
|
|
143
|
+
actual_index = @scroll_offset + i
|
|
144
|
+
status = job_status(job)
|
|
145
|
+
is_selected = actual_index == @selected
|
|
146
|
+
created = job.created_at&.strftime("%m/%d %H:%M") || "—"
|
|
147
|
+
|
|
148
|
+
line = "#{job.id.to_s.ljust(cols[:id])}#{job.class_name[0, cols[:job] - 1].ljust(cols[:job])}#{job.queue_name[0, cols[:queue] - 1].ljust(cols[:queue])}#{status_text(status)} #{created}"
|
|
149
|
+
|
|
150
|
+
if is_selected
|
|
151
|
+
puts truncate("\e[7m▸ #{line}\e[0m", w) + "\e[K"
|
|
152
|
+
else
|
|
153
|
+
puts truncate(" #{line}", w) + "\e[K"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Empty state
|
|
158
|
+
if visible_jobs.empty?
|
|
159
|
+
puts " \e[90mNo jobs found\e[0m\e[K"
|
|
160
|
+
(rows - 1).times { puts "\e[K" }
|
|
161
|
+
else
|
|
162
|
+
(rows - visible_jobs.length).times { puts "\e[K" }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Scrollbar hint
|
|
166
|
+
puts "\e[90m#{'─' * w}\e[0m\e[K"
|
|
167
|
+
|
|
168
|
+
# Message or footer
|
|
169
|
+
if @message
|
|
170
|
+
puts " \e[1;32m#{@message}\e[0m\e[K"
|
|
171
|
+
@message = nil
|
|
172
|
+
else
|
|
173
|
+
puts truncate(" \e[90m↑↓ Navigate ↵ Detail /Filter :Command r Retry d Discard q Quit\e[0m",
|
|
174
|
+
w) + "\e[K"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Position info
|
|
178
|
+
return unless @jobs.length.positive?
|
|
179
|
+
|
|
180
|
+
pos = "#{@selected + 1}/#{@total_count}"
|
|
181
|
+
print "\e[#{terminal_height};#{w - pos.length}H\e[90m#{pos}\e[0m"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def draw_detail_screen
|
|
185
|
+
print "\e[H"
|
|
186
|
+
w = terminal_width
|
|
187
|
+
rows = terminal_height
|
|
188
|
+
|
|
189
|
+
# Header
|
|
190
|
+
puts truncate("\e[1;36m sqdash \e[0m\e[36m Job ##{@detail_job.id}\e[0m", w) + "\e[K"
|
|
191
|
+
puts "\e[90m#{'─' * w}\e[0m\e[K"
|
|
192
|
+
|
|
193
|
+
# Content area
|
|
194
|
+
content_rows = rows - 4
|
|
195
|
+
lines = build_detail_lines(@detail_job)
|
|
196
|
+
|
|
197
|
+
# Clamp scroll
|
|
198
|
+
max_scroll = [lines.length - content_rows, 0].max
|
|
199
|
+
@detail_scroll = @detail_scroll.clamp(0, max_scroll)
|
|
200
|
+
|
|
201
|
+
visible = lines[@detail_scroll, content_rows] || []
|
|
202
|
+
visible.each { |line| puts truncate(" #{line}", w) + "\e[K" }
|
|
203
|
+
|
|
204
|
+
(content_rows - visible.length).times { puts "\e[K" }
|
|
205
|
+
|
|
206
|
+
puts "\e[90m#{'─' * w}\e[0m\e[K"
|
|
207
|
+
|
|
208
|
+
if @message
|
|
209
|
+
puts " \e[1;32m#{@message}\e[0m\e[K"
|
|
210
|
+
@message = nil
|
|
211
|
+
else
|
|
212
|
+
puts truncate(" \e[90mEsc Back ↑↓ Scroll r Retry d Discard q Quit\e[0m", w) + "\e[K"
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def build_detail_lines(job)
|
|
217
|
+
lines = []
|
|
218
|
+
|
|
219
|
+
lines << "\e[1mClass:\e[0m #{job.class_name}"
|
|
220
|
+
lines << "\e[1mQueue:\e[0m #{job.queue_name}"
|
|
221
|
+
lines << "\e[1mPriority:\e[0m #{job.priority || '—'}"
|
|
222
|
+
lines << "\e[1mActive Job:\e[0m #{job.active_job_id || '—'}"
|
|
223
|
+
lines << ""
|
|
224
|
+
|
|
225
|
+
status = job_status(job)
|
|
226
|
+
lines << "\e[1mStatus:\e[0m #{status_text(status)}"
|
|
227
|
+
lines << ""
|
|
228
|
+
|
|
229
|
+
lines << "\e[1mCreated:\e[0m #{job.created_at || '—'}"
|
|
230
|
+
lines << "\e[1mScheduled:\e[0m #{job.scheduled_at || '—'}"
|
|
231
|
+
lines << "\e[1mFinished:\e[0m #{job.finished_at || '—'}"
|
|
232
|
+
lines << ""
|
|
233
|
+
|
|
234
|
+
lines << "\e[1mArguments:\e[0m"
|
|
235
|
+
if job.arguments.nil? || job.arguments.empty?
|
|
236
|
+
lines << " —"
|
|
237
|
+
else
|
|
238
|
+
begin
|
|
239
|
+
args = JSON.parse(job.arguments)
|
|
240
|
+
JSON.pretty_generate(args).each_line { |l| lines << " #{l.chomp}" }
|
|
241
|
+
rescue JSON::ParserError, TypeError
|
|
242
|
+
lines << " #{job.arguments}"
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
if status == :failed && job.failed_execution
|
|
247
|
+
lines << ""
|
|
248
|
+
lines << "\e[1;31mError:\e[0m"
|
|
249
|
+
error_text = job.failed_execution.error || "No error message"
|
|
250
|
+
error_text.each_line { |l| lines << " #{l.chomp}" }
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
lines
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
data/lib/sqdash/version.rb
CHANGED
data/lib/sqdash.rb
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "sqdash/version"
|
|
4
|
+
require_relative "sqdash/config"
|
|
4
5
|
require_relative "sqdash/database"
|
|
5
6
|
require_relative "sqdash/models/job"
|
|
6
7
|
require_relative "sqdash/models/failed_execution"
|
|
7
8
|
require_relative "sqdash/models/ready_execution"
|
|
9
|
+
require_relative "sqdash/renderer"
|
|
10
|
+
require_relative "sqdash/autocomplete"
|
|
11
|
+
require_relative "sqdash/input_handler"
|
|
8
12
|
require_relative "sqdash/cli"
|
|
9
13
|
|
|
10
14
|
module Sqdash
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sqdash
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nuha
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -34,16 +34,21 @@ executables:
|
|
|
34
34
|
extensions: []
|
|
35
35
|
extra_rdoc_files: []
|
|
36
36
|
files:
|
|
37
|
+
- ".rubocop.yml"
|
|
37
38
|
- LICENSE.txt
|
|
38
39
|
- README.md
|
|
39
40
|
- Rakefile
|
|
40
41
|
- exe/sqdash
|
|
41
42
|
- lib/sqdash.rb
|
|
43
|
+
- lib/sqdash/autocomplete.rb
|
|
42
44
|
- lib/sqdash/cli.rb
|
|
45
|
+
- lib/sqdash/config.rb
|
|
43
46
|
- lib/sqdash/database.rb
|
|
47
|
+
- lib/sqdash/input_handler.rb
|
|
44
48
|
- lib/sqdash/models/failed_execution.rb
|
|
45
49
|
- lib/sqdash/models/job.rb
|
|
46
50
|
- lib/sqdash/models/ready_execution.rb
|
|
51
|
+
- lib/sqdash/renderer.rb
|
|
47
52
|
- lib/sqdash/version.rb
|
|
48
53
|
- sig/sqdash.rbs
|
|
49
54
|
homepage: https://github.com/nuhasami/sqdash
|
|
@@ -52,6 +57,7 @@ licenses:
|
|
|
52
57
|
metadata:
|
|
53
58
|
homepage_uri: https://github.com/nuhasami/sqdash
|
|
54
59
|
source_code_uri: https://github.com/nuhasami/sqdash
|
|
60
|
+
rubygems_mfa_required: 'true'
|
|
55
61
|
post_install_message:
|
|
56
62
|
rdoc_options: []
|
|
57
63
|
require_paths:
|
|
@@ -60,7 +66,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
60
66
|
requirements:
|
|
61
67
|
- - ">="
|
|
62
68
|
- !ruby/object:Gem::Version
|
|
63
|
-
version: 3.
|
|
69
|
+
version: 3.2.0
|
|
64
70
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
65
71
|
requirements:
|
|
66
72
|
- - ">="
|