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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +5 -1
- data/Procfile.dev +2 -0
- data/README.md +73 -21
- data/app/assets/javascripts/solid_queue_dashboard/alpine.js +5 -0
- data/app/assets/javascripts/solid_queue_dashboard/application.js +46 -0
- data/app/assets/stylesheets/solid_queue_dashboard/application.css +2049 -0
- data/app/assets/stylesheets/solid_queue_dashboard/tailwind.css +554 -0
- data/app/controllers/solid_queue_dashboard/appearance_controller.rb +8 -0
- data/app/controllers/solid_queue_dashboard/application_controller.rb +7 -0
- data/app/controllers/solid_queue_dashboard/dashboard_controller.rb +8 -0
- data/app/controllers/solid_queue_dashboard/jobs_controller.rb +42 -0
- data/app/controllers/solid_queue_dashboard/processes_controller.rb +28 -0
- data/app/controllers/solid_queue_dashboard/recurring_tasks_controller.rb +30 -0
- data/app/helpers/solid_queue_dashboard/appearance_helper.rb +7 -0
- data/app/helpers/solid_queue_dashboard/application_helper.rb +7 -0
- data/app/helpers/solid_queue_dashboard/icons_helper.rb +231 -0
- data/app/helpers/solid_queue_dashboard/jobs_helper.rb +51 -0
- data/app/helpers/solid_queue_dashboard/pagination_helper.rb +38 -0
- data/app/helpers/solid_queue_dashboard/processes_helper.rb +35 -0
- data/app/helpers/solid_queue_dashboard/recurring_tasks_helper.rb +33 -0
- data/app/views/layouts/solid_queue_dashboard/application.html.erb +21 -0
- data/app/views/solid_queue_dashboard/application/_flash_messages.html.erb +38 -0
- data/app/views/solid_queue_dashboard/application/_footer.html.erb +11 -0
- data/app/views/solid_queue_dashboard/application/_navbar.html.erb +50 -0
- data/app/views/solid_queue_dashboard/application/_pagination.html.erb +19 -0
- data/app/views/solid_queue_dashboard/dashboard/index.html.erb +41 -0
- data/app/views/solid_queue_dashboard/jobs/_filters.html.erb +87 -0
- data/app/views/solid_queue_dashboard/jobs/_table.html.erb +19 -0
- data/app/views/solid_queue_dashboard/jobs/_table_row.html.erb +82 -0
- data/app/views/solid_queue_dashboard/jobs/index.html.erb +57 -0
- data/app/views/solid_queue_dashboard/jobs/show.html.erb +192 -0
- data/app/views/solid_queue_dashboard/processes/_filters.html.erb +64 -0
- data/app/views/solid_queue_dashboard/processes/_table.html.erb +18 -0
- data/app/views/solid_queue_dashboard/processes/_table_row.html.erb +32 -0
- data/app/views/solid_queue_dashboard/processes/index.html.erb +27 -0
- data/app/views/solid_queue_dashboard/processes/show.html.erb +79 -0
- data/app/views/solid_queue_dashboard/recurring_tasks/_filters.html.erb +26 -0
- data/app/views/solid_queue_dashboard/recurring_tasks/_table.html.erb +19 -0
- data/app/views/solid_queue_dashboard/recurring_tasks/_table_row.html.erb +31 -0
- data/app/views/solid_queue_dashboard/recurring_tasks/index.html.erb +21 -0
- data/app/views/solid_queue_dashboard/recurring_tasks/show.html.erb +129 -0
- data/bun.lockb +0 -0
- data/config/routes.rb +18 -0
- data/lib/solid_queue_dashboard/configuration.rb +17 -0
- data/lib/solid_queue_dashboard/decorators/job_decorator.rb +63 -0
- data/lib/solid_queue_dashboard/decorators/jobs_decorator.rb +74 -0
- data/lib/solid_queue_dashboard/decorators/process_decorator.rb +13 -0
- data/lib/solid_queue_dashboard/decorators/processes_decorator.rb +15 -0
- data/lib/solid_queue_dashboard/decorators/recurring_task_decorator.rb +22 -0
- data/lib/solid_queue_dashboard/decorators/recurring_tasks_decorator.rb +26 -0
- data/lib/solid_queue_dashboard/engine.rb +13 -0
- data/lib/solid_queue_dashboard/job.rb +26 -0
- data/lib/solid_queue_dashboard/process.rb +24 -0
- data/lib/solid_queue_dashboard/recurring_task.rb +14 -0
- data/lib/solid_queue_dashboard/version.rb +1 -1
- data/lib/solid_queue_dashboard.rb +39 -1
- data/package.json +13 -0
- data/tailwind.config.js +83 -0
- 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('« 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">…</span>
|
13
|
+
<% end %>
|
14
|
+
<% end %>
|
15
|
+
|
16
|
+
<%= link_to raw('Next »'), 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 %>
|