solid_stack_web 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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +82 -0
- data/Rakefile +41 -0
- data/app/assets/stylesheets/solid_stack_web/_01_base.css +32 -0
- data/app/assets/stylesheets/solid_stack_web/_02_layout.css +83 -0
- data/app/assets/stylesheets/solid_stack_web/_03_stats.css +31 -0
- data/app/assets/stylesheets/solid_stack_web/_04_table.css +39 -0
- data/app/assets/stylesheets/solid_stack_web/_05_badges.css +20 -0
- data/app/assets/stylesheets/solid_stack_web/_06_buttons.css +40 -0
- data/app/assets/stylesheets/solid_stack_web/_07_dashboard.css +77 -0
- data/app/assets/stylesheets/solid_stack_web/application.css +1 -0
- data/app/controllers/solid_stack_web/application_controller.rb +43 -0
- data/app/controllers/solid_stack_web/cable_controller.rb +8 -0
- data/app/controllers/solid_stack_web/cache_controller.rb +8 -0
- data/app/controllers/solid_stack_web/dashboard_controller.rb +22 -0
- data/app/controllers/solid_stack_web/failed_jobs_controller.rb +25 -0
- data/app/controllers/solid_stack_web/jobs_controller.rb +42 -0
- data/app/controllers/solid_stack_web/processes_controller.rb +7 -0
- data/app/controllers/solid_stack_web/queues_controller.rb +26 -0
- data/app/helpers/solid_stack_web/application_helper.rb +9 -0
- data/app/views/layouts/solid_stack_web/application.html.erb +48 -0
- data/app/views/solid_stack_web/cable/index.html.erb +27 -0
- data/app/views/solid_stack_web/cache/index.html.erb +14 -0
- data/app/views/solid_stack_web/dashboard/index.html.erb +72 -0
- data/app/views/solid_stack_web/failed_jobs/index.html.erb +41 -0
- data/app/views/solid_stack_web/jobs/_empty.html.erb +3 -0
- data/app/views/solid_stack_web/jobs/destroy.turbo_stream.erb +7 -0
- data/app/views/solid_stack_web/jobs/index.html.erb +50 -0
- data/app/views/solid_stack_web/processes/index.html.erb +32 -0
- data/app/views/solid_stack_web/queues/index.html.erb +44 -0
- data/config/routes.rb +21 -0
- data/lib/solid_stack_web/engine.rb +19 -0
- data/lib/solid_stack_web/version.rb +3 -0
- data/lib/solid_stack_web.rb +25 -0
- data/lib/tasks/solid_stack_web_tasks.rake +4 -0
- metadata +151 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9ff5e759d441ff203317f09e35d4e9fb6876f81b544d4b075093c3d2cf397293
|
|
4
|
+
data.tar.gz: 9be02459700b7da623c939241ffc49e0147a4a460fb68f85f0ff3d63b10cea7f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8dcdb3c9445a05628849c51d6081b5a3f399249b690da8a254583e134b0d761b04a9d9228818614d1b0239d4226648cec75efd8da6c7b8a990f0832df57730a4
|
|
7
|
+
data.tar.gz: ea5a60256af380188021c58aeeab6ab7e58c8eea3bd5aaf9ff03c3306d763cebb667fef58482e09fc1ee677894b110f03ed6cadb989bc254b6cb73d997909a52
|
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,82 @@
|
|
|
1
|
+
# SolidStackWeb
|
|
2
|
+
|
|
3
|
+
[](https://github.com/eclectic-coding/solid_stack_web/actions/workflows/ci.yml)
|
|
4
|
+
[](https://badge.fury.io/rb/solid_stack_web)
|
|
5
|
+
[](https://www.ruby-lang.org)
|
|
6
|
+
[](https://codecov.io/gh/eclectic-coding/solid_stack_web)
|
|
7
|
+
|
|
8
|
+
A mountable Rails engine that provides a unified web dashboard for the full [Solid Stack](https://github.com/rails/solid_queue) — **Solid Queue**, **Solid Cache**, and **Solid Cable** — in a single interface with no asset pipeline dependency and no JavaScript runtime requirement.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **Overview dashboard** with live counts across all three Solid Stack components
|
|
13
|
+
- **Solid Queue** — browse jobs by status (ready, scheduled, claimed, blocked), manage failed jobs (retry / discard), pause/resume queues, and inspect worker processes
|
|
14
|
+
- **Solid Cache** — entry count and total byte size at a glance
|
|
15
|
+
- **Solid Cable** — active message count and distinct channel count
|
|
16
|
+
- **Turbo Stream** job discard — removes the row inline without a full page reload
|
|
17
|
+
- **Authentication hook** — plug in your own auth logic (Devise, Basic Auth, custom) via a one-line initializer
|
|
18
|
+
- **Zero asset pipeline coupling** — CSS is injected inline; safe to mount in any host app
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Add the gem to your application's `Gemfile`:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
gem "solid_stack_web"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Run:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bundle install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Mount the engine in `config/routes.rb`:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
mount SolidStackWeb::Engine, at: "/solid_stack"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The dashboard will be available at `/solid_stack` (or whatever path you choose).
|
|
41
|
+
|
|
42
|
+
## Configuration
|
|
43
|
+
|
|
44
|
+
Create an initializer at `config/initializers/solid_stack_web.rb`:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
SolidStackWeb.configure do |config|
|
|
48
|
+
# Number of items per paginated page (default: 25)
|
|
49
|
+
config.page_size = 50
|
|
50
|
+
|
|
51
|
+
# Authentication — block runs in controller context.
|
|
52
|
+
# Return a truthy value to allow access; falsy falls back to HTTP Basic.
|
|
53
|
+
config.authenticate do
|
|
54
|
+
current_user&.admin?
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Authentication
|
|
60
|
+
|
|
61
|
+
The `authenticate` block is evaluated in the context of each request's controller instance, so any helper method available to controllers (e.g. `current_user` from Devise) works directly. If the block returns `false` or `nil`, the engine falls back to HTTP Basic authentication. If no `authenticate` block is configured, the dashboard is open.
|
|
62
|
+
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
- Ruby >= 3.3
|
|
66
|
+
- Rails >= 8.1.3
|
|
67
|
+
- [solid_queue](https://github.com/rails/solid_queue) >= 1.0
|
|
68
|
+
- [solid_cache](https://github.com/rails/solid_cache) >= 1.0
|
|
69
|
+
- [solid_cable](https://github.com/rails/solid_cable) >= 1.0
|
|
70
|
+
|
|
71
|
+
## Contributing
|
|
72
|
+
|
|
73
|
+
1. Fork the repository
|
|
74
|
+
2. Create a feature branch (`git checkout -b feat/my-feature`)
|
|
75
|
+
3. Run the test suite: `bundle exec rake`
|
|
76
|
+
4. Open a pull request
|
|
77
|
+
|
|
78
|
+
Bug reports and feature requests are welcome on [GitHub Issues](https://github.com/eclectic-coding/solid_stack_web/issues).
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
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,41 @@
|
|
|
1
|
+
require "bundler/setup"
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rubocop/rake_task"
|
|
5
|
+
require "rspec/core/rake_task"
|
|
6
|
+
require "bundler/audit/task"
|
|
7
|
+
|
|
8
|
+
RuboCop::RakeTask.new
|
|
9
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
10
|
+
Bundler::Audit::Task.new
|
|
11
|
+
|
|
12
|
+
desc "Run bundler-audit, rubocop, and rspec (full CI suite)"
|
|
13
|
+
task ci: ["bundle:audit:update", "bundle:audit:check", :rubocop, :spec]
|
|
14
|
+
|
|
15
|
+
task default: :ci
|
|
16
|
+
|
|
17
|
+
namespace :dev do
|
|
18
|
+
def dummy_env
|
|
19
|
+
ENV["RAILS_ENV"] = "development"
|
|
20
|
+
require File.expand_path("spec/dummy/config/environment", __dir__)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
desc "Create and migrate the dummy app development database"
|
|
24
|
+
task :setup do
|
|
25
|
+
dummy_env
|
|
26
|
+
ActiveRecord::Tasks::DatabaseTasks.root = File.expand_path("spec/dummy", __dir__)
|
|
27
|
+
db_config = ActiveRecord::Base.configurations.find_db_config("development")
|
|
28
|
+
ActiveRecord::Tasks::DatabaseTasks.create(db_config)
|
|
29
|
+
load File.expand_path("spec/dummy/db/schema.rb", __dir__)
|
|
30
|
+
puts "Development database ready."
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
desc "Seed the dummy app development database with fake data"
|
|
34
|
+
task :seed do
|
|
35
|
+
dummy_env
|
|
36
|
+
load File.expand_path("spec/dummy/db/seeds.rb", __dir__)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
desc "Reset and reseed the dummy app development database"
|
|
40
|
+
task reset: [:setup, :seed]
|
|
41
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--bg: #f8f9fa;
|
|
5
|
+
--surface: #ffffff;
|
|
6
|
+
--border: #dee2e6;
|
|
7
|
+
--text: #212529;
|
|
8
|
+
--muted: #6c757d;
|
|
9
|
+
--primary: #0d6efd;
|
|
10
|
+
--danger: #dc3545;
|
|
11
|
+
--warning: #fd7e14;
|
|
12
|
+
--success: #198754;
|
|
13
|
+
--info: #0dcaf0;
|
|
14
|
+
--purple: #6f42c1;
|
|
15
|
+
--radius: 6px;
|
|
16
|
+
--shadow: 0 1px 3px rgba(0,0,0,.08);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
body {
|
|
20
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
21
|
+
font-size: 14px;
|
|
22
|
+
line-height: 1.5;
|
|
23
|
+
color: var(--text);
|
|
24
|
+
background: var(--bg);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
a { color: var(--primary); text-decoration: none; }
|
|
28
|
+
a:hover { text-decoration: underline; }
|
|
29
|
+
|
|
30
|
+
.sqw-monospace { font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; font-size: 13px; }
|
|
31
|
+
.sqw-muted { color: var(--muted); }
|
|
32
|
+
.sqw-truncate { max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
.sqw-header {
|
|
2
|
+
background: var(--surface);
|
|
3
|
+
border-bottom: 1px solid var(--border);
|
|
4
|
+
box-shadow: var(--shadow);
|
|
5
|
+
position: sticky;
|
|
6
|
+
top: 0;
|
|
7
|
+
z-index: 100;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.sqw-header__inner {
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
gap: 2rem;
|
|
14
|
+
max-width: 1200px;
|
|
15
|
+
margin: 0 auto;
|
|
16
|
+
padding: 0 1.5rem;
|
|
17
|
+
height: 52px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.sqw-header__logo {
|
|
21
|
+
font-weight: 700;
|
|
22
|
+
font-size: 16px;
|
|
23
|
+
color: var(--text);
|
|
24
|
+
white-space: nowrap;
|
|
25
|
+
}
|
|
26
|
+
.sqw-header__logo:hover { text-decoration: none; color: var(--primary); }
|
|
27
|
+
|
|
28
|
+
.sqw-nav { display: flex; gap: 0.25rem; }
|
|
29
|
+
|
|
30
|
+
.sqw-nav__link {
|
|
31
|
+
padding: 0.35rem 0.75rem;
|
|
32
|
+
border-radius: var(--radius);
|
|
33
|
+
color: var(--muted);
|
|
34
|
+
font-size: 13px;
|
|
35
|
+
font-weight: 500;
|
|
36
|
+
transition: background 0.1s, color 0.1s;
|
|
37
|
+
}
|
|
38
|
+
.sqw-nav__link:hover { background: var(--bg); color: var(--text); text-decoration: none; }
|
|
39
|
+
.sqw-nav__link--active { background: var(--bg); color: var(--text); }
|
|
40
|
+
|
|
41
|
+
.sqw-subnav {
|
|
42
|
+
background: var(--bg);
|
|
43
|
+
border-bottom: 1px solid var(--border);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.sqw-subnav__inner {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: 0.125rem;
|
|
50
|
+
max-width: 1200px;
|
|
51
|
+
margin: 0 auto;
|
|
52
|
+
padding: 0 1.5rem;
|
|
53
|
+
height: 36px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.sqw-subnav__link {
|
|
57
|
+
padding: 0.2rem 0.625rem;
|
|
58
|
+
border-radius: var(--radius);
|
|
59
|
+
color: var(--muted);
|
|
60
|
+
font-size: 12px;
|
|
61
|
+
font-weight: 500;
|
|
62
|
+
transition: background 0.1s, color 0.1s;
|
|
63
|
+
}
|
|
64
|
+
.sqw-subnav__link:hover { background: var(--surface); color: var(--text); text-decoration: none; }
|
|
65
|
+
.sqw-subnav__link--active { background: var(--surface); color: var(--text); }
|
|
66
|
+
|
|
67
|
+
.sqw-main {
|
|
68
|
+
max-width: 1200px;
|
|
69
|
+
margin: 0 auto;
|
|
70
|
+
padding: 1.5rem;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.sqw-page-header { margin-bottom: 1.25rem; }
|
|
74
|
+
.sqw-page-title { font-size: 20px; font-weight: 600; }
|
|
75
|
+
|
|
76
|
+
.sqw-flash {
|
|
77
|
+
padding: 0.75rem 1rem;
|
|
78
|
+
border-radius: var(--radius);
|
|
79
|
+
margin-bottom: 1rem;
|
|
80
|
+
font-size: 13px;
|
|
81
|
+
}
|
|
82
|
+
.sqw-flash--notice { background: #d1e7dd; color: #0a3622; border: 1px solid #a3cfbb; }
|
|
83
|
+
.sqw-flash--alert { background: #f8d7da; color: #58151c; border: 1px solid #f1aeb5; }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
.sqw-stats-grid {
|
|
2
|
+
display: grid;
|
|
3
|
+
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
|
4
|
+
gap: 1rem;
|
|
5
|
+
margin-bottom: 1.5rem;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.sqw-stat {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
gap: 0.5rem;
|
|
12
|
+
background: var(--surface);
|
|
13
|
+
border: 1px solid var(--border);
|
|
14
|
+
border-radius: var(--radius);
|
|
15
|
+
padding: 1rem 1.25rem;
|
|
16
|
+
box-shadow: var(--shadow);
|
|
17
|
+
color: var(--text);
|
|
18
|
+
transition: box-shadow 0.15s;
|
|
19
|
+
}
|
|
20
|
+
a.sqw-stat:hover { box-shadow: 0 3px 8px rgba(0,0,0,.12); text-decoration: none; }
|
|
21
|
+
|
|
22
|
+
.sqw-stat__label { font-size: 12px; font-weight: 500; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
|
|
23
|
+
.sqw-stat__value { font-size: 28px; font-weight: 700; line-height: 1; }
|
|
24
|
+
|
|
25
|
+
.sqw-stat--ready .sqw-stat__value { color: var(--success); }
|
|
26
|
+
.sqw-stat--scheduled .sqw-stat__value { color: var(--info); }
|
|
27
|
+
.sqw-stat--claimed .sqw-stat__value { color: var(--primary); }
|
|
28
|
+
.sqw-stat--blocked .sqw-stat__value { color: var(--warning); }
|
|
29
|
+
.sqw-stat--failed .sqw-stat__value { color: var(--danger); }
|
|
30
|
+
.sqw-stat--cache .sqw-stat__value { color: var(--purple); }
|
|
31
|
+
.sqw-stat--cable .sqw-stat__value { color: var(--info); }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
.sqw-table {
|
|
2
|
+
width: 100%;
|
|
3
|
+
border-collapse: collapse;
|
|
4
|
+
background: var(--surface);
|
|
5
|
+
border: 1px solid var(--border);
|
|
6
|
+
border-radius: var(--radius);
|
|
7
|
+
box-shadow: var(--shadow);
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.sqw-table th,
|
|
12
|
+
.sqw-table td {
|
|
13
|
+
padding: 0.6rem 0.875rem;
|
|
14
|
+
text-align: left;
|
|
15
|
+
border-bottom: 1px solid var(--border);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.sqw-table th {
|
|
19
|
+
font-size: 11px;
|
|
20
|
+
font-weight: 600;
|
|
21
|
+
text-transform: uppercase;
|
|
22
|
+
letter-spacing: .05em;
|
|
23
|
+
color: var(--muted);
|
|
24
|
+
background: var(--bg);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.sqw-table tbody tr:last-child td { border-bottom: none; }
|
|
28
|
+
.sqw-table tbody tr:hover { background: #f9fafb; }
|
|
29
|
+
|
|
30
|
+
.sqw-actions { text-align: right; white-space: nowrap; }
|
|
31
|
+
|
|
32
|
+
.sqw-empty {
|
|
33
|
+
background: var(--surface);
|
|
34
|
+
border: 1px solid var(--border);
|
|
35
|
+
border-radius: var(--radius);
|
|
36
|
+
padding: 3rem 1.5rem;
|
|
37
|
+
text-align: center;
|
|
38
|
+
color: var(--muted);
|
|
39
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
.sqw-badge {
|
|
2
|
+
display: inline-block;
|
|
3
|
+
padding: 0.2em 0.55em;
|
|
4
|
+
font-size: 11px;
|
|
5
|
+
font-weight: 600;
|
|
6
|
+
border-radius: 4px;
|
|
7
|
+
text-transform: uppercase;
|
|
8
|
+
letter-spacing: .04em;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.sqw-badge--ready { background: #d1e7dd; color: #0a3622; }
|
|
12
|
+
.sqw-badge--scheduled { background: #cff4fc; color: #055160; }
|
|
13
|
+
.sqw-badge--claimed { background: #cfe2ff; color: #084298; }
|
|
14
|
+
.sqw-badge--blocked { background: #fff3cd; color: #664d03; }
|
|
15
|
+
.sqw-badge--failed { background: #f8d7da; color: #58151c; }
|
|
16
|
+
.sqw-badge--paused { background: #e2e3e5; color: #41464b; }
|
|
17
|
+
.sqw-badge--queue { background: #e9ecef; color: #495057; font-weight: 500; text-transform: none; letter-spacing: 0; }
|
|
18
|
+
.sqw-badge--worker { background: #cfe2ff; color: #084298; }
|
|
19
|
+
.sqw-badge--supervisor { background: #d1e7dd; color: #0a3622; }
|
|
20
|
+
.sqw-badge--dispatcher { background: #fff3cd; color: #664d03; }
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
.sqw-btn {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: 0.3rem;
|
|
5
|
+
padding: 0.4rem 0.875rem;
|
|
6
|
+
font-size: 13px;
|
|
7
|
+
font-weight: 500;
|
|
8
|
+
border: 1px solid transparent;
|
|
9
|
+
border-radius: var(--radius);
|
|
10
|
+
cursor: pointer;
|
|
11
|
+
background: var(--primary);
|
|
12
|
+
color: #fff;
|
|
13
|
+
transition: opacity 0.1s;
|
|
14
|
+
}
|
|
15
|
+
.sqw-btn:hover { opacity: 0.88; text-decoration: none; }
|
|
16
|
+
|
|
17
|
+
.sqw-btn--sm { padding: 0.25rem 0.6rem; font-size: 12px; }
|
|
18
|
+
.sqw-btn--danger { background: var(--danger); }
|
|
19
|
+
.sqw-btn--muted { background: var(--surface); color: var(--text); border-color: var(--border); }
|
|
20
|
+
.sqw-btn--muted:hover { background: var(--bg); }
|
|
21
|
+
|
|
22
|
+
.sqw-tabs {
|
|
23
|
+
display: flex;
|
|
24
|
+
gap: 0.25rem;
|
|
25
|
+
margin-bottom: 1rem;
|
|
26
|
+
border-bottom: 1px solid var(--border);
|
|
27
|
+
padding-bottom: 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.sqw-tab {
|
|
31
|
+
padding: 0.5rem 1rem;
|
|
32
|
+
font-size: 13px;
|
|
33
|
+
font-weight: 500;
|
|
34
|
+
color: var(--muted);
|
|
35
|
+
border-bottom: 2px solid transparent;
|
|
36
|
+
margin-bottom: -1px;
|
|
37
|
+
transition: color 0.1s, border-color 0.1s;
|
|
38
|
+
}
|
|
39
|
+
.sqw-tab:hover { color: var(--text); text-decoration: none; }
|
|
40
|
+
.sqw-tab--active { color: var(--primary); border-bottom-color: var(--primary); }
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
.sqw-gem-grid {
|
|
2
|
+
display: grid;
|
|
3
|
+
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
4
|
+
gap: 1.5rem;
|
|
5
|
+
margin-bottom: 1.5rem;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.sqw-gem-card {
|
|
9
|
+
background: var(--surface);
|
|
10
|
+
border: 1px solid var(--border);
|
|
11
|
+
border-top: 3px solid var(--border);
|
|
12
|
+
border-radius: var(--radius);
|
|
13
|
+
box-shadow: var(--shadow);
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.sqw-gem-card--queue { border-top-color: var(--primary); }
|
|
18
|
+
.sqw-gem-card--cache { border-top-color: var(--purple); }
|
|
19
|
+
.sqw-gem-card--cable { border-top-color: var(--info); }
|
|
20
|
+
|
|
21
|
+
.sqw-gem-card__header {
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
justify-content: space-between;
|
|
25
|
+
padding: 0.875rem 1.25rem;
|
|
26
|
+
border-bottom: 1px solid var(--border);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.sqw-gem-card__title {
|
|
30
|
+
font-size: 13px;
|
|
31
|
+
font-weight: 700;
|
|
32
|
+
text-transform: uppercase;
|
|
33
|
+
letter-spacing: .06em;
|
|
34
|
+
color: var(--text);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.sqw-gem-card__link {
|
|
38
|
+
font-size: 12px;
|
|
39
|
+
font-weight: 500;
|
|
40
|
+
color: var(--muted);
|
|
41
|
+
}
|
|
42
|
+
.sqw-gem-card__link:hover { color: var(--primary); text-decoration: none; }
|
|
43
|
+
|
|
44
|
+
.sqw-gem-card__body {
|
|
45
|
+
display: grid;
|
|
46
|
+
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
|
|
47
|
+
gap: 1.25rem;
|
|
48
|
+
padding: 1.25rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.sqw-inline-stat {
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-direction: column;
|
|
54
|
+
gap: 0.25rem;
|
|
55
|
+
color: var(--text);
|
|
56
|
+
transition: opacity 0.15s;
|
|
57
|
+
}
|
|
58
|
+
a.sqw-inline-stat:hover { opacity: 0.7; text-decoration: none; }
|
|
59
|
+
|
|
60
|
+
.sqw-inline-stat__label {
|
|
61
|
+
font-size: 11px;
|
|
62
|
+
font-weight: 500;
|
|
63
|
+
color: var(--muted);
|
|
64
|
+
text-transform: uppercase;
|
|
65
|
+
letter-spacing: .04em;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.sqw-inline-stat__value { font-size: 28px; font-weight: 700; line-height: 1; }
|
|
69
|
+
|
|
70
|
+
.sqw-inline-stat--ready .sqw-inline-stat__value { color: var(--success); }
|
|
71
|
+
.sqw-inline-stat--scheduled .sqw-inline-stat__value { color: var(--info); }
|
|
72
|
+
.sqw-inline-stat--claimed .sqw-inline-stat__value { color: var(--primary); }
|
|
73
|
+
.sqw-inline-stat--blocked .sqw-inline-stat__value { color: var(--warning); }
|
|
74
|
+
.sqw-inline-stat--failed .sqw-inline-stat__value { color: var(--danger); }
|
|
75
|
+
.sqw-inline-stat--cache .sqw-inline-stat__value { color: var(--purple); }
|
|
76
|
+
.sqw-inline-stat--cable .sqw-inline-stat__value { color: var(--info); }
|
|
77
|
+
.sqw-inline-stat--neutral .sqw-inline-stat__value { color: var(--muted); }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* Styles are loaded inline via the inline_styles helper which globs _*.css files. */
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
class ApplicationController < ActionController::Base
|
|
3
|
+
include Pagy::Method
|
|
4
|
+
|
|
5
|
+
before_action :authenticate!
|
|
6
|
+
around_action :with_database_connection
|
|
7
|
+
|
|
8
|
+
helper_method :current_section
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def current_section
|
|
13
|
+
case controller_name
|
|
14
|
+
when "jobs", "failed_jobs", "queues", "processes" then :queue
|
|
15
|
+
when "cache" then :cache
|
|
16
|
+
when "cable" then :cable
|
|
17
|
+
else :overview
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def with_database_connection
|
|
22
|
+
config = SolidStackWeb.connects_to
|
|
23
|
+
return yield unless config
|
|
24
|
+
|
|
25
|
+
if config.key?(:reading) && config.key?(:writing)
|
|
26
|
+
role = request.get? ? config[:reading] : config[:writing]
|
|
27
|
+
ActiveRecord::Base.connected_to(role: role) { yield }
|
|
28
|
+
else
|
|
29
|
+
ActiveRecord::Base.connected_to(**config) { yield }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def authenticate!
|
|
34
|
+
return unless (auth = SolidStackWeb.authenticate)
|
|
35
|
+
|
|
36
|
+
instance_exec(&auth) || request_basic_auth
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def request_basic_auth
|
|
40
|
+
request_http_basic_authentication("Solid Stack Dashboard")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
class DashboardController < ApplicationController
|
|
3
|
+
def index
|
|
4
|
+
@queue_stats = {
|
|
5
|
+
ready: ::SolidQueue::ReadyExecution.count,
|
|
6
|
+
scheduled: ::SolidQueue::ScheduledExecution.count,
|
|
7
|
+
claimed: ::SolidQueue::ClaimedExecution.count,
|
|
8
|
+
blocked: ::SolidQueue::BlockedExecution.count,
|
|
9
|
+
failed: ::SolidQueue::FailedExecution.count,
|
|
10
|
+
processes: ::SolidQueue::Process.count
|
|
11
|
+
}
|
|
12
|
+
@cache_stats = {
|
|
13
|
+
entries: ::SolidCache::Entry.count,
|
|
14
|
+
byte_size: ::SolidCache::Entry.sum(:byte_size)
|
|
15
|
+
}
|
|
16
|
+
@cable_stats = {
|
|
17
|
+
messages: ::SolidCable::Message.count,
|
|
18
|
+
channels: ::SolidCable::Message.distinct.count(:channel)
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
class FailedJobsController < ApplicationController
|
|
3
|
+
def index
|
|
4
|
+
scope = ::SolidQueue::FailedExecution.includes(:job).order(created_at: :desc)
|
|
5
|
+
@pagy, @executions = pagy(scope)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def destroy
|
|
9
|
+
execution = ::SolidQueue::FailedExecution.find(params[:id])
|
|
10
|
+
execution.job.destroy!
|
|
11
|
+
@executions_remain = ::SolidQueue::FailedExecution.exists?
|
|
12
|
+
|
|
13
|
+
respond_to do |format|
|
|
14
|
+
format.html { redirect_to failed_jobs_path }
|
|
15
|
+
format.turbo_stream
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def retry
|
|
20
|
+
execution = ::SolidQueue::FailedExecution.find(params[:id])
|
|
21
|
+
execution.retry
|
|
22
|
+
redirect_to failed_jobs_path
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
class JobsController < ApplicationController
|
|
3
|
+
EXECUTION_MODELS = {
|
|
4
|
+
"ready" => ::SolidQueue::ReadyExecution,
|
|
5
|
+
"scheduled" => ::SolidQueue::ScheduledExecution,
|
|
6
|
+
"claimed" => ::SolidQueue::ClaimedExecution,
|
|
7
|
+
"blocked" => ::SolidQueue::BlockedExecution
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
DISCARDABLE = %w[ready scheduled blocked].freeze
|
|
11
|
+
|
|
12
|
+
before_action :set_status
|
|
13
|
+
before_action :require_discardable, only: :destroy
|
|
14
|
+
|
|
15
|
+
def index
|
|
16
|
+
scope = EXECUTION_MODELS[@status].includes(:job).order(created_at: :desc)
|
|
17
|
+
@pagy, @executions = pagy(scope)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def destroy
|
|
21
|
+
model = EXECUTION_MODELS[@status]
|
|
22
|
+
execution = model.find(params[:id])
|
|
23
|
+
execution.job.destroy!
|
|
24
|
+
@executions_remain = model.exists?
|
|
25
|
+
|
|
26
|
+
respond_to do |format|
|
|
27
|
+
format.html { redirect_to jobs_path(status: @status) }
|
|
28
|
+
format.turbo_stream
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def set_status
|
|
35
|
+
@status = params[:status].presence_in(EXECUTION_MODELS.keys) || "ready"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def require_discardable
|
|
39
|
+
head :unprocessable_entity unless DISCARDABLE.include?(@status)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
class QueuesController < ApplicationController
|
|
3
|
+
def index
|
|
4
|
+
queue_names = ::SolidQueue::ReadyExecution.distinct.pluck(:queue_name)
|
|
5
|
+
paused = ::SolidQueue::Pause.pluck(:queue_name).to_set
|
|
6
|
+
|
|
7
|
+
@queues = queue_names.sort.map do |name|
|
|
8
|
+
{
|
|
9
|
+
name: name,
|
|
10
|
+
size: ::SolidQueue::ReadyExecution.where(queue_name: name).count,
|
|
11
|
+
paused: paused.include?(name)
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def pause
|
|
17
|
+
::SolidQueue::Pause.find_or_create_by!(queue_name: params[:id])
|
|
18
|
+
redirect_to queues_path
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def resume
|
|
22
|
+
::SolidQueue::Pause.find_by(queue_name: params[:id])&.destroy
|
|
23
|
+
redirect_to queues_path
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
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 Stack Dashboard</title>
|
|
7
|
+
<%= csrf_meta_tags %>
|
|
8
|
+
<%= csp_meta_tag %>
|
|
9
|
+
<%= inline_styles %>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<header class="sqw-header">
|
|
13
|
+
<div class="sqw-header__inner">
|
|
14
|
+
<%= link_to "Solid Stack", root_path, class: "sqw-header__logo" %>
|
|
15
|
+
<nav class="sqw-nav">
|
|
16
|
+
<%= link_to "Queue", jobs_path,
|
|
17
|
+
class: "sqw-nav__link#{" sqw-nav__link--active" if current_section == :queue}" %>
|
|
18
|
+
<%= link_to "Cache", cache_path,
|
|
19
|
+
class: "sqw-nav__link#{" sqw-nav__link--active" if current_section == :cache}" %>
|
|
20
|
+
<%= link_to "Cable", cable_path,
|
|
21
|
+
class: "sqw-nav__link#{" sqw-nav__link--active" if current_section == :cable}" %>
|
|
22
|
+
</nav>
|
|
23
|
+
</div>
|
|
24
|
+
</header>
|
|
25
|
+
|
|
26
|
+
<% if current_section == :queue %>
|
|
27
|
+
<nav class="sqw-subnav">
|
|
28
|
+
<div class="sqw-subnav__inner">
|
|
29
|
+
<%= link_to "Jobs", jobs_path,
|
|
30
|
+
class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "jobs"}" %>
|
|
31
|
+
<%= link_to "Failed", failed_jobs_path,
|
|
32
|
+
class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "failed_jobs"}" %>
|
|
33
|
+
<%= link_to "Queues", queues_path,
|
|
34
|
+
class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "queues"}" %>
|
|
35
|
+
<%= link_to "Processes", processes_path,
|
|
36
|
+
class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "processes"}" %>
|
|
37
|
+
</div>
|
|
38
|
+
</nav>
|
|
39
|
+
<% end %>
|
|
40
|
+
|
|
41
|
+
<main class="sqw-main">
|
|
42
|
+
<% flash.each do |type, message| %>
|
|
43
|
+
<div class="sqw-flash sqw-flash--<%= type %>"><%= message %></div>
|
|
44
|
+
<% end %>
|
|
45
|
+
<%= yield %>
|
|
46
|
+
</main>
|
|
47
|
+
</body>
|
|
48
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<div class="sqw-page-header">
|
|
2
|
+
<h1 class="sqw-page-title">Solid Cable</h1>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div class="sqw-stats-grid">
|
|
6
|
+
<div class="sqw-stat sqw-stat--cable">
|
|
7
|
+
<span class="sqw-stat__label">Total Messages</span>
|
|
8
|
+
<span class="sqw-stat__value"><%= @total_messages %></span>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="sqw-stat sqw-stat--cable">
|
|
11
|
+
<span class="sqw-stat__label">Channels</span>
|
|
12
|
+
<span class="sqw-stat__value"><%= @channels.size %></span>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<% if @channels.any? %>
|
|
17
|
+
<table class="sqw-table">
|
|
18
|
+
<thead>
|
|
19
|
+
<tr><th>Channel</th></tr>
|
|
20
|
+
</thead>
|
|
21
|
+
<tbody>
|
|
22
|
+
<% @channels.each do |channel| %>
|
|
23
|
+
<tr><td class="sqw-monospace"><%= channel %></td></tr>
|
|
24
|
+
<% end %>
|
|
25
|
+
</tbody>
|
|
26
|
+
</table>
|
|
27
|
+
<% end %>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<div class="sqw-page-header">
|
|
2
|
+
<h1 class="sqw-page-title">Solid Cache</h1>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div class="sqw-stats-grid">
|
|
6
|
+
<div class="sqw-stat sqw-stat--cache">
|
|
7
|
+
<span class="sqw-stat__label">Total Entries</span>
|
|
8
|
+
<span class="sqw-stat__value"><%= @total_entries %></span>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="sqw-stat sqw-stat--cache">
|
|
11
|
+
<span class="sqw-stat__label">Total Size</span>
|
|
12
|
+
<span class="sqw-stat__value"><%= number_to_human_size(@total_byte_size) %></span>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<div class="sqw-page-header">
|
|
2
|
+
<h1 class="sqw-page-title">Overview</h1>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div class="sqw-gem-grid">
|
|
6
|
+
<div class="sqw-gem-card sqw-gem-card--queue">
|
|
7
|
+
<div class="sqw-gem-card__header">
|
|
8
|
+
<span class="sqw-gem-card__title">Solid Queue</span>
|
|
9
|
+
<%= link_to "View Jobs →", jobs_path, class: "sqw-gem-card__link" %>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="sqw-gem-card__body">
|
|
12
|
+
<a href="<%= jobs_path(status: "ready") %>" class="sqw-inline-stat sqw-inline-stat--ready">
|
|
13
|
+
<span class="sqw-inline-stat__label">Ready</span>
|
|
14
|
+
<span class="sqw-inline-stat__value"><%= @queue_stats[:ready] %></span>
|
|
15
|
+
</a>
|
|
16
|
+
<a href="<%= jobs_path(status: "scheduled") %>" class="sqw-inline-stat sqw-inline-stat--scheduled">
|
|
17
|
+
<span class="sqw-inline-stat__label">Scheduled</span>
|
|
18
|
+
<span class="sqw-inline-stat__value"><%= @queue_stats[:scheduled] %></span>
|
|
19
|
+
</a>
|
|
20
|
+
<a href="<%= jobs_path(status: "claimed") %>" class="sqw-inline-stat sqw-inline-stat--claimed">
|
|
21
|
+
<span class="sqw-inline-stat__label">Running</span>
|
|
22
|
+
<span class="sqw-inline-stat__value"><%= @queue_stats[:claimed] %></span>
|
|
23
|
+
</a>
|
|
24
|
+
<a href="<%= jobs_path(status: "blocked") %>" class="sqw-inline-stat sqw-inline-stat--blocked">
|
|
25
|
+
<span class="sqw-inline-stat__label">Blocked</span>
|
|
26
|
+
<span class="sqw-inline-stat__value"><%= @queue_stats[:blocked] %></span>
|
|
27
|
+
</a>
|
|
28
|
+
<a href="<%= failed_jobs_path %>" class="sqw-inline-stat sqw-inline-stat--failed">
|
|
29
|
+
<span class="sqw-inline-stat__label">Failed</span>
|
|
30
|
+
<span class="sqw-inline-stat__value"><%= @queue_stats[:failed] %></span>
|
|
31
|
+
</a>
|
|
32
|
+
<a href="<%= processes_path %>" class="sqw-inline-stat sqw-inline-stat--neutral">
|
|
33
|
+
<span class="sqw-inline-stat__label">Processes</span>
|
|
34
|
+
<span class="sqw-inline-stat__value"><%= @queue_stats[:processes] %></span>
|
|
35
|
+
</a>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="sqw-gem-card sqw-gem-card--cache">
|
|
40
|
+
<div class="sqw-gem-card__header">
|
|
41
|
+
<span class="sqw-gem-card__title">Solid Cache</span>
|
|
42
|
+
<%= link_to "View Cache →", cache_path, class: "sqw-gem-card__link" %>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="sqw-gem-card__body">
|
|
45
|
+
<div class="sqw-inline-stat sqw-inline-stat--cache">
|
|
46
|
+
<span class="sqw-inline-stat__label">Entries</span>
|
|
47
|
+
<span class="sqw-inline-stat__value"><%= @cache_stats[:entries] %></span>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="sqw-inline-stat sqw-inline-stat--cache">
|
|
50
|
+
<span class="sqw-inline-stat__label">Size</span>
|
|
51
|
+
<span class="sqw-inline-stat__value"><%= number_to_human_size(@cache_stats[:byte_size]) %></span>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="sqw-gem-card sqw-gem-card--cable">
|
|
57
|
+
<div class="sqw-gem-card__header">
|
|
58
|
+
<span class="sqw-gem-card__title">Solid Cable</span>
|
|
59
|
+
<%= link_to "View Cable →", cable_path, class: "sqw-gem-card__link" %>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="sqw-gem-card__body">
|
|
62
|
+
<div class="sqw-inline-stat sqw-inline-stat--cable">
|
|
63
|
+
<span class="sqw-inline-stat__label">Messages</span>
|
|
64
|
+
<span class="sqw-inline-stat__value"><%= @cable_stats[:messages] %></span>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="sqw-inline-stat sqw-inline-stat--cable">
|
|
67
|
+
<span class="sqw-inline-stat__label">Channels</span>
|
|
68
|
+
<span class="sqw-inline-stat__value"><%= @cable_stats[:channels] %></span>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<div class="sqw-page-header">
|
|
2
|
+
<h1 class="sqw-page-title">Failed Jobs</h1>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div id="sqw-jobs-table">
|
|
6
|
+
<% if @executions.any? %>
|
|
7
|
+
<table class="sqw-table">
|
|
8
|
+
<thead>
|
|
9
|
+
<tr>
|
|
10
|
+
<th>Job Class</th>
|
|
11
|
+
<th>Queue</th>
|
|
12
|
+
<th>Error</th>
|
|
13
|
+
<th>Failed At</th>
|
|
14
|
+
<th></th>
|
|
15
|
+
</tr>
|
|
16
|
+
</thead>
|
|
17
|
+
<tbody>
|
|
18
|
+
<% @executions.each do |execution| %>
|
|
19
|
+
<tr id="execution_<%= execution.id %>">
|
|
20
|
+
<td class="sqw-monospace"><%= execution.job.class_name %></td>
|
|
21
|
+
<td><span class="sqw-badge sqw-badge--queue"><%= execution.job.queue_name %></span></td>
|
|
22
|
+
<td class="sqw-muted sqw-truncate"><%= execution.error&.lines&.first&.strip %></td>
|
|
23
|
+
<td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
|
|
24
|
+
<td class="sqw-actions">
|
|
25
|
+
<%= button_to "Retry", retry_failed_job_path(execution),
|
|
26
|
+
method: :post, class: "sqw-btn sqw-btn--sm" %>
|
|
27
|
+
<%= button_to "Discard", failed_job_path(execution),
|
|
28
|
+
method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
|
|
29
|
+
data: { turbo_confirm: "Discard this job?" } %>
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
<% end %>
|
|
33
|
+
</tbody>
|
|
34
|
+
</table>
|
|
35
|
+
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
|
|
36
|
+
<% else %>
|
|
37
|
+
<div class="sqw-empty">
|
|
38
|
+
<p>No failed jobs.</p>
|
|
39
|
+
</div>
|
|
40
|
+
<% end %>
|
|
41
|
+
</div>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<div class="sqw-page-header">
|
|
2
|
+
<h1 class="sqw-page-title">Jobs</h1>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div class="sqw-tabs">
|
|
6
|
+
<% [["ready", "Ready"], ["scheduled", "Scheduled"], ["claimed", "Running"], ["blocked", "Blocked"]].each do |status, label| %>
|
|
7
|
+
<%= link_to label, jobs_path(status: status),
|
|
8
|
+
class: "sqw-tab #{"sqw-tab--active" if @status == status}" %>
|
|
9
|
+
<% end %>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div id="sqw-jobs-table">
|
|
13
|
+
<% if @executions.any? %>
|
|
14
|
+
<table class="sqw-table">
|
|
15
|
+
<thead>
|
|
16
|
+
<tr>
|
|
17
|
+
<th>Job Class</th>
|
|
18
|
+
<th>Queue</th>
|
|
19
|
+
<th>Priority</th>
|
|
20
|
+
<th>Enqueued At</th>
|
|
21
|
+
<% if @status == "scheduled" %><th>Scheduled At</th><% end %>
|
|
22
|
+
<th></th>
|
|
23
|
+
</tr>
|
|
24
|
+
</thead>
|
|
25
|
+
<tbody>
|
|
26
|
+
<% @executions.each do |execution| %>
|
|
27
|
+
<tr id="execution_<%= execution.id %>">
|
|
28
|
+
<td class="sqw-monospace"><%= execution.job.class_name %></td>
|
|
29
|
+
<td><span class="sqw-badge sqw-badge--queue"><%= execution.job.queue_name %></span></td>
|
|
30
|
+
<td><%= execution.job.priority %></td>
|
|
31
|
+
<td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
|
|
32
|
+
<% if @status == "scheduled" %>
|
|
33
|
+
<td class="sqw-muted"><%= execution.scheduled_at&.strftime("%b %d %H:%M") %></td>
|
|
34
|
+
<% end %>
|
|
35
|
+
<td class="sqw-actions">
|
|
36
|
+
<% if %w[ready scheduled blocked].include?(@status) %>
|
|
37
|
+
<%= button_to "Discard", job_path(execution, status: @status),
|
|
38
|
+
method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
|
|
39
|
+
data: { turbo_confirm: "Discard this job?" } %>
|
|
40
|
+
<% end %>
|
|
41
|
+
</td>
|
|
42
|
+
</tr>
|
|
43
|
+
<% end %>
|
|
44
|
+
</tbody>
|
|
45
|
+
</table>
|
|
46
|
+
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
|
|
47
|
+
<% else %>
|
|
48
|
+
<%= render "empty" %>
|
|
49
|
+
<% end %>
|
|
50
|
+
</div>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<div class="sqw-page-header">
|
|
2
|
+
<h1 class="sqw-page-title">Processes</h1>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<% if @processes.any? %>
|
|
6
|
+
<table class="sqw-table">
|
|
7
|
+
<thead>
|
|
8
|
+
<tr>
|
|
9
|
+
<th>Kind</th>
|
|
10
|
+
<th>Name</th>
|
|
11
|
+
<th>PID</th>
|
|
12
|
+
<th>Host</th>
|
|
13
|
+
<th>Last Heartbeat</th>
|
|
14
|
+
</tr>
|
|
15
|
+
</thead>
|
|
16
|
+
<tbody>
|
|
17
|
+
<% @processes.each do |process| %>
|
|
18
|
+
<tr>
|
|
19
|
+
<td><span class="sqw-badge sqw-badge--<%= process.kind.downcase %>"><%= process.kind %></span></td>
|
|
20
|
+
<td class="sqw-monospace"><%= process.name %></td>
|
|
21
|
+
<td class="sqw-monospace"><%= process.pid %></td>
|
|
22
|
+
<td class="sqw-muted"><%= process.hostname %></td>
|
|
23
|
+
<td class="sqw-muted"><%= process.last_heartbeat_at&.strftime("%b %d %H:%M:%S") %></td>
|
|
24
|
+
</tr>
|
|
25
|
+
<% end %>
|
|
26
|
+
</tbody>
|
|
27
|
+
</table>
|
|
28
|
+
<% else %>
|
|
29
|
+
<div class="sqw-empty">
|
|
30
|
+
<p>No active processes.</p>
|
|
31
|
+
</div>
|
|
32
|
+
<% end %>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<div class="sqw-page-header">
|
|
2
|
+
<h1 class="sqw-page-title">Queues</h1>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<% if @queues.any? %>
|
|
6
|
+
<table class="sqw-table">
|
|
7
|
+
<thead>
|
|
8
|
+
<tr>
|
|
9
|
+
<th>Name</th>
|
|
10
|
+
<th>Size</th>
|
|
11
|
+
<th>Status</th>
|
|
12
|
+
<th></th>
|
|
13
|
+
</tr>
|
|
14
|
+
</thead>
|
|
15
|
+
<tbody>
|
|
16
|
+
<% @queues.each do |queue| %>
|
|
17
|
+
<tr>
|
|
18
|
+
<td class="sqw-monospace"><%= queue[:name] %></td>
|
|
19
|
+
<td><%= queue[:size] %></td>
|
|
20
|
+
<td>
|
|
21
|
+
<% if queue[:paused] %>
|
|
22
|
+
<span class="sqw-badge sqw-badge--paused">Paused</span>
|
|
23
|
+
<% else %>
|
|
24
|
+
<span class="sqw-badge sqw-badge--ready">Running</span>
|
|
25
|
+
<% end %>
|
|
26
|
+
</td>
|
|
27
|
+
<td class="sqw-actions">
|
|
28
|
+
<% if queue[:paused] %>
|
|
29
|
+
<%= button_to "Resume", resume_queue_path(queue[:name]),
|
|
30
|
+
method: :delete, class: "sqw-btn sqw-btn--sm" %>
|
|
31
|
+
<% else %>
|
|
32
|
+
<%= button_to "Pause", pause_queue_path(queue[:name]),
|
|
33
|
+
method: :post, class: "sqw-btn sqw-btn--sm" %>
|
|
34
|
+
<% end %>
|
|
35
|
+
</td>
|
|
36
|
+
</tr>
|
|
37
|
+
<% end %>
|
|
38
|
+
</tbody>
|
|
39
|
+
</table>
|
|
40
|
+
<% else %>
|
|
41
|
+
<div class="sqw-empty">
|
|
42
|
+
<p>No queues with ready jobs.</p>
|
|
43
|
+
</div>
|
|
44
|
+
<% end %>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
SolidStackWeb::Engine.routes.draw do
|
|
2
|
+
root to: "dashboard#index"
|
|
3
|
+
|
|
4
|
+
resources :jobs, only: [:index, :destroy]
|
|
5
|
+
|
|
6
|
+
resources :failed_jobs, only: [:index, :destroy] do
|
|
7
|
+
member { post :retry }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
resources :queues, only: [:index] do
|
|
11
|
+
member do
|
|
12
|
+
post :pause
|
|
13
|
+
delete :resume
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
resources :processes, only: [:index]
|
|
18
|
+
|
|
19
|
+
get "cache", to: "cache#index", as: :cache
|
|
20
|
+
get "cable", to: "cable#index", as: :cable
|
|
21
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require "pagy"
|
|
2
|
+
require "pagy/toolbox/paginators/method"
|
|
3
|
+
require "solid_queue"
|
|
4
|
+
require "solid_cache"
|
|
5
|
+
require "solid_cable"
|
|
6
|
+
|
|
7
|
+
module SolidStackWeb
|
|
8
|
+
class Engine < ::Rails::Engine
|
|
9
|
+
isolate_namespace SolidStackWeb
|
|
10
|
+
|
|
11
|
+
config.i18n.load_path += Gem.find_files("pagy/locales/en.yml")
|
|
12
|
+
|
|
13
|
+
initializer "solid_stack_web.pagy" do |app|
|
|
14
|
+
app.config.after_initialize do
|
|
15
|
+
Pagy::OPTIONS[:limit] = SolidStackWeb.page_size
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require "solid_stack_web/version"
|
|
2
|
+
require "solid_stack_web/engine"
|
|
3
|
+
|
|
4
|
+
module SolidStackWeb
|
|
5
|
+
class << self
|
|
6
|
+
attr_writer :page_size, :connects_to
|
|
7
|
+
|
|
8
|
+
def page_size
|
|
9
|
+
@page_size || 25
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def connects_to
|
|
13
|
+
@connects_to
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def configure
|
|
17
|
+
yield self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def authenticate(&block)
|
|
21
|
+
@authenticate = block if block_given?
|
|
22
|
+
@authenticate
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: solid_stack_web
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.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: pagy
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '43.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '43.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: solid_queue
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: solid_cache
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: solid_cable
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.0'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '1.0'
|
|
82
|
+
description: Mount SolidStackWeb in any Rails app using the Solid Stack to get a single
|
|
83
|
+
dashboard covering Solid Queue job monitoring, Solid Cache statistics, and Solid
|
|
84
|
+
Cable connection observability — all without leaving your app.
|
|
85
|
+
email:
|
|
86
|
+
- eclectic-coding@users.noreply.github.com
|
|
87
|
+
executables: []
|
|
88
|
+
extensions: []
|
|
89
|
+
extra_rdoc_files: []
|
|
90
|
+
files:
|
|
91
|
+
- MIT-LICENSE
|
|
92
|
+
- README.md
|
|
93
|
+
- Rakefile
|
|
94
|
+
- app/assets/stylesheets/solid_stack_web/_01_base.css
|
|
95
|
+
- app/assets/stylesheets/solid_stack_web/_02_layout.css
|
|
96
|
+
- app/assets/stylesheets/solid_stack_web/_03_stats.css
|
|
97
|
+
- app/assets/stylesheets/solid_stack_web/_04_table.css
|
|
98
|
+
- app/assets/stylesheets/solid_stack_web/_05_badges.css
|
|
99
|
+
- app/assets/stylesheets/solid_stack_web/_06_buttons.css
|
|
100
|
+
- app/assets/stylesheets/solid_stack_web/_07_dashboard.css
|
|
101
|
+
- app/assets/stylesheets/solid_stack_web/application.css
|
|
102
|
+
- app/controllers/solid_stack_web/application_controller.rb
|
|
103
|
+
- app/controllers/solid_stack_web/cable_controller.rb
|
|
104
|
+
- app/controllers/solid_stack_web/cache_controller.rb
|
|
105
|
+
- app/controllers/solid_stack_web/dashboard_controller.rb
|
|
106
|
+
- app/controllers/solid_stack_web/failed_jobs_controller.rb
|
|
107
|
+
- app/controllers/solid_stack_web/jobs_controller.rb
|
|
108
|
+
- app/controllers/solid_stack_web/processes_controller.rb
|
|
109
|
+
- app/controllers/solid_stack_web/queues_controller.rb
|
|
110
|
+
- app/helpers/solid_stack_web/application_helper.rb
|
|
111
|
+
- app/views/layouts/solid_stack_web/application.html.erb
|
|
112
|
+
- app/views/solid_stack_web/cable/index.html.erb
|
|
113
|
+
- app/views/solid_stack_web/cache/index.html.erb
|
|
114
|
+
- app/views/solid_stack_web/dashboard/index.html.erb
|
|
115
|
+
- app/views/solid_stack_web/failed_jobs/index.html.erb
|
|
116
|
+
- app/views/solid_stack_web/jobs/_empty.html.erb
|
|
117
|
+
- app/views/solid_stack_web/jobs/destroy.turbo_stream.erb
|
|
118
|
+
- app/views/solid_stack_web/jobs/index.html.erb
|
|
119
|
+
- app/views/solid_stack_web/processes/index.html.erb
|
|
120
|
+
- app/views/solid_stack_web/queues/index.html.erb
|
|
121
|
+
- config/routes.rb
|
|
122
|
+
- lib/solid_stack_web.rb
|
|
123
|
+
- lib/solid_stack_web/engine.rb
|
|
124
|
+
- lib/solid_stack_web/version.rb
|
|
125
|
+
- lib/tasks/solid_stack_web_tasks.rake
|
|
126
|
+
homepage: https://github.com/eclectic-coding/solid_stack_web
|
|
127
|
+
licenses:
|
|
128
|
+
- MIT
|
|
129
|
+
metadata:
|
|
130
|
+
homepage_uri: https://github.com/eclectic-coding/solid_stack_web
|
|
131
|
+
source_code_uri: https://github.com/eclectic-coding/solid_stack_web
|
|
132
|
+
changelog_uri: https://github.com/eclectic-coding/solid_stack_web/blob/main/CHANGELOG.md
|
|
133
|
+
rdoc_options: []
|
|
134
|
+
require_paths:
|
|
135
|
+
- lib
|
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
137
|
+
requirements:
|
|
138
|
+
- - ">="
|
|
139
|
+
- !ruby/object:Gem::Version
|
|
140
|
+
version: '3.3'
|
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - ">="
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '0'
|
|
146
|
+
requirements: []
|
|
147
|
+
rubygems_version: 4.0.10
|
|
148
|
+
specification_version: 4
|
|
149
|
+
summary: A unified Rails engine dashboard for Solid Queue, Solid Cache, and Solid
|
|
150
|
+
Cable.
|
|
151
|
+
test_files: []
|