solid_queue_web 0.2.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +65 -0
- data/Rakefile +36 -0
- data/app/assets/stylesheets/solid_queue_web/application.css +294 -0
- data/app/controllers/solid_queue_web/application_controller.rb +17 -0
- data/app/controllers/solid_queue_web/dashboard_controller.rb +15 -0
- data/app/controllers/solid_queue_web/failed_jobs_controller.rb +10 -0
- data/app/controllers/solid_queue_web/jobs_controller.rb +21 -0
- data/app/controllers/solid_queue_web/queues_controller.rb +7 -0
- data/app/helpers/solid_queue_web/application_helper.rb +8 -0
- data/app/jobs/solid_queue_web/application_job.rb +4 -0
- data/app/models/solid_queue_web/application_record.rb +5 -0
- data/app/views/layouts/solid_queue_web/application.html.erb +37 -0
- data/app/views/solid_queue_web/dashboard/index.html.erb +60 -0
- data/app/views/solid_queue_web/failed_jobs/index.html.erb +37 -0
- data/app/views/solid_queue_web/jobs/index.html.erb +54 -0
- data/app/views/solid_queue_web/queues/index.html.erb +34 -0
- data/config/routes.rb +7 -0
- data/lib/solid_queue_web/engine.rb +7 -0
- data/lib/solid_queue_web/version.rb +3 -0
- data/lib/solid_queue_web.rb +11 -0
- data/lib/tasks/solid_queue_web_tasks.rake +4 -0
- metadata +94 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6c49ff8a8bc89fae42038615cbc586d01e108db9549af1184d77623c51c9fb90
|
|
4
|
+
data.tar.gz: 5dc479a2e9b27676cb8a00f5d9034f3769af6310d90d6996dd3f5b4bc6569dea
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a6b5cc22477ff63ce321eeb3947e6200753c6960dc04e510f5e3d08aff750d87511e42668e48e154bc977dd123b9485e85e31eb9ca0c62ee315af3ada929dfd1
|
|
7
|
+
data.tar.gz: 59107143c3c2f59e25685e638dc61d2377cd9498cf7ef942dc43f3001b998ea73a218bdc64cbb2318859fb25bc8e8ec691ce688e9aaa7e6b17d76ccd5a746a30
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright Chuck Smith
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# SolidQueueWeb
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/solid_queue_web)
|
|
4
|
+
|
|
5
|
+
A read-only Rails engine that mounts a monitoring dashboard for [Solid Queue](https://github.com/rails/solid_queue). View queues, inspect jobs by status, and browse failed executions — all without leaving your app.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Dashboard** — stat cards showing counts for ready, scheduled, running, blocked, and failed jobs, plus queues and processes
|
|
10
|
+
- **Queues** — all queues sorted by name
|
|
11
|
+
- **Jobs** — filterable by status (ready, scheduled, claimed, blocked, failed) and by queue
|
|
12
|
+
- **Failed jobs** — list of failed executions with error details
|
|
13
|
+
- No external CSS framework — works out of the box
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add to your application's Gemfile:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem "solid_queue_web"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then run:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bundle install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Mounting the engine
|
|
30
|
+
|
|
31
|
+
Add to your `config/routes.rb`:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
mount SolidQueueWeb::Engine, at: "/jobs"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The dashboard will be available at `/jobs`.
|
|
38
|
+
|
|
39
|
+
## Authentication
|
|
40
|
+
|
|
41
|
+
The engine ships with no authentication by default. Add a block to an initializer (e.g. `config/initializers/solid_queue_web.rb`) to protect the dashboard:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
SolidQueueWeb.authenticate do
|
|
45
|
+
# Called in the context of ApplicationController — use any helper available there.
|
|
46
|
+
# Return a truthy value to allow access, falsy to deny (triggers HTTP Basic prompt).
|
|
47
|
+
current_user&.admin?
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
HTTP Basic authentication is used as a fallback when the block returns falsy.
|
|
52
|
+
|
|
53
|
+
## Requirements
|
|
54
|
+
|
|
55
|
+
- Ruby >= 3.3
|
|
56
|
+
- Rails >= 8.1.3
|
|
57
|
+
- solid_queue >= 1.0
|
|
58
|
+
|
|
59
|
+
## Contributing
|
|
60
|
+
|
|
61
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/eclectic-coding/solid_queue_web).
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require "bundler/setup"
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rubocop/rake_task"
|
|
5
|
+
require "rspec/core/rake_task"
|
|
6
|
+
|
|
7
|
+
RuboCop::RakeTask.new
|
|
8
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
9
|
+
|
|
10
|
+
task default: [ :rubocop, :spec ]
|
|
11
|
+
|
|
12
|
+
namespace :dev do
|
|
13
|
+
def dummy_env
|
|
14
|
+
ENV["RAILS_ENV"] = "development"
|
|
15
|
+
require File.expand_path("spec/dummy/config/environment", __dir__)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
desc "Create and migrate the dummy app development database"
|
|
19
|
+
task :setup do
|
|
20
|
+
dummy_env
|
|
21
|
+
ActiveRecord::Tasks::DatabaseTasks.root = File.expand_path("spec/dummy", __dir__)
|
|
22
|
+
db_config = ActiveRecord::Base.configurations.find_db_config("development")
|
|
23
|
+
ActiveRecord::Tasks::DatabaseTasks.create(db_config)
|
|
24
|
+
load File.expand_path("spec/dummy/db/schema.rb", __dir__)
|
|
25
|
+
puts "Development database ready."
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
desc "Seed the dummy app development database with fake data"
|
|
29
|
+
task :seed do
|
|
30
|
+
dummy_env
|
|
31
|
+
load File.expand_path("spec/dummy/db/seeds.rb", __dir__)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
desc "Reset and reseed the dummy app development database"
|
|
35
|
+
task reset: [ :setup, :seed ]
|
|
36
|
+
end
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/*
|
|
2
|
+
*= require_self
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
--bg: #f8f9fa;
|
|
9
|
+
--surface: #ffffff;
|
|
10
|
+
--border: #dee2e6;
|
|
11
|
+
--text: #212529;
|
|
12
|
+
--muted: #6c757d;
|
|
13
|
+
--primary: #0d6efd;
|
|
14
|
+
--danger: #dc3545;
|
|
15
|
+
--warning: #fd7e14;
|
|
16
|
+
--success: #198754;
|
|
17
|
+
--info: #0dcaf0;
|
|
18
|
+
--purple: #6f42c1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
body {
|
|
22
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
23
|
+
font-size: 14px;
|
|
24
|
+
background: var(--bg);
|
|
25
|
+
color: var(--text);
|
|
26
|
+
line-height: 1.5;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Layout */
|
|
30
|
+
.sqd-header {
|
|
31
|
+
background: var(--surface);
|
|
32
|
+
border-bottom: 1px solid var(--border);
|
|
33
|
+
padding: 0 1.5rem;
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 2rem;
|
|
37
|
+
height: 56px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.sqd-header__title {
|
|
41
|
+
font-size: 16px;
|
|
42
|
+
font-weight: 600;
|
|
43
|
+
color: var(--text);
|
|
44
|
+
text-decoration: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.sqd-nav {
|
|
48
|
+
display: flex;
|
|
49
|
+
gap: 0.25rem;
|
|
50
|
+
list-style: none;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.sqd-nav a {
|
|
54
|
+
display: block;
|
|
55
|
+
padding: 0.35rem 0.75rem;
|
|
56
|
+
border-radius: 6px;
|
|
57
|
+
color: var(--muted);
|
|
58
|
+
text-decoration: none;
|
|
59
|
+
font-size: 13px;
|
|
60
|
+
font-weight: 500;
|
|
61
|
+
transition: background 0.1s, color 0.1s;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.sqd-nav a:hover,
|
|
65
|
+
.sqd-nav a.active {
|
|
66
|
+
background: var(--bg);
|
|
67
|
+
color: var(--text);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.sqd-main {
|
|
71
|
+
max-width: 1200px;
|
|
72
|
+
margin: 0 auto;
|
|
73
|
+
padding: 2rem 1.5rem;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.sqd-page-title {
|
|
77
|
+
font-size: 20px;
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
margin-bottom: 1.5rem;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Flash notices */
|
|
83
|
+
.sqd-flash {
|
|
84
|
+
padding: 0.75rem 1rem;
|
|
85
|
+
border-radius: 6px;
|
|
86
|
+
margin-bottom: 1rem;
|
|
87
|
+
font-size: 13px;
|
|
88
|
+
}
|
|
89
|
+
.sqd-flash--notice { background: #d1e7dd; color: #0f5132; border: 1px solid #badbcc; }
|
|
90
|
+
.sqd-flash--alert { background: #f8d7da; color: #842029; border: 1px solid #f5c2c7; }
|
|
91
|
+
|
|
92
|
+
/* Stat cards */
|
|
93
|
+
.sqd-stats {
|
|
94
|
+
display: grid;
|
|
95
|
+
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
96
|
+
gap: 1rem;
|
|
97
|
+
margin-bottom: 2rem;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.sqd-stat {
|
|
101
|
+
background: var(--surface);
|
|
102
|
+
border: 1px solid var(--border);
|
|
103
|
+
border-radius: 8px;
|
|
104
|
+
padding: 1.25rem 1rem;
|
|
105
|
+
text-align: center;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.sqd-stat__value {
|
|
109
|
+
font-size: 28px;
|
|
110
|
+
font-weight: 700;
|
|
111
|
+
line-height: 1;
|
|
112
|
+
margin-bottom: 0.25rem;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.sqd-stat__label {
|
|
116
|
+
font-size: 12px;
|
|
117
|
+
color: var(--muted);
|
|
118
|
+
text-transform: uppercase;
|
|
119
|
+
letter-spacing: 0.05em;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.sqd-stat--ready .sqd-stat__value { color: var(--success); }
|
|
123
|
+
.sqd-stat--scheduled .sqd-stat__value { color: var(--info); }
|
|
124
|
+
.sqd-stat--claimed .sqd-stat__value { color: var(--primary); }
|
|
125
|
+
.sqd-stat--failed .sqd-stat__value { color: var(--danger); }
|
|
126
|
+
.sqd-stat--blocked .sqd-stat__value { color: var(--warning); }
|
|
127
|
+
.sqd-stat--queues .sqd-stat__value { color: var(--purple); }
|
|
128
|
+
.sqd-stat--processes .sqd-stat__value { color: var(--muted); }
|
|
129
|
+
|
|
130
|
+
/* Tables */
|
|
131
|
+
.sqd-card {
|
|
132
|
+
background: var(--surface);
|
|
133
|
+
border: 1px solid var(--border);
|
|
134
|
+
border-radius: 8px;
|
|
135
|
+
overflow: hidden;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.sqd-card__header {
|
|
139
|
+
padding: 0.875rem 1rem;
|
|
140
|
+
border-bottom: 1px solid var(--border);
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: space-between;
|
|
144
|
+
gap: 1rem;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.sqd-card__title {
|
|
148
|
+
font-size: 14px;
|
|
149
|
+
font-weight: 600;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
table {
|
|
153
|
+
width: 100%;
|
|
154
|
+
border-collapse: collapse;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
th {
|
|
158
|
+
padding: 0.625rem 1rem;
|
|
159
|
+
text-align: left;
|
|
160
|
+
font-size: 12px;
|
|
161
|
+
font-weight: 600;
|
|
162
|
+
color: var(--muted);
|
|
163
|
+
text-transform: uppercase;
|
|
164
|
+
letter-spacing: 0.05em;
|
|
165
|
+
border-bottom: 1px solid var(--border);
|
|
166
|
+
white-space: nowrap;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
td {
|
|
170
|
+
padding: 0.75rem 1rem;
|
|
171
|
+
border-bottom: 1px solid var(--border);
|
|
172
|
+
vertical-align: middle;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
tr:last-child td { border-bottom: none; }
|
|
176
|
+
tbody tr:hover { background: var(--bg); }
|
|
177
|
+
|
|
178
|
+
.sqd-empty {
|
|
179
|
+
text-align: center;
|
|
180
|
+
padding: 3rem 1rem;
|
|
181
|
+
color: var(--muted);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* Badges */
|
|
185
|
+
.sqd-badge {
|
|
186
|
+
display: inline-block;
|
|
187
|
+
padding: 0.2em 0.55em;
|
|
188
|
+
border-radius: 4px;
|
|
189
|
+
font-size: 11px;
|
|
190
|
+
font-weight: 600;
|
|
191
|
+
line-height: 1;
|
|
192
|
+
text-transform: uppercase;
|
|
193
|
+
letter-spacing: 0.04em;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.sqd-badge--ready { background: #d1e7dd; color: #0f5132; }
|
|
197
|
+
.sqd-badge--scheduled { background: #cff4fc; color: #055160; }
|
|
198
|
+
.sqd-badge--claimed { background: #cfe2ff; color: #084298; }
|
|
199
|
+
.sqd-badge--failed { background: #f8d7da; color: #842029; }
|
|
200
|
+
.sqd-badge--blocked { background: #fff3cd; color: #664d03; }
|
|
201
|
+
.sqd-badge--paused { background: #e2e3e5; color: #41464b; }
|
|
202
|
+
.sqd-badge--running { background: #d1e7dd; color: #0f5132; }
|
|
203
|
+
|
|
204
|
+
/* Buttons */
|
|
205
|
+
.sqd-btn {
|
|
206
|
+
display: inline-flex;
|
|
207
|
+
align-items: center;
|
|
208
|
+
padding: 0.35rem 0.75rem;
|
|
209
|
+
border-radius: 5px;
|
|
210
|
+
font-size: 12px;
|
|
211
|
+
font-weight: 500;
|
|
212
|
+
text-decoration: none;
|
|
213
|
+
border: 1px solid transparent;
|
|
214
|
+
cursor: pointer;
|
|
215
|
+
transition: opacity 0.15s;
|
|
216
|
+
}
|
|
217
|
+
.sqd-btn:hover { opacity: 0.85; }
|
|
218
|
+
.sqd-btn--primary { background: var(--primary); color: #fff; border-color: var(--primary); }
|
|
219
|
+
.sqd-btn--danger { background: var(--danger); color: #fff; border-color: var(--danger); }
|
|
220
|
+
.sqd-btn--muted { background: var(--surface); color: var(--text); border-color: var(--border); }
|
|
221
|
+
|
|
222
|
+
/* Filters */
|
|
223
|
+
.sqd-filters {
|
|
224
|
+
display: flex;
|
|
225
|
+
gap: 0.5rem;
|
|
226
|
+
flex-wrap: wrap;
|
|
227
|
+
margin-bottom: 1rem;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.sqd-filters a {
|
|
231
|
+
padding: 0.35rem 0.875rem;
|
|
232
|
+
border-radius: 20px;
|
|
233
|
+
font-size: 12px;
|
|
234
|
+
font-weight: 500;
|
|
235
|
+
text-decoration: none;
|
|
236
|
+
border: 1px solid var(--border);
|
|
237
|
+
color: var(--muted);
|
|
238
|
+
background: var(--surface);
|
|
239
|
+
transition: all 0.1s;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.sqd-filters a:hover,
|
|
243
|
+
.sqd-filters a.active {
|
|
244
|
+
background: var(--primary);
|
|
245
|
+
border-color: var(--primary);
|
|
246
|
+
color: #fff;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* Code / monospace */
|
|
250
|
+
.sqd-mono {
|
|
251
|
+
font-family: "SFMono-Regular", Menlo, Monaco, Consolas, monospace;
|
|
252
|
+
font-size: 12px;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.sqd-error-msg {
|
|
256
|
+
color: var(--danger);
|
|
257
|
+
font-size: 12px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.sqd-truncate {
|
|
261
|
+
max-width: 320px;
|
|
262
|
+
overflow: hidden;
|
|
263
|
+
text-overflow: ellipsis;
|
|
264
|
+
white-space: nowrap;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* Pagination (pagy v43 series-nav) */
|
|
268
|
+
nav.pagy {
|
|
269
|
+
display: flex;
|
|
270
|
+
justify-content: center;
|
|
271
|
+
gap: 0.25rem;
|
|
272
|
+
padding: 1rem;
|
|
273
|
+
list-style: none;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
nav.pagy a {
|
|
277
|
+
display: inline-flex;
|
|
278
|
+
align-items: center;
|
|
279
|
+
justify-content: center;
|
|
280
|
+
min-width: 32px;
|
|
281
|
+
height: 32px;
|
|
282
|
+
padding: 0 0.5rem;
|
|
283
|
+
border-radius: 5px;
|
|
284
|
+
font-size: 13px;
|
|
285
|
+
text-decoration: none;
|
|
286
|
+
border: 1px solid var(--border);
|
|
287
|
+
color: var(--text);
|
|
288
|
+
background: var(--surface);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
nav.pagy a:hover:not([aria-disabled="true"]) { background: var(--bg); }
|
|
292
|
+
nav.pagy a[aria-current="page"] { background: var(--primary); color: #fff; border-color: var(--primary); }
|
|
293
|
+
nav.pagy a[role="separator"],
|
|
294
|
+
nav.pagy a[aria-disabled="true"] { color: var(--muted); cursor: default; }
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module SolidQueueWeb
|
|
2
|
+
class ApplicationController < ActionController::Base
|
|
3
|
+
before_action :authenticate!
|
|
4
|
+
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def authenticate!
|
|
8
|
+
return unless (auth = SolidQueueWeb.authenticate)
|
|
9
|
+
|
|
10
|
+
instance_exec(&auth) || request_basic_auth
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def request_basic_auth
|
|
14
|
+
request_http_basic_authentication("Solid Queue Dashboard")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module SolidQueueWeb
|
|
2
|
+
class DashboardController < ApplicationController
|
|
3
|
+
def index
|
|
4
|
+
@stats = {
|
|
5
|
+
ready: SolidQueue::ReadyExecution.count,
|
|
6
|
+
scheduled: SolidQueue::ScheduledExecution.count,
|
|
7
|
+
claimed: SolidQueue::ClaimedExecution.count,
|
|
8
|
+
failed: SolidQueue::FailedExecution.count,
|
|
9
|
+
blocked: SolidQueue::BlockedExecution.count,
|
|
10
|
+
queues: SolidQueue::Job.select(:queue_name).distinct.count,
|
|
11
|
+
processes: SolidQueue::Process.count
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module SolidQueueWeb
|
|
2
|
+
class JobsController < ApplicationController
|
|
3
|
+
STATUSES = %w[ready scheduled claimed blocked failed].freeze
|
|
4
|
+
|
|
5
|
+
def index
|
|
6
|
+
@status = params[:status].presence_in(STATUSES) || "ready"
|
|
7
|
+
@queue = params[:queue].presence
|
|
8
|
+
|
|
9
|
+
@jobs = case @status
|
|
10
|
+
when "ready" then SolidQueue::ReadyExecution.includes(:job)
|
|
11
|
+
when "scheduled" then SolidQueue::ScheduledExecution.includes(:job)
|
|
12
|
+
when "claimed" then SolidQueue::ClaimedExecution.includes(:job)
|
|
13
|
+
when "blocked" then SolidQueue::BlockedExecution.includes(:job)
|
|
14
|
+
when "failed" then SolidQueue::FailedExecution.includes(:job)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
@jobs = @jobs.where(jobs: { queue_name: @queue }) if @queue.present?
|
|
18
|
+
@jobs = @jobs.order(created_at: :desc).limit(100)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Solid Queue Dashboard</title>
|
|
7
|
+
<%= csrf_meta_tags %>
|
|
8
|
+
<%= csp_meta_tag %>
|
|
9
|
+
<%= inline_styles %>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
|
|
13
|
+
<header class="sqd-header">
|
|
14
|
+
<%= link_to "Solid Queue", root_path, class: "sqd-header__title" %>
|
|
15
|
+
<nav>
|
|
16
|
+
<ul class="sqd-nav">
|
|
17
|
+
<li><%= link_to "Dashboard", root_path, class: current_page?(root_path) ? "active" : "" %></li>
|
|
18
|
+
<li><%= link_to "Queues", queues_path, class: current_page?(queues_path) ? "active" : "" %></li>
|
|
19
|
+
<li><%= link_to "Jobs", jobs_path, class: current_page?(jobs_path) ? "active" : "" %></li>
|
|
20
|
+
<li><%= link_to "Failed", failed_jobs_path, class: current_page?(failed_jobs_path) ? "active" : "" %></li>
|
|
21
|
+
</ul>
|
|
22
|
+
</nav>
|
|
23
|
+
</header>
|
|
24
|
+
|
|
25
|
+
<main class="sqd-main">
|
|
26
|
+
<% if notice.present? %>
|
|
27
|
+
<div class="sqd-flash sqd-flash--notice"><%= notice %></div>
|
|
28
|
+
<% end %>
|
|
29
|
+
<% if alert.present? %>
|
|
30
|
+
<div class="sqd-flash sqd-flash--alert"><%= alert %></div>
|
|
31
|
+
<% end %>
|
|
32
|
+
|
|
33
|
+
<%= yield %>
|
|
34
|
+
</main>
|
|
35
|
+
|
|
36
|
+
</body>
|
|
37
|
+
</html>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<h1 class="sqd-page-title">Dashboard</h1>
|
|
2
|
+
|
|
3
|
+
<div class="sqd-stats">
|
|
4
|
+
<div class="sqd-stat sqd-stat--ready">
|
|
5
|
+
<div class="sqd-stat__value"><%= @stats[:ready] %></div>
|
|
6
|
+
<div class="sqd-stat__label">Ready</div>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="sqd-stat sqd-stat--scheduled">
|
|
9
|
+
<div class="sqd-stat__value"><%= @stats[:scheduled] %></div>
|
|
10
|
+
<div class="sqd-stat__label">Scheduled</div>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="sqd-stat sqd-stat--claimed">
|
|
13
|
+
<div class="sqd-stat__value"><%= @stats[:claimed] %></div>
|
|
14
|
+
<div class="sqd-stat__label">Running</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="sqd-stat sqd-stat--blocked">
|
|
17
|
+
<div class="sqd-stat__value"><%= @stats[:blocked] %></div>
|
|
18
|
+
<div class="sqd-stat__label">Blocked</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="sqd-stat sqd-stat--failed">
|
|
21
|
+
<div class="sqd-stat__value"><%= @stats[:failed] %></div>
|
|
22
|
+
<div class="sqd-stat__label">Failed</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="sqd-stat sqd-stat--queues">
|
|
25
|
+
<div class="sqd-stat__value"><%= @stats[:queues] %></div>
|
|
26
|
+
<div class="sqd-stat__label">Queues</div>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="sqd-stat sqd-stat--processes">
|
|
29
|
+
<div class="sqd-stat__value"><%= @stats[:processes] %></div>
|
|
30
|
+
<div class="sqd-stat__label">Processes</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
|
|
35
|
+
<div class="sqd-card">
|
|
36
|
+
<div class="sqd-card__header">
|
|
37
|
+
<span class="sqd-card__title">Quick Links</span>
|
|
38
|
+
</div>
|
|
39
|
+
<div style="padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
|
|
40
|
+
<%= link_to "View all ready jobs", jobs_path(status: "ready"), class: "sqd-btn sqd-btn--muted" %>
|
|
41
|
+
<%= link_to "View scheduled jobs", jobs_path(status: "scheduled"), class: "sqd-btn sqd-btn--muted" %>
|
|
42
|
+
<%= link_to "View failed jobs", failed_jobs_path, class: "sqd-btn sqd-btn--muted" %>
|
|
43
|
+
<%= link_to "Manage queues", queues_path, class: "sqd-btn sqd-btn--muted" %>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<% if @stats[:failed] > 0 %>
|
|
48
|
+
<div class="sqd-card">
|
|
49
|
+
<div class="sqd-card__header">
|
|
50
|
+
<span class="sqd-card__title">Attention Required</span>
|
|
51
|
+
</div>
|
|
52
|
+
<div style="padding: 1rem;">
|
|
53
|
+
<p style="color: var(--danger); margin-bottom: 0.75rem;">
|
|
54
|
+
<%= pluralize(@stats[:failed], "failed job") %> need attention.
|
|
55
|
+
</p>
|
|
56
|
+
<%= link_to "Review failed jobs →", failed_jobs_path, class: "sqd-btn sqd-btn--danger" %>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<% end %>
|
|
60
|
+
</div>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<h1 class="sqd-page-title">Failed Jobs</h1>
|
|
2
|
+
|
|
3
|
+
<div class="sqd-card">
|
|
4
|
+
<% if @failed_jobs.empty? %>
|
|
5
|
+
<div class="sqd-empty">No failed jobs. All clear!</div>
|
|
6
|
+
<% else %>
|
|
7
|
+
<table>
|
|
8
|
+
<thead>
|
|
9
|
+
<tr>
|
|
10
|
+
<th>Job Class</th>
|
|
11
|
+
<th>Queue</th>
|
|
12
|
+
<th>Error</th>
|
|
13
|
+
<th>Failed At</th>
|
|
14
|
+
</tr>
|
|
15
|
+
</thead>
|
|
16
|
+
<tbody>
|
|
17
|
+
<% @failed_jobs.each do |execution| %>
|
|
18
|
+
<% job = execution.job %>
|
|
19
|
+
<tr>
|
|
20
|
+
<td><%= job.class_name %></td>
|
|
21
|
+
<td class="sqd-mono"><%= job.queue_name %></td>
|
|
22
|
+
<td>
|
|
23
|
+
<% if execution.exception_class.present? %>
|
|
24
|
+
<div class="sqd-error-msg sqd-truncate" title="<%= execution.exception_class %>: <%= execution.message %>">
|
|
25
|
+
<strong><%= execution.exception_class %></strong>: <%= execution.message %>
|
|
26
|
+
</div>
|
|
27
|
+
<% else %>
|
|
28
|
+
<span class="sqd-muted">—</span>
|
|
29
|
+
<% end %>
|
|
30
|
+
</td>
|
|
31
|
+
<td class="sqd-mono"><%= execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
32
|
+
</tr>
|
|
33
|
+
<% end %>
|
|
34
|
+
</tbody>
|
|
35
|
+
</table>
|
|
36
|
+
<% end %>
|
|
37
|
+
</div>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<h1 class="sqd-page-title">Jobs</h1>
|
|
2
|
+
|
|
3
|
+
<div class="sqd-filters">
|
|
4
|
+
<%= link_to "Ready", jobs_path(status: "ready", queue: @queue), class: @status == "ready" ? "active" : "" %>
|
|
5
|
+
<%= link_to "Scheduled", jobs_path(status: "scheduled", queue: @queue), class: @status == "scheduled" ? "active" : "" %>
|
|
6
|
+
<%= link_to "Running", jobs_path(status: "claimed", queue: @queue), class: @status == "claimed" ? "active" : "" %>
|
|
7
|
+
<%= link_to "Blocked", jobs_path(status: "blocked", queue: @queue), class: @status == "blocked" ? "active" : "" %>
|
|
8
|
+
<%= link_to "Failed", jobs_path(status: "failed", queue: @queue), class: @status == "failed" ? "active" : "" %>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div class="sqd-card">
|
|
12
|
+
<% if @jobs.empty? %>
|
|
13
|
+
<div class="sqd-empty">No <%= @status %> jobs.</div>
|
|
14
|
+
<% else %>
|
|
15
|
+
<table>
|
|
16
|
+
<thead>
|
|
17
|
+
<tr>
|
|
18
|
+
<th>Job Class</th>
|
|
19
|
+
<th>Queue</th>
|
|
20
|
+
<th>Priority</th>
|
|
21
|
+
<th>Scheduled At</th>
|
|
22
|
+
<th>Enqueued At</th>
|
|
23
|
+
</tr>
|
|
24
|
+
</thead>
|
|
25
|
+
<tbody>
|
|
26
|
+
<% @jobs.each do |execution| %>
|
|
27
|
+
<% job = execution.job %>
|
|
28
|
+
<tr>
|
|
29
|
+
<td>
|
|
30
|
+
<span class="sqd-badge sqd-badge--<%= @status %>"><%= @status %></span>
|
|
31
|
+
<span style="margin-left: 0.5rem;"><%= job.class_name %></span>
|
|
32
|
+
</td>
|
|
33
|
+
<td>
|
|
34
|
+
<%= link_to job.queue_name, jobs_path(status: @status, queue: job.queue_name),
|
|
35
|
+
class: "sqd-mono", style: "color: inherit;" %>
|
|
36
|
+
</td>
|
|
37
|
+
<td><%= job.priority %></td>
|
|
38
|
+
<td class="sqd-mono">
|
|
39
|
+
<%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %>
|
|
40
|
+
</td>
|
|
41
|
+
<td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
42
|
+
</tr>
|
|
43
|
+
<% end %>
|
|
44
|
+
</tbody>
|
|
45
|
+
</table>
|
|
46
|
+
<% end %>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<% if @queue.present? %>
|
|
50
|
+
<p style="margin-top: 0.75rem; font-size: 13px; color: var(--muted);">
|
|
51
|
+
Filtering by queue: <strong><%= @queue %></strong> —
|
|
52
|
+
<%= link_to "Clear filter", jobs_path(status: @status) %>
|
|
53
|
+
</p>
|
|
54
|
+
<% end %>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<h1 class="sqd-page-title">Queues</h1>
|
|
2
|
+
|
|
3
|
+
<div class="sqd-card">
|
|
4
|
+
<% if @queues.empty? %>
|
|
5
|
+
<div class="sqd-empty">No queues found.</div>
|
|
6
|
+
<% else %>
|
|
7
|
+
<table>
|
|
8
|
+
<thead>
|
|
9
|
+
<tr>
|
|
10
|
+
<th>Name</th>
|
|
11
|
+
<th>Size</th>
|
|
12
|
+
<th>Latency</th>
|
|
13
|
+
<th>Status</th>
|
|
14
|
+
</tr>
|
|
15
|
+
</thead>
|
|
16
|
+
<tbody>
|
|
17
|
+
<% @queues.each do |queue| %>
|
|
18
|
+
<tr>
|
|
19
|
+
<td class="sqd-mono"><%= queue.name %></td>
|
|
20
|
+
<td><%= queue.size %></td>
|
|
21
|
+
<td><%= queue.human_latency %></td>
|
|
22
|
+
<td>
|
|
23
|
+
<% if queue.paused? %>
|
|
24
|
+
<span class="sqd-badge sqd-badge--paused">Paused</span>
|
|
25
|
+
<% else %>
|
|
26
|
+
<span class="sqd-badge sqd-badge--running">Running</span>
|
|
27
|
+
<% end %>
|
|
28
|
+
</td>
|
|
29
|
+
</tr>
|
|
30
|
+
<% end %>
|
|
31
|
+
</tbody>
|
|
32
|
+
</table>
|
|
33
|
+
<% end %>
|
|
34
|
+
</div>
|
data/config/routes.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: solid_queue_web
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Chuck Smith
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 8.1.3
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 8.1.3
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: solid_queue
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.0'
|
|
40
|
+
description: Mount SolidQueueWeb in any Rails app using Solid Queue to get a real-time
|
|
41
|
+
read-only view of your queues, jobs by status, and failed executions.
|
|
42
|
+
email:
|
|
43
|
+
- eclectic-coding@users.noreply.github.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- MIT-LICENSE
|
|
49
|
+
- README.md
|
|
50
|
+
- Rakefile
|
|
51
|
+
- app/assets/stylesheets/solid_queue_web/application.css
|
|
52
|
+
- app/controllers/solid_queue_web/application_controller.rb
|
|
53
|
+
- app/controllers/solid_queue_web/dashboard_controller.rb
|
|
54
|
+
- app/controllers/solid_queue_web/failed_jobs_controller.rb
|
|
55
|
+
- app/controllers/solid_queue_web/jobs_controller.rb
|
|
56
|
+
- app/controllers/solid_queue_web/queues_controller.rb
|
|
57
|
+
- app/helpers/solid_queue_web/application_helper.rb
|
|
58
|
+
- app/jobs/solid_queue_web/application_job.rb
|
|
59
|
+
- app/models/solid_queue_web/application_record.rb
|
|
60
|
+
- app/views/layouts/solid_queue_web/application.html.erb
|
|
61
|
+
- app/views/solid_queue_web/dashboard/index.html.erb
|
|
62
|
+
- app/views/solid_queue_web/failed_jobs/index.html.erb
|
|
63
|
+
- app/views/solid_queue_web/jobs/index.html.erb
|
|
64
|
+
- app/views/solid_queue_web/queues/index.html.erb
|
|
65
|
+
- config/routes.rb
|
|
66
|
+
- lib/solid_queue_web.rb
|
|
67
|
+
- lib/solid_queue_web/engine.rb
|
|
68
|
+
- lib/solid_queue_web/version.rb
|
|
69
|
+
- lib/tasks/solid_queue_web_tasks.rake
|
|
70
|
+
homepage: https://github.com/eclectic-coding/solid_queue_web
|
|
71
|
+
licenses:
|
|
72
|
+
- MIT
|
|
73
|
+
metadata:
|
|
74
|
+
homepage_uri: https://github.com/eclectic-coding/solid_queue_web
|
|
75
|
+
source_code_uri: https://github.com/eclectic-coding/solid_queue_web
|
|
76
|
+
changelog_uri: https://github.com/eclectic-coding/solid_queue_web/blob/main/CHANGELOG.md
|
|
77
|
+
rdoc_options: []
|
|
78
|
+
require_paths:
|
|
79
|
+
- lib
|
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: '3.3'
|
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
requirements: []
|
|
91
|
+
rubygems_version: 4.0.10
|
|
92
|
+
specification_version: 4
|
|
93
|
+
summary: A read-only Rails engine dashboard for monitoring Solid Queue jobs.
|
|
94
|
+
test_files: []
|