solid_queue_dashboard 0.0.1 → 0.1.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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +1 -1
  4. data/Procfile.dev +2 -0
  5. data/README.md +72 -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 +2062 -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 +72 -3
@@ -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
+ OpenStruct.new(
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 %>