solid_queue_dashboard 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +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 %>
|