solid_queue_tui 0.1.1 → 0.1.3
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/exe/sqtui +37 -1
- data/lib/solid_queue_tui/actions/discard_job.rb +4 -21
- data/lib/solid_queue_tui/actions/discard_scheduled_job.rb +5 -21
- data/lib/solid_queue_tui/actions/dispatch_scheduled_job.rb +4 -23
- data/lib/solid_queue_tui/actions/enqueue_recurring_task.rb +12 -0
- data/lib/solid_queue_tui/actions/retry_job.rb +14 -59
- data/lib/solid_queue_tui/actions/toggle_queue_pause.rb +19 -0
- data/lib/solid_queue_tui/application.rb +109 -21
- data/lib/solid_queue_tui/cli.rb +15 -8
- data/lib/solid_queue_tui/components/header.rb +26 -27
- data/lib/solid_queue_tui/components/help_bar.rb +1 -0
- data/lib/solid_queue_tui/components/job_table.rb +13 -2
- data/lib/solid_queue_tui/data/failed_query.rb +37 -91
- data/lib/solid_queue_tui/data/jobs_query.rb +119 -121
- data/lib/solid_queue_tui/data/processes_query.rb +32 -33
- data/lib/solid_queue_tui/data/queues_query.rb +6 -15
- data/lib/solid_queue_tui/data/recurring_tasks_query.rb +36 -0
- data/lib/solid_queue_tui/data/stats.rb +9 -27
- data/lib/solid_queue_tui/formatting_helpers.rb +63 -0
- data/lib/solid_queue_tui/railtie.rb +9 -0
- data/lib/solid_queue_tui/version.rb +1 -1
- data/lib/solid_queue_tui/views/blocked_view.rb +85 -74
- data/lib/solid_queue_tui/views/concerns/confirmable.rb +53 -0
- data/lib/solid_queue_tui/views/concerns/filterable.rb +128 -0
- data/lib/solid_queue_tui/views/concerns/paginatable.rb +79 -0
- data/lib/solid_queue_tui/views/dashboard_view.rb +4 -5
- data/lib/solid_queue_tui/views/failed_view.rb +65 -179
- data/lib/solid_queue_tui/views/finished_view.rb +33 -114
- data/lib/solid_queue_tui/views/in_progress_view.rb +85 -69
- data/lib/solid_queue_tui/views/job_detail_view.rb +179 -31
- data/lib/solid_queue_tui/views/processes_view.rb +2 -24
- data/lib/solid_queue_tui/views/queues_view.rb +250 -30
- data/lib/solid_queue_tui/views/recurring_tasks_view.rb +155 -0
- data/lib/solid_queue_tui/views/scheduled_view.rb +69 -107
- data/lib/solid_queue_tui.rb +18 -4
- data/lib/tasks/solid_queue_tui.rake +8 -0
- metadata +20 -25
- data/lib/solid_queue_tui/connection.rb +0 -58
|
@@ -20,18 +20,16 @@ module SolidQueueTui
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def self.fetch
|
|
23
|
-
conn = ActiveRecord::Base.connection
|
|
24
|
-
|
|
25
23
|
new(
|
|
26
|
-
ready:
|
|
27
|
-
claimed:
|
|
28
|
-
failed:
|
|
29
|
-
scheduled:
|
|
30
|
-
blocked:
|
|
31
|
-
total_jobs:
|
|
32
|
-
completed_jobs:
|
|
33
|
-
process_count:
|
|
34
|
-
processes_by_kind:
|
|
24
|
+
ready: SolidQueue::ReadyExecution.count,
|
|
25
|
+
claimed: SolidQueue::ClaimedExecution.count,
|
|
26
|
+
failed: SolidQueue::FailedExecution.count,
|
|
27
|
+
scheduled: SolidQueue::ScheduledExecution.count,
|
|
28
|
+
blocked: SolidQueue::BlockedExecution.count,
|
|
29
|
+
total_jobs: SolidQueue::Job.count,
|
|
30
|
+
completed_jobs: SolidQueue::Job.finished.count,
|
|
31
|
+
process_count: SolidQueue::Process.count,
|
|
32
|
+
processes_by_kind: SolidQueue::Process.group(:kind).count
|
|
35
33
|
)
|
|
36
34
|
rescue => e
|
|
37
35
|
empty(error: e.message)
|
|
@@ -44,22 +42,6 @@ module SolidQueueTui
|
|
|
44
42
|
processes_by_kind: {}
|
|
45
43
|
)
|
|
46
44
|
end
|
|
47
|
-
|
|
48
|
-
private_class_method def self.count_table(conn, table)
|
|
49
|
-
conn.select_value("SELECT COUNT(*) FROM #{table}").to_i
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
private_class_method def self.count_where(conn, table, condition)
|
|
53
|
-
conn.select_value("SELECT COUNT(*) FROM #{table} WHERE #{condition}").to_i
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
private_class_method def self.processes_by_kind(conn)
|
|
57
|
-
rows = conn.select_rows(
|
|
58
|
-
"SELECT kind, COUNT(*) FROM solid_queue_processes GROUP BY kind"
|
|
59
|
-
)
|
|
60
|
-
rows.to_h { |kind, count| [kind, count.to_i] }
|
|
61
|
-
end
|
|
62
|
-
|
|
63
45
|
end
|
|
64
46
|
end
|
|
65
47
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueTui
|
|
4
|
+
module FormattingHelpers
|
|
5
|
+
def time_ago(time)
|
|
6
|
+
return "n/a" unless time
|
|
7
|
+
seconds = (Time.now.utc - time).to_i
|
|
8
|
+
case seconds
|
|
9
|
+
when 0..59 then "#{seconds}s ago"
|
|
10
|
+
when 60..3599 then "#{seconds / 60}m ago"
|
|
11
|
+
when 3600..86399 then "#{seconds / 3600}h ago"
|
|
12
|
+
else "#{seconds / 86400}d ago"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def format_time(time)
|
|
17
|
+
return "n/a" unless time
|
|
18
|
+
time.strftime("%Y-%m-%d %H:%M:%S")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def format_duration(seconds)
|
|
22
|
+
return "n/a" unless seconds
|
|
23
|
+
seconds = seconds.to_i
|
|
24
|
+
if seconds < 1
|
|
25
|
+
"<1s"
|
|
26
|
+
elsif seconds < 60
|
|
27
|
+
"#{seconds}s"
|
|
28
|
+
elsif seconds < 3600
|
|
29
|
+
"#{seconds / 60}m #{seconds % 60}s"
|
|
30
|
+
elsif seconds < 86400
|
|
31
|
+
"#{seconds / 3600}h #{(seconds % 3600) / 60}m"
|
|
32
|
+
else
|
|
33
|
+
"#{seconds / 86400}d #{(seconds % 86400) / 3600}h"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def format_number(n)
|
|
38
|
+
return "0" if n.nil? || n == 0
|
|
39
|
+
n.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def truncate(str, max)
|
|
43
|
+
return "" unless str
|
|
44
|
+
str.length > max ? "#{str[0...max - 3]}..." : str
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def humanize_duration(seconds)
|
|
48
|
+
case seconds.abs
|
|
49
|
+
when 0..59 then "#{seconds.abs}s"
|
|
50
|
+
when 60..3599 then "#{seconds.abs / 60}m"
|
|
51
|
+
when 3600..86399 then "#{seconds.abs / 3600}h"
|
|
52
|
+
else "#{seconds.abs / 86400}d"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def time_until(time)
|
|
57
|
+
return "n/a" unless time
|
|
58
|
+
seconds = (time - Time.now.utc).to_i
|
|
59
|
+
return "now" if seconds <= 0
|
|
60
|
+
"in #{humanize_duration(seconds)}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -3,21 +3,95 @@
|
|
|
3
3
|
module SolidQueueTui
|
|
4
4
|
module Views
|
|
5
5
|
class BlockedView
|
|
6
|
+
include Filterable
|
|
7
|
+
include Paginatable
|
|
8
|
+
include FormattingHelpers
|
|
9
|
+
|
|
6
10
|
def initialize(tui)
|
|
7
11
|
@tui = tui
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@selected_row = 0
|
|
11
|
-
@jobs = []
|
|
12
|
+
init_pagination
|
|
13
|
+
init_filter
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def update(jobs:)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
update_items(jobs)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def append(jobs:)
|
|
21
|
+
append_items(jobs)
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
def render(frame, area)
|
|
25
|
+
if filter_mode?
|
|
26
|
+
filter_area, content_area = @tui.layout_split(
|
|
27
|
+
area,
|
|
28
|
+
direction: :vertical,
|
|
29
|
+
constraints: [
|
|
30
|
+
@tui.constraint_length(3),
|
|
31
|
+
@tui.constraint_fill(1)
|
|
32
|
+
]
|
|
33
|
+
)
|
|
34
|
+
render_filter_input(frame, filter_area)
|
|
35
|
+
render_table(frame, content_area)
|
|
36
|
+
else
|
|
37
|
+
render_table(frame, area)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def handle_input(event)
|
|
42
|
+
if filter_mode?
|
|
43
|
+
handle_filter_input(event)
|
|
44
|
+
else
|
|
45
|
+
handle_normal_input(event)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def bindings
|
|
50
|
+
if filter_mode?
|
|
51
|
+
filter_bindings
|
|
52
|
+
else
|
|
53
|
+
[
|
|
54
|
+
{ key: "j/k", action: "Navigate" },
|
|
55
|
+
{ key: "Enter", action: "Detail" },
|
|
56
|
+
{ key: "/", action: "Filter" },
|
|
57
|
+
{ key: "G/g", action: "Bottom/Top" }
|
|
58
|
+
]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def capturing_input?
|
|
63
|
+
filter_mode?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def breadcrumb
|
|
67
|
+
@filters.empty? ? "blocked" : "blocked:filtered"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def handle_normal_input(event)
|
|
73
|
+
case event
|
|
74
|
+
in { type: :key, code: "j" } | { type: :key, code: "up" }
|
|
75
|
+
move_selection(-1)
|
|
76
|
+
in { type: :key, code: "k" } | { type: :key, code: "down" }
|
|
77
|
+
result = move_selection(1)
|
|
78
|
+
return :load_more if result == :load_more
|
|
79
|
+
nil
|
|
80
|
+
in { type: :key, code: "g" }
|
|
81
|
+
jump_to_top
|
|
82
|
+
in { type: :key, code: "G" }
|
|
83
|
+
jump_to_bottom
|
|
84
|
+
in { type: :key, code: "/" }
|
|
85
|
+
enter_filter_mode
|
|
86
|
+
nil
|
|
87
|
+
in { type: :key, code: "esc" }
|
|
88
|
+
clear_filter
|
|
89
|
+
else
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def render_table(frame, area)
|
|
21
95
|
columns = [
|
|
22
96
|
{ key: :id, label: "ID", width: 8 },
|
|
23
97
|
{ key: :queue_name, label: "QUEUE", width: 14 },
|
|
@@ -28,7 +102,7 @@ module SolidQueueTui
|
|
|
28
102
|
{ key: :blocked_since, label: "BLOCKED SINCE", width: 14 }
|
|
29
103
|
]
|
|
30
104
|
|
|
31
|
-
rows =
|
|
105
|
+
rows = items.map do |job|
|
|
32
106
|
{
|
|
33
107
|
id: job.id,
|
|
34
108
|
queue_name: job.queue_name,
|
|
@@ -42,80 +116,17 @@ module SolidQueueTui
|
|
|
42
116
|
|
|
43
117
|
table = Components::JobTable.new(
|
|
44
118
|
@tui,
|
|
45
|
-
title: "Blocked",
|
|
119
|
+
title: filter_title("Blocked"),
|
|
46
120
|
columns: columns,
|
|
47
121
|
rows: rows,
|
|
48
122
|
selected_row: @selected_row,
|
|
49
|
-
|
|
123
|
+
total_count: @total_count,
|
|
124
|
+
empty_message: @filters.empty? ? "No blocked jobs" : "No blocked jobs matching filters"
|
|
50
125
|
)
|
|
51
126
|
|
|
52
127
|
table.render(frame, area, @table_state)
|
|
53
128
|
end
|
|
54
129
|
|
|
55
|
-
def handle_input(event)
|
|
56
|
-
case event
|
|
57
|
-
in { type: :key, code: "j" } | { type: :key, code: "up" }
|
|
58
|
-
move_selection(-1)
|
|
59
|
-
in { type: :key, code: "k" } | { type: :key, code: "down" }
|
|
60
|
-
move_selection(1)
|
|
61
|
-
in { type: :key, code: "g" }
|
|
62
|
-
jump_to_top
|
|
63
|
-
in { type: :key, code: "G" }
|
|
64
|
-
jump_to_bottom
|
|
65
|
-
else
|
|
66
|
-
nil
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def selected_item
|
|
71
|
-
return nil if @jobs.empty? || @selected_row >= @jobs.size
|
|
72
|
-
@jobs[@selected_row]
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def bindings
|
|
76
|
-
[
|
|
77
|
-
{ key: "j/k", action: "Navigate" },
|
|
78
|
-
{ key: "Enter", action: "Detail" },
|
|
79
|
-
{ key: "G/g", action: "Bottom/Top" }
|
|
80
|
-
]
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def breadcrumb = "blocked"
|
|
84
|
-
|
|
85
|
-
private
|
|
86
|
-
|
|
87
|
-
def move_selection(delta)
|
|
88
|
-
return if @jobs.empty?
|
|
89
|
-
@selected_row = (@selected_row + delta).clamp(0, @jobs.size - 1)
|
|
90
|
-
@table_state.select(@selected_row)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def jump_to_top
|
|
94
|
-
@selected_row = 0
|
|
95
|
-
@table_state.select(0)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def jump_to_bottom
|
|
99
|
-
return if @jobs.empty?
|
|
100
|
-
@selected_row = @jobs.size - 1
|
|
101
|
-
@table_state.select(@selected_row)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def format_time(time)
|
|
105
|
-
return "n/a" unless time
|
|
106
|
-
time.strftime("%Y-%m-%d %H:%M:%S")
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def time_ago(time)
|
|
110
|
-
return "n/a" unless time
|
|
111
|
-
seconds = (Time.now.utc - time).to_i
|
|
112
|
-
case seconds
|
|
113
|
-
when 0..59 then "#{seconds}s ago"
|
|
114
|
-
when 60..3599 then "#{seconds / 60}m ago"
|
|
115
|
-
when 3600..86399 then "#{seconds / 3600}h ago"
|
|
116
|
-
else "#{seconds / 86400}d ago"
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
130
|
end
|
|
120
131
|
end
|
|
121
132
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueTui
|
|
4
|
+
module Views
|
|
5
|
+
module Confirmable
|
|
6
|
+
def init_confirm
|
|
7
|
+
@confirm_action = nil
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def confirm_mode? = !!@confirm_action
|
|
11
|
+
|
|
12
|
+
def confirm_bindings
|
|
13
|
+
[{ key: "y", action: "Confirm" }, { key: "n/Esc", action: "Cancel" }]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def handle_confirm_input(event)
|
|
17
|
+
case event
|
|
18
|
+
in { type: :key, code: "y" }
|
|
19
|
+
action = @confirm_action
|
|
20
|
+
@confirm_action = nil
|
|
21
|
+
execute_confirm_action(action)
|
|
22
|
+
in { type: :key, code: "n" } | { type: :key, code: "esc" }
|
|
23
|
+
@confirm_action = nil
|
|
24
|
+
nil
|
|
25
|
+
else
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def render_confirm_popup(frame, area)
|
|
31
|
+
popup_area = area.centered(
|
|
32
|
+
@tui.constraint_percentage(50),
|
|
33
|
+
@tui.constraint_length(5)
|
|
34
|
+
)
|
|
35
|
+
frame.render_widget(@tui.clear(), popup_area)
|
|
36
|
+
frame.render_widget(
|
|
37
|
+
@tui.paragraph(
|
|
38
|
+
text: " #{confirm_message}",
|
|
39
|
+
style: @tui.style(fg: :yellow, modifiers: [:bold]),
|
|
40
|
+
block: @tui.block(
|
|
41
|
+
title: " Confirm ",
|
|
42
|
+
title_style: @tui.style(fg: :red, modifiers: [:bold]),
|
|
43
|
+
borders: [:all],
|
|
44
|
+
border_type: :rounded,
|
|
45
|
+
border_style: @tui.style(fg: :red)
|
|
46
|
+
)
|
|
47
|
+
),
|
|
48
|
+
popup_area
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueTui
|
|
4
|
+
module Views
|
|
5
|
+
module Filterable
|
|
6
|
+
FILTER_FIELDS = [
|
|
7
|
+
{ key: :class_name, label: "Class" },
|
|
8
|
+
{ key: :queue, label: "Queue" }
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
def init_filter
|
|
12
|
+
@filters = {}
|
|
13
|
+
@filter_mode = false
|
|
14
|
+
@filter_inputs = {}
|
|
15
|
+
@active_field = 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def filters = @filters
|
|
19
|
+
def filter_mode? = @filter_mode
|
|
20
|
+
|
|
21
|
+
def handle_filter_input(event)
|
|
22
|
+
case event
|
|
23
|
+
in { type: :key, code: "enter" }
|
|
24
|
+
@filters = @filter_inputs.reject { |_, v| v.nil? || v.empty? }
|
|
25
|
+
@filter_mode = false
|
|
26
|
+
@selected_row = 0
|
|
27
|
+
@table_state.select(0)
|
|
28
|
+
:refresh
|
|
29
|
+
in { type: :key, code: "esc" }
|
|
30
|
+
@filter_mode = false
|
|
31
|
+
@filter_inputs = @filters.dup
|
|
32
|
+
nil
|
|
33
|
+
in { type: :key, code: "tab" }
|
|
34
|
+
@active_field = (@active_field + 1) % FILTER_FIELDS.size
|
|
35
|
+
nil
|
|
36
|
+
in { type: :key, code: "back_tab" }
|
|
37
|
+
@active_field = (@active_field - 1) % FILTER_FIELDS.size
|
|
38
|
+
nil
|
|
39
|
+
in { type: :key, code: "backspace" }
|
|
40
|
+
field_key = FILTER_FIELDS[@active_field][:key]
|
|
41
|
+
current = @filter_inputs[field_key] || ""
|
|
42
|
+
@filter_inputs[field_key] = current[0...-1]
|
|
43
|
+
nil
|
|
44
|
+
in { type: :key, code: /\A.\z/ => char }
|
|
45
|
+
field_key = FILTER_FIELDS[@active_field][:key]
|
|
46
|
+
@filter_inputs[field_key] = (@filter_inputs[field_key] || "") + char
|
|
47
|
+
nil
|
|
48
|
+
else
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def enter_filter_mode
|
|
54
|
+
@filter_mode = true
|
|
55
|
+
@filter_inputs = @filters.dup
|
|
56
|
+
@active_field = 0
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def clear_filter
|
|
60
|
+
@filters = {}
|
|
61
|
+
@filter_inputs = {}
|
|
62
|
+
:refresh
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def render_filter_input(frame, area)
|
|
66
|
+
spans = []
|
|
67
|
+
|
|
68
|
+
FILTER_FIELDS.each_with_index do |field, idx|
|
|
69
|
+
active = idx == @active_field
|
|
70
|
+
value = @filter_inputs[field[:key]] || ""
|
|
71
|
+
|
|
72
|
+
spans << @tui.text_span(
|
|
73
|
+
content: " #{field[:label]}: ",
|
|
74
|
+
style: @tui.style(fg: active ? :yellow : :dark_gray, modifiers: active ? [:bold] : [])
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if active
|
|
78
|
+
spans << @tui.text_span(content: value + "\u2588", style: @tui.style(fg: :white))
|
|
79
|
+
else
|
|
80
|
+
spans << @tui.text_span(
|
|
81
|
+
content: value.empty? ? "\u2014" : value,
|
|
82
|
+
style: @tui.style(fg: :dark_gray)
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
spans << @tui.text_span(content: " ", style: @tui.style(fg: :white))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
frame.render_widget(
|
|
90
|
+
@tui.paragraph(
|
|
91
|
+
text: @tui.text_line(spans: spans),
|
|
92
|
+
block: @tui.block(
|
|
93
|
+
title: " Filters ",
|
|
94
|
+
title_style: @tui.style(fg: :yellow),
|
|
95
|
+
titles: [
|
|
96
|
+
{ content: " Tab: next field \u2502 Enter: apply \u2502 Esc: cancel ",
|
|
97
|
+
position: :top, alignment: :right }
|
|
98
|
+
],
|
|
99
|
+
borders: [:all],
|
|
100
|
+
border_type: :rounded,
|
|
101
|
+
border_style: @tui.style(fg: :cyan)
|
|
102
|
+
)
|
|
103
|
+
),
|
|
104
|
+
area
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def filter_title(base_title)
|
|
109
|
+
return base_title if @filters.empty?
|
|
110
|
+
|
|
111
|
+
parts = FILTER_FIELDS.filter_map do |field|
|
|
112
|
+
value = @filters[field[:key]]
|
|
113
|
+
"#{field[:label].downcase}: #{value}" if value && !value.empty?
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
"#{base_title} (#{parts.join(', ')})"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def filter_bindings
|
|
120
|
+
[
|
|
121
|
+
{ key: "Tab", action: "Next Field" },
|
|
122
|
+
{ key: "Enter", action: "Apply" },
|
|
123
|
+
{ key: "Esc", action: "Cancel" }
|
|
124
|
+
]
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueTui
|
|
4
|
+
module Views
|
|
5
|
+
module Paginatable
|
|
6
|
+
LOAD_THRESHOLD = 10
|
|
7
|
+
|
|
8
|
+
def init_pagination
|
|
9
|
+
@table_state = RatatuiRuby::TableState.new(nil)
|
|
10
|
+
@table_state.select(0)
|
|
11
|
+
@selected_row = 0
|
|
12
|
+
@items = []
|
|
13
|
+
@total_count = nil
|
|
14
|
+
@all_loaded = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def items = @items
|
|
18
|
+
|
|
19
|
+
def selected_item
|
|
20
|
+
return nil if @items.empty? || @selected_row >= @items.size
|
|
21
|
+
@items[@selected_row]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def total_count=(count)
|
|
25
|
+
@total_count = count
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def current_offset
|
|
29
|
+
@items.size
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def reset_pagination!
|
|
33
|
+
@items = []
|
|
34
|
+
@total_count = nil
|
|
35
|
+
@all_loaded = false
|
|
36
|
+
@selected_row = 0
|
|
37
|
+
@table_state.select(0)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def update_items(new_items)
|
|
43
|
+
@selected_row = 0 if @selected_row >= new_items.size
|
|
44
|
+
@items = new_items
|
|
45
|
+
@all_loaded = new_items.size < SolidQueueTui.page_size
|
|
46
|
+
@selected_row = @selected_row.clamp(0, [@items.size - 1, 0].max)
|
|
47
|
+
@table_state.select(@selected_row)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def append_items(more_items)
|
|
51
|
+
@items.concat(more_items)
|
|
52
|
+
@all_loaded = more_items.size < SolidQueueTui.page_size
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def needs_more?
|
|
56
|
+
!@all_loaded && @selected_row >= @items.size - LOAD_THRESHOLD
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def move_selection(delta)
|
|
60
|
+
return if @items.empty?
|
|
61
|
+
@selected_row = (@selected_row + delta).clamp(0, @items.size - 1)
|
|
62
|
+
@table_state.select(@selected_row)
|
|
63
|
+
:load_more if needs_more?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def jump_to_top
|
|
67
|
+
@selected_row = 0
|
|
68
|
+
@table_state.select(0)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def jump_to_bottom
|
|
72
|
+
return if @items.empty?
|
|
73
|
+
@selected_row = @items.size - 1
|
|
74
|
+
@table_state.select(@selected_row)
|
|
75
|
+
:load_more if needs_more?
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module SolidQueueTui
|
|
4
4
|
module Views
|
|
5
5
|
class DashboardView
|
|
6
|
+
include FormattingHelpers
|
|
7
|
+
|
|
6
8
|
def initialize(tui)
|
|
7
9
|
@tui = tui
|
|
8
10
|
@selected_row = 0
|
|
@@ -36,7 +38,8 @@ module SolidQueueTui
|
|
|
36
38
|
|
|
37
39
|
def bindings
|
|
38
40
|
[
|
|
39
|
-
{ key: "Tab", action: "Next View" }
|
|
41
|
+
{ key: "Tab", action: "Next View" },
|
|
42
|
+
{ key: "Shift Tab", action: "Prev View" },
|
|
40
43
|
]
|
|
41
44
|
end
|
|
42
45
|
|
|
@@ -178,10 +181,6 @@ module SolidQueueTui
|
|
|
178
181
|
])
|
|
179
182
|
end
|
|
180
183
|
|
|
181
|
-
def format_number(n)
|
|
182
|
-
return "0" if n.nil? || n == 0
|
|
183
|
-
n.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
184
|
-
end
|
|
185
184
|
end
|
|
186
185
|
end
|
|
187
186
|
end
|