solid_queue_dashboard 0.0.1 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +5 -1
  4. data/Procfile.dev +2 -0
  5. data/README.md +73 -21
  6. data/app/assets/javascripts/solid_queue_dashboard/alpine.js +5 -0
  7. data/app/assets/javascripts/solid_queue_dashboard/application.js +46 -0
  8. data/app/assets/stylesheets/solid_queue_dashboard/application.css +2049 -0
  9. data/app/assets/stylesheets/solid_queue_dashboard/tailwind.css +554 -0
  10. data/app/controllers/solid_queue_dashboard/appearance_controller.rb +8 -0
  11. data/app/controllers/solid_queue_dashboard/application_controller.rb +7 -0
  12. data/app/controllers/solid_queue_dashboard/dashboard_controller.rb +8 -0
  13. data/app/controllers/solid_queue_dashboard/jobs_controller.rb +42 -0
  14. data/app/controllers/solid_queue_dashboard/processes_controller.rb +28 -0
  15. data/app/controllers/solid_queue_dashboard/recurring_tasks_controller.rb +30 -0
  16. data/app/helpers/solid_queue_dashboard/appearance_helper.rb +7 -0
  17. data/app/helpers/solid_queue_dashboard/application_helper.rb +7 -0
  18. data/app/helpers/solid_queue_dashboard/icons_helper.rb +231 -0
  19. data/app/helpers/solid_queue_dashboard/jobs_helper.rb +51 -0
  20. data/app/helpers/solid_queue_dashboard/pagination_helper.rb +38 -0
  21. data/app/helpers/solid_queue_dashboard/processes_helper.rb +35 -0
  22. data/app/helpers/solid_queue_dashboard/recurring_tasks_helper.rb +33 -0
  23. data/app/views/layouts/solid_queue_dashboard/application.html.erb +21 -0
  24. data/app/views/solid_queue_dashboard/application/_flash_messages.html.erb +38 -0
  25. data/app/views/solid_queue_dashboard/application/_footer.html.erb +11 -0
  26. data/app/views/solid_queue_dashboard/application/_navbar.html.erb +50 -0
  27. data/app/views/solid_queue_dashboard/application/_pagination.html.erb +19 -0
  28. data/app/views/solid_queue_dashboard/dashboard/index.html.erb +41 -0
  29. data/app/views/solid_queue_dashboard/jobs/_filters.html.erb +87 -0
  30. data/app/views/solid_queue_dashboard/jobs/_table.html.erb +19 -0
  31. data/app/views/solid_queue_dashboard/jobs/_table_row.html.erb +82 -0
  32. data/app/views/solid_queue_dashboard/jobs/index.html.erb +57 -0
  33. data/app/views/solid_queue_dashboard/jobs/show.html.erb +192 -0
  34. data/app/views/solid_queue_dashboard/processes/_filters.html.erb +64 -0
  35. data/app/views/solid_queue_dashboard/processes/_table.html.erb +18 -0
  36. data/app/views/solid_queue_dashboard/processes/_table_row.html.erb +32 -0
  37. data/app/views/solid_queue_dashboard/processes/index.html.erb +27 -0
  38. data/app/views/solid_queue_dashboard/processes/show.html.erb +79 -0
  39. data/app/views/solid_queue_dashboard/recurring_tasks/_filters.html.erb +26 -0
  40. data/app/views/solid_queue_dashboard/recurring_tasks/_table.html.erb +19 -0
  41. data/app/views/solid_queue_dashboard/recurring_tasks/_table_row.html.erb +31 -0
  42. data/app/views/solid_queue_dashboard/recurring_tasks/index.html.erb +21 -0
  43. data/app/views/solid_queue_dashboard/recurring_tasks/show.html.erb +129 -0
  44. data/bun.lockb +0 -0
  45. data/config/routes.rb +18 -0
  46. data/lib/solid_queue_dashboard/configuration.rb +17 -0
  47. data/lib/solid_queue_dashboard/decorators/job_decorator.rb +63 -0
  48. data/lib/solid_queue_dashboard/decorators/jobs_decorator.rb +74 -0
  49. data/lib/solid_queue_dashboard/decorators/process_decorator.rb +13 -0
  50. data/lib/solid_queue_dashboard/decorators/processes_decorator.rb +15 -0
  51. data/lib/solid_queue_dashboard/decorators/recurring_task_decorator.rb +22 -0
  52. data/lib/solid_queue_dashboard/decorators/recurring_tasks_decorator.rb +26 -0
  53. data/lib/solid_queue_dashboard/engine.rb +13 -0
  54. data/lib/solid_queue_dashboard/job.rb +26 -0
  55. data/lib/solid_queue_dashboard/process.rb +24 -0
  56. data/lib/solid_queue_dashboard/recurring_task.rb +14 -0
  57. data/lib/solid_queue_dashboard/version.rb +1 -1
  58. data/lib/solid_queue_dashboard.rb +39 -1
  59. data/package.json +13 -0
  60. data/tailwind.config.js +83 -0
  61. metadata +73 -4
@@ -0,0 +1,231 @@
1
+ module SolidQueueDashboard
2
+ module IconsHelper
3
+ def icon_refresh_cw(options = {})
4
+ svg_options = {
5
+ xmlns: "http://www.w3.org/2000/svg",
6
+ width: "24",
7
+ height: "24",
8
+ viewBox: "0 0 24 24",
9
+ fill: "none",
10
+ stroke: "currentColor",
11
+ "stroke-width": "2",
12
+ "stroke-linecap": "round",
13
+ "stroke-linejoin": "round"
14
+ }.merge(options)
15
+
16
+ tag.svg(**svg_options) do
17
+ concat tag.path(d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8")
18
+ concat tag.path(d: "M21 3v5h-5")
19
+ concat tag.path(d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16")
20
+ concat tag.path(d: "M8 16H3v5")
21
+ end
22
+ end
23
+
24
+ def icon_triangle_alert(options = {})
25
+ svg_options = {
26
+ xmlns: "http://www.w3.org/2000/svg",
27
+ width: "24",
28
+ height: "24",
29
+ viewBox: "0 0 24 24",
30
+ fill: "none",
31
+ stroke: "currentColor",
32
+ "stroke-width": "2",
33
+ "stroke-linecap": "round",
34
+ "stroke-linejoin": "round"
35
+ }.merge(options)
36
+
37
+ tag.svg(**svg_options) do
38
+ concat tag.path(d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3")
39
+ concat tag.path(d: "M12 9v4")
40
+ concat tag.path(d: "M12 17h.01")
41
+ end
42
+ end
43
+
44
+ def icon_server(options = {})
45
+ svg_options = {
46
+ xmlns: "http://www.w3.org/2000/svg",
47
+ width: "24",
48
+ height: "24",
49
+ viewBox: "0 0 24 24",
50
+ fill: "none",
51
+ stroke: "currentColor",
52
+ "stroke-width": "2",
53
+ "stroke-linecap": "round",
54
+ "stroke-linejoin": "round"
55
+ }.merge(options)
56
+
57
+ tag.svg(**svg_options) do
58
+ concat tag.rect(width: "20", height: "8", x: "2", y: "2", rx: "2", ry: "2")
59
+ concat tag.rect(width: "20", height: "8", x: "2", y: "14", rx: "2", ry: "2")
60
+ concat tag.line(x1: "6", x2: "6.01", y1: "6", y2: "6")
61
+ concat tag.line(x1: "6", x2: "6.01", y1: "18", y2: "18")
62
+ end
63
+ end
64
+
65
+ def icon_layout_dashboard(options = {})
66
+ svg_options = {
67
+ xmlns: "http://www.w3.org/2000/svg",
68
+ width: "24",
69
+ height: "24",
70
+ viewBox: "0 0 24 24",
71
+ fill: "none",
72
+ stroke: "currentColor",
73
+ "stroke-width": "2",
74
+ "stroke-linecap": "round",
75
+ "stroke-linejoin": "round"
76
+ }.merge(options)
77
+
78
+ tag.svg(**svg_options) do
79
+ concat tag.rect(width: "7", height: "9", x: "3", y: "3", rx: "1")
80
+ concat tag.rect(width: "7", height: "5", x: "14", y: "3", rx: "1")
81
+ concat tag.rect(width: "7", height: "9", x: "14", y: "12", rx: "1")
82
+ concat tag.rect(width: "7", height: "5", x: "3", y: "16", rx: "1")
83
+ end
84
+ end
85
+
86
+ def icon_logs(options = {})
87
+ svg_options = {
88
+ xmlns: "http://www.w3.org/2000/svg",
89
+ width: "24",
90
+ height: "24",
91
+ viewBox: "0 0 24 24",
92
+ fill: "none",
93
+ stroke: "currentColor",
94
+ "stroke-width": "2",
95
+ "stroke-linecap": "round",
96
+ "stroke-linejoin": "round"
97
+ }.merge(options)
98
+
99
+ tag.svg(**svg_options) do
100
+ concat tag.path(d: "M13 12h8")
101
+ concat tag.path(d: "M13 18h8")
102
+ concat tag.path(d: "M13 6h8")
103
+ concat tag.path(d: "M3 12h1")
104
+ concat tag.path(d: "M3 18h1")
105
+ concat tag.path(d: "M3 6h1")
106
+ concat tag.path(d: "M8 12h1")
107
+ concat tag.path(d: "M8 18h1")
108
+ concat tag.path(d: "M8 6h1")
109
+ end
110
+ end
111
+
112
+ def icon_clock(options = {})
113
+ svg_options = {
114
+ xmlns: "http://www.w3.org/2000/svg",
115
+ width: "24",
116
+ height: "24",
117
+ viewBox: "0 0 24 24",
118
+ fill: "none",
119
+ stroke: "currentColor",
120
+ "stroke-width": "2",
121
+ "stroke-linecap": "round",
122
+ "stroke-linejoin": "round"
123
+ }.merge(options)
124
+
125
+ tag.svg(**svg_options) do
126
+ concat tag.circle(cx: "12", cy: "12", r: "10")
127
+ concat tag.polyline(points: "12 6 12 12 16 14")
128
+ end
129
+ end
130
+
131
+ def icon_github(options = {})
132
+ svg_options = {
133
+ xmlns: "http://www.w3.org/2000/svg",
134
+ width: "24",
135
+ height: "24",
136
+ viewBox: "0 0 24 24",
137
+ fill: "none",
138
+ stroke: "currentColor",
139
+ "stroke-width": "2",
140
+ "stroke-linecap": "round",
141
+ "stroke-linejoin": "round"
142
+ }.merge(options)
143
+
144
+ tag.svg(**svg_options) do
145
+ concat tag.path(d: "M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4")
146
+ concat tag.path(d: "M9 18c-4.51 2-5-2-7-2")
147
+ end
148
+ end
149
+
150
+ def icon_x(options = {})
151
+ svg_options = {
152
+ xmlns: "http://www.w3.org/2000/svg",
153
+ width: "24",
154
+ height: "24",
155
+ viewBox: "0 0 24 24",
156
+ fill: "none",
157
+ stroke: "currentColor",
158
+ "stroke-width": "2",
159
+ "stroke-linecap": "round",
160
+ "stroke-linejoin": "round"
161
+ }.merge(options)
162
+
163
+ tag.svg(**svg_options) do
164
+ concat tag.path(d: "M18 6 6 18")
165
+ concat tag.path(d: "m6 6 12 12")
166
+ end
167
+ end
168
+
169
+ def icon_moon(options = {})
170
+ svg_options = {
171
+ xmlns: "http://www.w3.org/2000/svg",
172
+ width: "24",
173
+ height: "24",
174
+ viewBox: "0 0 24 24",
175
+ fill: "none",
176
+ stroke: "currentColor",
177
+ "stroke-width": "2",
178
+ "stroke-linecap": "round",
179
+ "stroke-linejoin": "round"
180
+ }.merge(options)
181
+
182
+ tag.svg(**svg_options) do
183
+ concat tag.path(d: "M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z")
184
+ end
185
+ end
186
+
187
+ def icon_sun(options = {})
188
+ svg_options = {
189
+ xmlns: "http://www.w3.org/2000/svg",
190
+ width: "24",
191
+ height: "24",
192
+ viewBox: "0 0 24 24",
193
+ fill: "none",
194
+ stroke: "currentColor",
195
+ "stroke-width": "2",
196
+ "stroke-linecap": "round",
197
+ "stroke-linejoin": "round"
198
+ }.merge(options)
199
+
200
+ tag.svg(**svg_options) do
201
+ concat tag.circle(cx: "12", cy: "12", r: "4")
202
+ concat tag.path(d: "M12 2v2")
203
+ concat tag.path(d: "M12 20v2")
204
+ concat tag.path(d: "m4.93 4.93 1.41 1.41")
205
+ concat tag.path(d: "m17.66 17.66 1.41 1.41")
206
+ concat tag.path(d: "M2 12h2")
207
+ concat tag.path(d: "M20 12h2")
208
+ concat tag.path(d: "m6.34 17.66-1.41 1.41")
209
+ concat tag.path(d: "m19.07 4.93-1.41 1.41")
210
+ end
211
+ end
212
+
213
+ def icon_play(options = {})
214
+ svg_options = {
215
+ xmlns: "http://www.w3.org/2000/svg",
216
+ width: "24",
217
+ height: "24",
218
+ viewBox: "0 0 24 24",
219
+ fill: "none",
220
+ stroke: "currentColor",
221
+ "stroke-width": "2",
222
+ "stroke-linecap": "round",
223
+ "stroke-linejoin": "round"
224
+ }.merge(options)
225
+
226
+ tag.svg(**svg_options) do
227
+ concat tag.polygon(points: "6 3 20 12 6 21 6 3")
228
+ end
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,51 @@
1
+ module SolidQueueDashboard
2
+ module JobsHelper
3
+ def job_status_circle(status, options = {})
4
+ options[:class] = [ "circle", job_status_circle_class(status), options[:class] ].compact_blank.join(" ")
5
+ tag.span("", **options)
6
+ end
7
+
8
+ def job_status_circle_class(status)
9
+ {
10
+ "green": "circle-green",
11
+ "amber": "circle-amber",
12
+ "red": "circle-red",
13
+ "blue": "circle-blue",
14
+ "zinc": "circle-zinc"
15
+ }[Job::STATUS_COLORS[status]&.to_sym || :zinc]
16
+ end
17
+
18
+ def job_status_badge(status, options = {})
19
+ options[:class] = [ "badge", job_status_badge_class(status), options[:class] ].compact_blank.join(" ")
20
+ tag.span(status.to_s.titleize, **options)
21
+ end
22
+
23
+ def job_status_badge_class(status)
24
+ {
25
+ "green": "badge-green",
26
+ "amber": "badge-amber",
27
+ "red": "badge-red",
28
+ "blue": "badge-blue",
29
+ "zinc": "badge-zinc"
30
+ }[Job::STATUS_COLORS[status]&.to_sym || :zinc]
31
+ end
32
+
33
+ def format_failure_rate(failure_rate, options = {})
34
+ badge_variant = case failure_rate
35
+ when 0..1
36
+ "badge-emerald"
37
+ when 1..5
38
+ "badge-amber"
39
+ else
40
+ "badge-red"
41
+ end
42
+
43
+ options[:class] = [ "badge", badge_variant, options[:class] ].compact_blank.join(" ")
44
+ tag.span(number_to_percentage(failure_rate, precision: 2, strip_insignificant_zeros: true), **options)
45
+ end
46
+
47
+ def any_jobs_filters?
48
+ params[:class_name].present? || params[:status].present? || params[:queue_name].present?
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,38 @@
1
+ module SolidQueueDashboard
2
+ module PaginationHelper
3
+ def paginate(scope, page:, per_page:)
4
+ page = [ page.to_i, 1 ].max
5
+ per_page = per_page.zero? ? 25 : [ per_page.to_i, 1 ].max
6
+
7
+ offset = (page - 1) * per_page
8
+
9
+ records = scope.offset(offset).limit(per_page)
10
+ total_count = scope.count
11
+ total_pages = (total_count.to_f / per_page).ceil
12
+
13
+ {
14
+ records: records,
15
+ current_page: page,
16
+ per_page: per_page,
17
+ total_pages: total_pages,
18
+ total_count: total_count
19
+ }
20
+ end
21
+
22
+ def page_range(current_page, total_pages, window: 2)
23
+ if total_pages <= 7
24
+ (1..total_pages).to_a
25
+ else
26
+ [
27
+ 1,
28
+ (current_page - window..current_page + window).to_a,
29
+ total_pages
30
+ ].flatten.uniq.sort.reject { |p| p < 1 || p > total_pages }.tap do |range|
31
+ range.each_cons(2) do |a, b|
32
+ range.insert(range.index(b), :gap) if b - a > 1
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,35 @@
1
+ module SolidQueueDashboard
2
+ module ProcessesHelper
3
+ def process_kind_circle(kind, options = {})
4
+ options[:class] = [ "circle", process_kind_circle_class(kind), options[:class] ].compact_blank.join(" ")
5
+ tag.span("", **options)
6
+ end
7
+
8
+ def process_kind_circle_class(kind)
9
+ {
10
+ "blue": "circle-blue",
11
+ "green": "circle-green",
12
+ "yellow": "circle-yellow",
13
+ "purple": "circle-purple"
14
+ }[Process::KIND_COLORS[kind]&.to_sym || :zinc]
15
+ end
16
+
17
+ def process_kind_badge(kind, options = {})
18
+ options[:class] = [ "badge", process_kind_badge_class(kind), options[:class] ].compact_blank.join(" ")
19
+ tag.span(kind.to_s.titleize, **options)
20
+ end
21
+
22
+ def process_kind_badge_class(kind)
23
+ {
24
+ "blue": "badge-blue",
25
+ "green": "badge-green",
26
+ "yellow": "badge-yellow",
27
+ "purple": "badge-purple"
28
+ }[Process::KIND_COLORS[kind]&.to_sym || :zinc]
29
+ end
30
+
31
+ def any_processes_filters?
32
+ params[:kind].present? || params[:hostname].present?
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,33 @@
1
+ module SolidQueueDashboard
2
+ module RecurringTasksHelper
3
+ def recurring_task_circle(type, options = {})
4
+ options[:class] = [ "circle", recurring_task_circle_class(type), options[:class] ].compact_blank.join(" ")
5
+ tag.span("", **options)
6
+ end
7
+
8
+ def recurring_task_circle_class(type)
9
+ {
10
+ "amber": "circle-amber",
11
+ "sky": "circle-sky",
12
+ "zinc": "circle-zinc"
13
+ }[RecurringTask::TYPE_COLORS[type]&.to_sym || :zinc]
14
+ end
15
+
16
+ def recurring_task_type_badge(type, options = {})
17
+ options[:class] = [ "badge", recurring_task_type_badge_class(type), options[:class] ].compact_blank.join(" ")
18
+ tag.span(type.to_s.titleize, **options)
19
+ end
20
+
21
+ def recurring_task_type_badge_class(type)
22
+ {
23
+ "amber": "badge-amber",
24
+ "sky": "badge-sky",
25
+ "zinc": "badge-zinc"
26
+ }[RecurringTask::TYPE_COLORS[type]&.to_sym || :zinc]
27
+ end
28
+
29
+ def any_recurring_tasks_filters?
30
+ params[:class_name].present? || params[:queue_name].present?
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ <!DOCTYPE html>
2
+ <html class="<%= dark_mode? ? "dark" : "" %>" lang="en">
3
+ <head>
4
+ <title>Solid Queue Dashboard</title>
5
+
6
+ <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
8
+
9
+ <%= stylesheet_link_tag "solid_queue_dashboard/application", media: "all" %>
10
+ <%= javascript_include_tag "solid_queue_dashboard/application" %>
11
+ <%= javascript_include_tag "solid_queue_dashboard/alpine", defer: true %>
12
+ </head>
13
+ <body class="pb-[15vh] sm:pb-[25vh]">
14
+ <div class="max-w-[1920px] mx-auto px-4 sm:px-6 lg:px-8">
15
+ <%= render 'navbar' %>
16
+ <%= render 'flash_messages' %>
17
+ <%= yield %>
18
+ <%= render 'footer' %>
19
+ </div>
20
+ </body>
21
+ </html>
@@ -0,0 +1,38 @@
1
+ <% if flash.any? %>
2
+ <div class="space-y-4 mb-8">
3
+ <% flash.each do |type, message| %>
4
+ <% alert_class = case type
5
+ when "notice" then "alert alert-blue"
6
+ when "success" then "alert alert-green"
7
+ when "error" then "alert alert-red"
8
+ when "alert" then "alert alert-yellow"
9
+ else "alert alert-default"
10
+ end %>
11
+
12
+ <div class="<%= alert_class %>" role="alert">
13
+ <% if type == "notice" %>
14
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
15
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
16
+ </svg>
17
+ <% elsif type == "success" %>
18
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
19
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
20
+ </svg>
21
+ <% elsif type == "error" %>
22
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
23
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
24
+ </svg>
25
+ <% elsif type == "alert" %>
26
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
27
+ <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
28
+ </svg>
29
+ <% end %>
30
+
31
+ <h3 class="alert-title"><%= type.capitalize %></h3>
32
+ <div class="alert-description">
33
+ <%= message %>
34
+ </div>
35
+ </div>
36
+ <% end %>
37
+ </div>
38
+ <% end %>
@@ -0,0 +1,11 @@
1
+ <footer class="mt-6">
2
+ <p class="text-xs text-center text-muted-foreground">
3
+ <a
4
+ href="https://github.com/akodkod/solid_queue_dashboard"
5
+ target="_blank"
6
+ class="hover:underline hover:text-foreground"
7
+ >
8
+ <%= icon_github class: "inline-block size-3 mr-0.5 -translate-y-px" %>GitHub
9
+ </a>
10
+ </p>
11
+ </footer>
@@ -0,0 +1,50 @@
1
+ <nav class="navbar mb-6">
2
+ <%= link_to root_path, class: "inline-flex items-center gap-0.5 text-xl font-bold tracking-tight translate-y-px" do %>
3
+ <span class="circle circle-blue"></span>
4
+ <span class="circle circle-green"></span>
5
+ <span class="circle circle-yellow"></span>
6
+ <span class="circle circle-red"></span>
7
+ <span class="ml-1.5 -translate-y-px">Solid Queue</span>
8
+ <% end %>
9
+
10
+ <div class="navbar-section">
11
+ <%= link_to root_path, class: "navbar-item #{current_page?(controller: 'dashboard', action: 'index') ? 'navbar-item-current' : 'navbar-item-default'}" do %>
12
+ <%= icon_layout_dashboard class: "size-4 mr-1.5" %>
13
+ Dashboard
14
+ <% end %>
15
+
16
+ <%= link_to jobs_path, class: "navbar-item #{current_page?(jobs_path) ? 'navbar-item-current' : 'navbar-item-default'}" do %>
17
+ <%= icon_logs class: "size-4 mr-1.5" %>
18
+ Jobs
19
+ <% end %>
20
+
21
+ <%= link_to processes_path, class: "navbar-item #{current_page?(processes_path) ? 'navbar-item-current' : 'navbar-item-default'}" do %>
22
+ <%= icon_server class: "size-4 mr-1.5" %>
23
+ Processes
24
+ <% end %>
25
+
26
+ <%= link_to recurring_tasks_path, class: "navbar-item #{current_page?(recurring_tasks_path) ? 'navbar-item-current' : 'navbar-item-default'}" do %>
27
+ <%= icon_clock class: "size-4 mr-1.5" %>
28
+ Recurring Tasks
29
+ <% end %>
30
+ </div>
31
+
32
+ <div class="ml-auto">
33
+ <div>
34
+
35
+ </div>
36
+
37
+ <%= form_with url: toggle_appearance_path, method: :post do |f| %>
38
+ <button
39
+ type="submit"
40
+ class="btn btn-icon btn-secondary"
41
+ >
42
+ <% if dark_mode?%>
43
+ <%= icon_sun class: "size-4" %>
44
+ <% else %>
45
+ <%= icon_moon class: "size-4" %>
46
+ <% end %>
47
+ </button>
48
+ <% end %>
49
+ </div>
50
+ </nav>
@@ -0,0 +1,19 @@
1
+ <% if total_pages > 1 %>
2
+ <nav class="pagination" role="navigation" aria-label="pagination">
3
+ <div class="pagination-content">
4
+ <%= link_to raw('&laquo; Previous'), url_for(page: current_page - 1, per_page: per_page), class: "pagination-link pagination-previous #{current_page > 1 ? 'pagination-link-active' : 'pagination-link-inactive'}", rel: "prev" unless current_page == 1 %>
5
+
6
+ <% page_range(current_page, total_pages).each do |page| %>
7
+ <% if page.is_a?(Integer) %>
8
+ <span class="pagination-item">
9
+ <%= link_to page, url_for(page: page, per_page: per_page), class: "pagination-link pagination-link-default #{page == current_page ? 'pagination-link-active' : 'pagination-link-inactive'}" %>
10
+ </span>
11
+ <% else %>
12
+ <span class="pagination-ellipsis">&hellip;</span>
13
+ <% end %>
14
+ <% end %>
15
+
16
+ <%= link_to raw('Next &raquo;'), url_for(page: current_page + 1, per_page: per_page), class: "pagination-link pagination-next #{current_page < total_pages ? 'pagination-link-active' : 'pagination-link-inactive'}", rel: "next" unless current_page == total_pages %>
17
+ </div>
18
+ </nav>
19
+ <% end %>
@@ -0,0 +1,41 @@
1
+ <div class="grid grid-cols-5 gap-4">
2
+ <% SolidQueueDashboard::Job::STATUSES.each do |status| %>
3
+ <div class="card" data-href="<%= jobs_path(status:) %>">
4
+ <div class="card-content pt-5">
5
+ <h4 class="font-medium">
6
+ <%= status.to_s.titleize %>
7
+ <span class="ml-0.5 -translate-y-px circle <%= job_status_circle_class(status) %>"></span>
8
+ </h4>
9
+ <p class="text-4xl font-bold mt-1 text-black dark:text-white">
10
+ <%= @jobs.with_status(status).count %>
11
+ </p>
12
+ </div>
13
+ </div>
14
+ <% end %>
15
+ </div>
16
+
17
+ <div class="card mt-4">
18
+ <div class="card-header border-b">
19
+ <h3 class="card-title">Failure Rate</h3>
20
+ </div>
21
+ <div class="card-content !p-0">
22
+ <div class="table-wrapper">
23
+ <table class="table">
24
+ <thead class="table-header">
25
+ <tr class="table-row">
26
+ <th class="table-head">Job</th>
27
+ <th class="table-head">Failure Rate</th>
28
+ </tr>
29
+ </thead>
30
+ <tbody class="table-body">
31
+ <% @job_class_names.each do |class_name| %>
32
+ <tr class="table-row" data-href="<%= jobs_path(class_name:, status: :failed) %>">
33
+ <td class="table-cell font-medium"><%= class_name.titleize %></td>
34
+ <td class="table-cell font-medium"><%= format_failure_rate(SolidQueueDashboard.decorate(SolidQueue::Job.where(class_name:)).failure_rate) %></td>
35
+ </tr>
36
+ <% end %>
37
+ </tbody>
38
+ </table>
39
+ </div>
40
+ </div>
41
+ </div>
@@ -0,0 +1,87 @@
1
+ <%= form_with url: jobs_path, method: :get, class: "space-y-3" do |form| %>
2
+ <div>
3
+ <label class="label">Status</label>
4
+ <div class="flex flex-wrap gap-1 mt-1">
5
+ <% SolidQueueDashboard::Job::STATUSES.each do |status| %>
6
+ <%= button_tag type: :submit,
7
+ name: :status,
8
+ value: status,
9
+ class: "px-2 badge #{params[:status] == status.to_s ? 'badge-primary' : 'badge-outline'}" do
10
+ %>
11
+ <span class="circle <%= job_status_circle_class(status) %>"></span>
12
+ <%= status.to_s.titleize %>
13
+ <span class="opacity-50 -ml-0.5">
14
+ <%= SolidQueueDashboard.decorate(SolidQueue::Job.all).with_status(status).count %>
15
+ </span>
16
+ <% end %>
17
+ <% end %>
18
+
19
+ <% if params[:status].present? %>
20
+ <%= button_tag type: :submit, name: :status, value: nil, class: "badge badge-destructive gap-1" do %>
21
+ <%= icon_x class: "size-3.5 text-red-500" %>
22
+ Clear
23
+ <% end %>
24
+ <% end %>
25
+ </div>
26
+ </div>
27
+
28
+ <div>
29
+ <label class="label">Job Class</label>
30
+ <div class="flex flex-wrap gap-1 mt-1">
31
+ <% @job_class_names.each do |class_name| %>
32
+ <%= button_tag type: :submit,
33
+ name: :class_name,
34
+ value: class_name,
35
+ class: "px-2 badge #{params[:class_name] == class_name ? 'badge-primary' : 'badge-outline'}" do
36
+ %>
37
+ <%= class_name.to_s.titleize %>
38
+ <span class="opacity-50 -ml-0.5">
39
+ <%= SolidQueue::Job.where(class_name:).count %>
40
+ </span>
41
+ <% end %>
42
+ <% end %>
43
+
44
+ <% if params[:class_name].present? %>
45
+ <%= button_tag type: :submit, name: :class_name, value: nil, class: "badge badge-destructive gap-1" do %>
46
+ <%= icon_x class: "size-3.5 text-red-500" %>
47
+ Clear
48
+ <% end %>
49
+ <% end %>
50
+ </div>
51
+ </div>
52
+
53
+ <% if @job_queue_names.size > 1 %>
54
+ <div>
55
+ <label class="label">Queue</label>
56
+ <div class="flex flex-wrap gap-1 mt-1">
57
+ <% @job_queue_names.each do |queue_name| %>
58
+ <%= button_tag type: :submit,
59
+ name: :queue_name,
60
+ value: queue_name,
61
+ class: "px-2 badge #{params[:queue_name] == queue_name ? 'badge-primary' : 'badge-outline'}" do
62
+ %>
63
+ <%= queue_name.to_s.titleize %>
64
+ <span class="opacity-50 -ml-0.5">
65
+ <%= SolidQueue::Job.where(queue_name:).count %>
66
+ </span>
67
+ <% end %>
68
+ <% end %>
69
+
70
+ <% if params[:queue_name].present? %>
71
+ <%= button_tag type: :submit, name: :queue_name, value: nil, class: "badge badge-destructive gap-1" do %>
72
+ <%= icon_x class: "size-3.5 text-red-500" %>
73
+ Clear
74
+ <% end %>
75
+ <% end %>
76
+ </div>
77
+ </div>
78
+ <% end %>
79
+
80
+ <% if any_jobs_filters? %>
81
+ <hr>
82
+ <%= link_to jobs_path, class: "btn btn-outline btn-xs" do %>
83
+ <%= icon_x class: "size-4 text-red-500" %>
84
+ Clear All Filters
85
+ <% end %>
86
+ <% end %>
87
+ <% end %>