modern_queue_dashboard 0.3.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 +7 -0
- data/.npmignore +25 -0
- data/.npmrc +2 -0
- data/.rubocop.yml +18 -0
- data/CHANGELOG.md +37 -0
- data/CODE_OF_CONDUCT.md +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +168 -0
- data/Rakefile +70 -0
- data/app/assets/builds/modern_queue_dashboard.css +1 -0
- data/app/assets/builds/modern_queue_dashboard.js +19 -0
- data/app/assets/javascripts/modern_queue_dashboard.js +18 -0
- data/app/assets/stylesheets/modern_queue_dashboard.css +8 -0
- data/app/controllers/modern_queue_dashboard/application_controller.rb +7 -0
- data/app/controllers/modern_queue_dashboard/dashboard_controller.rb +10 -0
- data/app/controllers/modern_queue_dashboard/queues_controller.rb +19 -0
- data/app/models/modern_queue_dashboard/metrics.rb +49 -0
- data/app/models/modern_queue_dashboard/queue_summary.rb +52 -0
- data/app/views/layouts/modern_queue_dashboard/application.html.erb +15 -0
- data/app/views/modern_queue_dashboard/dashboard/index.html.erb +51 -0
- data/app/views/modern_queue_dashboard/queues/index.html.erb +35 -0
- data/app/views/modern_queue_dashboard/queues/show.html.erb +51 -0
- data/config/routes.rb +7 -0
- data/docs/PLAN.md +201 -0
- data/lib/modern_queue_dashboard/engine.rb +24 -0
- data/lib/modern_queue_dashboard/metrics.rb +43 -0
- data/lib/modern_queue_dashboard/queue_summary.rb +38 -0
- data/lib/modern_queue_dashboard/version.rb +5 -0
- data/lib/modern_queue_dashboard.rb +35 -0
- data/package-lock.json +1506 -0
- data/package.json +17 -0
- data/sig/modern_queue_dashboard.rbs +4 -0
- data/tailwind.config.js +11 -0
- metadata +196 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8f3b6ba3fdf6b602a2b5b6701717d6768a56ced216ec87b5f8aa36ac94a171c8
|
4
|
+
data.tar.gz: 7b96e4fd86584d611e1c1ac0b68eccc3239aecdcc1261e8bed105afd682bb17a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7280186443a102521984f7987e2a5664c63567cc646f7f5b1867893cdf518ac71643a5051fa40bae050be5bd752fb958ca1b01f14f305d01c643310a44b189b2
|
7
|
+
data.tar.gz: 710daee003dd37bbc49eb854d3ce106b4100660ab105038711304846271238e2058101590a9f32a4215990322dd3c40adb6b76a3056f1cc9e90ed4054e02c806
|
data/.npmignore
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# Ruby gem files
|
2
|
+
*.gem
|
3
|
+
*.gemspec
|
4
|
+
Gemfile
|
5
|
+
Gemfile.lock
|
6
|
+
Rakefile
|
7
|
+
bin/
|
8
|
+
lib/
|
9
|
+
test/
|
10
|
+
app/
|
11
|
+
config/
|
12
|
+
docs/
|
13
|
+
.ruby-version
|
14
|
+
.yardoc
|
15
|
+
.rubocop.yml
|
16
|
+
|
17
|
+
# Other files
|
18
|
+
.git/
|
19
|
+
.github/
|
20
|
+
.gitignore
|
21
|
+
.DS_Store
|
22
|
+
.rspec_status
|
23
|
+
pkg/
|
24
|
+
tmp/
|
25
|
+
coverage/
|
data/.npmrc
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
inherit_gem:
|
2
|
+
rubocop-rails-omakase: rubocop.yml
|
3
|
+
|
4
|
+
AllCops:
|
5
|
+
TargetRubyVersion: 3.3.8
|
6
|
+
NewCops: enable
|
7
|
+
Exclude:
|
8
|
+
- 'test/dummy/**/*'
|
9
|
+
- 'bin/**/*'
|
10
|
+
- 'tmp/**/*'
|
11
|
+
- 'vendor/**/*'
|
12
|
+
- 'node_modules/**/*'
|
13
|
+
|
14
|
+
Style/StringLiterals:
|
15
|
+
EnforcedStyle: double_quotes
|
16
|
+
|
17
|
+
Style/StringLiteralsInInterpolation:
|
18
|
+
EnforcedStyle: double_quotes
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
## [Unreleased]
|
2
|
+
|
3
|
+
## [0.3.1] - 2025-05-20
|
4
|
+
|
5
|
+
- Fixed compatibility with Solid Queue standard schema structure
|
6
|
+
- Updated queries to use `scheduled_at` instead of `run_at`
|
7
|
+
- Improved job status tracking via the appropriate Solid Queue tables
|
8
|
+
- Updated README to clarify that this dashboard is exclusively for Solid Queue
|
9
|
+
|
10
|
+
## [0.3.0] - 2025-05-19
|
11
|
+
|
12
|
+
- Updated tailwindcss-rails dependency to ~> 4.0
|
13
|
+
- Updated turbo-rails dependency to ~> 2.0
|
14
|
+
- Improved compatibility with Rails 8.0.2
|
15
|
+
|
16
|
+
## [0.2.2] - 2025-05-18
|
17
|
+
|
18
|
+
- Fixed CI workflow to run RuboCop and fail the build if there are issues
|
19
|
+
- Added a CHANGELOG.md file
|
20
|
+
- Added a Gemfile.lock file
|
21
|
+
|
22
|
+
## [0.2.1] - 2023-07-30
|
23
|
+
|
24
|
+
- Updated UI color scheme from indigo to sky blue
|
25
|
+
- Fixed Tailwind CSS configuration and build process
|
26
|
+
|
27
|
+
## [0.2.0] - 2023-07-28
|
28
|
+
|
29
|
+
- Added CI workflow with GitHub Actions
|
30
|
+
- Integrated RuboCop with Rails Omakase style
|
31
|
+
- Added proper test setup including unit and integration tests
|
32
|
+
- Improved engine compatibility with Rails 8 asset systems
|
33
|
+
- Added developer documentation and test helpers
|
34
|
+
|
35
|
+
## [0.1.0] - 2023-07-25
|
36
|
+
|
37
|
+
- Initial release
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# Community Guidelines for Conduct
|
2
|
+
|
3
|
+
Modern Queue Dashboard follows the Ruby on Rails guidelines for conduct in all collaborative spaces, including mailing lists, submitted patches, commit comments, and discussions:
|
4
|
+
|
5
|
+
* Participants are expected to be tolerant of opposing views.
|
6
|
+
* Participants must ensure that their language and actions are free from personal attacks and disparaging remarks.
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
8
|
+
* Behavior that could reasonably be considered harassment will not be tolerated.
|
9
|
+
|
10
|
+
## Reporting
|
11
|
+
|
12
|
+
If you believe these guidelines have been violated, please contact the project maintainers.
|
13
|
+
|
14
|
+
## Attribution
|
15
|
+
|
16
|
+
This Code of Conduct is adapted from the [Ruby on Rails Community Guidelines for Conduct](https://rubyonrails.org/conduct).
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Clayton Lengel-Zigich
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
# Modern Queue Dashboard
|
2
|
+
|
3
|
+
A mountable Rails engine that provides a clean, Hotwire-powered dashboard **specifically designed for monitoring [Solid Queue](https://github.com/basecamp/solid_queue)** jobs. Built with Tailwind CSS, Turbo frames, and Stimulus controllers.
|
4
|
+
|
5
|
+

|
6
|
+
|
7
|
+
## Features
|
8
|
+
|
9
|
+
* High-level metrics - counts for pending, scheduled, running, completed and failed jobs
|
10
|
+
* Per-queue statistics - job counts and latency metrics
|
11
|
+
* Job details - arguments, timestamps, state transitions, and error information
|
12
|
+
* Real-time updates - auto-refreshing metrics via Turbo Stream polling
|
13
|
+
* Clean UI - responsive interface using Tailwind CSS
|
14
|
+
* Zero setup - install, mount, and go
|
15
|
+
|
16
|
+
## Requirements
|
17
|
+
|
18
|
+
* Rails 8.0+
|
19
|
+
* Ruby 3.3.8+
|
20
|
+
* **Solid Queue 1.1+** (this dashboard is exclusively for Solid Queue and does not support other job backends)
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Add this line to your application's Gemfile:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
gem "modern_queue_dashboard"
|
28
|
+
```
|
29
|
+
|
30
|
+
And then execute:
|
31
|
+
|
32
|
+
```bash
|
33
|
+
bundle install
|
34
|
+
```
|
35
|
+
|
36
|
+
## Mounting the Dashboard
|
37
|
+
|
38
|
+
Add the following to your `config/routes.rb`:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
Rails.application.routes.draw do
|
42
|
+
# ... your other routes
|
43
|
+
|
44
|
+
# Mount the dashboard at /queue-dashboard
|
45
|
+
mount ModernQueueDashboard::Engine, at: "/queue-dashboard"
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
## Security
|
50
|
+
|
51
|
+
The dashboard doesn't include authentication by itself. You should restrict access using your application's authentication system.
|
52
|
+
|
53
|
+
### With Devise
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
# In config/routes.rb
|
57
|
+
authenticate :user, -> { current_user.admin? } do
|
58
|
+
mount ModernQueueDashboard::Engine, at: "/queue-dashboard"
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
### With Basic Auth
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
# In config/routes.rb
|
66
|
+
require "authenticated_constraint"
|
67
|
+
|
68
|
+
constraints AuthenticatedConstraint.new do
|
69
|
+
mount ModernQueueDashboard::Engine, at: "/queue-dashboard"
|
70
|
+
end
|
71
|
+
|
72
|
+
# In lib/authenticated_constraint.rb
|
73
|
+
class AuthenticatedConstraint
|
74
|
+
def matches?(request)
|
75
|
+
return false unless request.session[:user_id]
|
76
|
+
user = User.find_by(id: request.session[:user_id])
|
77
|
+
user && user.admin?
|
78
|
+
end
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
### With HTTP Basic Auth
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
# In config/routes.rb
|
86
|
+
mount ModernQueueDashboard::Engine, at: "/queue-dashboard", constraints: lambda { |request|
|
87
|
+
ActiveSupport::SecurityUtils.secure_compare(
|
88
|
+
::Digest::SHA256.hexdigest(request.headers["Authorization"].to_s),
|
89
|
+
::Digest::SHA256.hexdigest("Basic #{Base64.encode64("username:password")}")
|
90
|
+
)
|
91
|
+
}
|
92
|
+
```
|
93
|
+
|
94
|
+
## Configuration
|
95
|
+
|
96
|
+
You can configure the dashboard by creating an initializer:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
# config/initializers/modern_queue_dashboard.rb
|
100
|
+
ModernQueueDashboard.configure do |config|
|
101
|
+
config.refresh_interval = 5 # seconds
|
102
|
+
config.time_zone = "UTC"
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
## Development
|
107
|
+
|
108
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run the following:
|
109
|
+
|
110
|
+
```bash
|
111
|
+
# Run unit tests
|
112
|
+
bundle exec rake unit_tests
|
113
|
+
|
114
|
+
# Run unit tests + RuboCop style checks
|
115
|
+
bundle exec rake ci
|
116
|
+
|
117
|
+
# Run just RuboCop
|
118
|
+
bundle exec rubocop
|
119
|
+
```
|
120
|
+
|
121
|
+
### Building Tailwind CSS
|
122
|
+
|
123
|
+
The dashboard uses Tailwind CSS for styling. When making changes to the styles, you need to rebuild the CSS file:
|
124
|
+
|
125
|
+
```bash
|
126
|
+
# Install Node.js dependencies (only needed once)
|
127
|
+
npm install -D tailwindcss
|
128
|
+
|
129
|
+
# Build the CSS file
|
130
|
+
bin/build
|
131
|
+
```
|
132
|
+
|
133
|
+
This will process the Tailwind directives in `app/assets/stylesheets/modern_queue_dashboard.css` and output the compiled CSS to `app/assets/builds/modern_queue_dashboard.css`.
|
134
|
+
|
135
|
+
### Dummy Application
|
136
|
+
|
137
|
+
The gem includes a dummy Rails application in `test/dummy` for integration testing and development:
|
138
|
+
|
139
|
+
```bash
|
140
|
+
# Start the dummy app server
|
141
|
+
cd test/dummy
|
142
|
+
bin/rails server
|
143
|
+
|
144
|
+
# Start the solid_queue worker
|
145
|
+
cd test/dummy
|
146
|
+
bin/rails solid_queue:process
|
147
|
+
```
|
148
|
+
|
149
|
+
Then visit http://localhost:3000 and navigate to http://localhost:3000/queue-dashboard to see the dashboard.
|
150
|
+
|
151
|
+
### CI Process
|
152
|
+
|
153
|
+
The CI pipeline runs on GitHub Actions and includes:
|
154
|
+
- Unit tests for the gem
|
155
|
+
- RuboCop style checks using the Rails Omakase style guide
|
156
|
+
- (Optional) Integration tests with the dummy application
|
157
|
+
|
158
|
+
## Contributing
|
159
|
+
|
160
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/clayton/modern_queue_dashboard. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/clayton/modern_queue_dashboard/blob/main/CODE_OF_CONDUCT.md).
|
161
|
+
|
162
|
+
## License
|
163
|
+
|
164
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
165
|
+
|
166
|
+
## Code of Conduct
|
167
|
+
|
168
|
+
Everyone interacting in the Modern Queue Dashboard project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/clayton/modern_queue_dashboard/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
# Main test task for gem's unit tests
|
7
|
+
Rake::TestTask.new(:unit_tests) do |t|
|
8
|
+
t.libs << "test"
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"].exclude("test/dummy/**/*_test.rb")
|
10
|
+
end
|
11
|
+
|
12
|
+
# Simple task that runs everything in sequence
|
13
|
+
desc "Run all tests and quality checks"
|
14
|
+
task :ci do
|
15
|
+
failed = false
|
16
|
+
|
17
|
+
# Run unit tests
|
18
|
+
puts "\n== Running unit tests =="
|
19
|
+
Rake::Task["unit_tests"].invoke
|
20
|
+
|
21
|
+
# Run dummy app tests if they exist, but don't fail the build if they fail
|
22
|
+
if File.directory?("test/dummy/test/integration")
|
23
|
+
puts "\n== Running integration tests (optional) =="
|
24
|
+
unless system("cd test/dummy && bin/rails test test/integration 2>/dev/null")
|
25
|
+
puts "Integration tests failed or could not run. This is allowed in CI."
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Always run Rubocop and fail the build if there are issues
|
30
|
+
puts "\n== Running RuboCop (Rails Omakase style) =="
|
31
|
+
unless system("bundle exec rubocop")
|
32
|
+
puts "\nRuboCop found issues - failing the build"
|
33
|
+
failed = true
|
34
|
+
end
|
35
|
+
|
36
|
+
# Fail the build if any step failed
|
37
|
+
fail "CI task failed" if failed
|
38
|
+
end
|
39
|
+
|
40
|
+
# More strict version for CI environments
|
41
|
+
desc "Run all tests and quality checks strictly (for CI)"
|
42
|
+
task :ci_strict do
|
43
|
+
failed = false
|
44
|
+
|
45
|
+
# Run unit tests
|
46
|
+
puts "\n== Running unit tests =="
|
47
|
+
Rake::Task["unit_tests"].invoke
|
48
|
+
|
49
|
+
# Run dummy app tests if they exist
|
50
|
+
if File.directory?("test/dummy/test/integration")
|
51
|
+
puts "\n== Running integration tests =="
|
52
|
+
unless system("cd test/dummy && bin/rails test test/integration")
|
53
|
+
puts "Warning: Integration tests failed or could not run"
|
54
|
+
failed = true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Run Rubocop if available
|
59
|
+
if system("which rubocop > /dev/null 2>&1")
|
60
|
+
puts "\n== Running RuboCop =="
|
61
|
+
system("rubocop --format simple")
|
62
|
+
failed = true unless $?.success?
|
63
|
+
end
|
64
|
+
|
65
|
+
# Fail the build if any step failed
|
66
|
+
raise "CI task failed" if failed
|
67
|
+
end
|
68
|
+
|
69
|
+
# Set default task to run everything
|
70
|
+
task default: :ci
|
@@ -0,0 +1 @@
|
|
1
|
+
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.mx-auto{margin-left:auto;margin-right:auto}.mb-4{margin-bottom:1rem}.flex{display:flex}.table{display:table}.grid{display:grid}.min-h-screen{min-height:100vh}.min-w-full{min-width:100%}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.items-center{align-items:center}.gap-4{gap:1rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity,1))}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.border{border-width:1px}.border-red-400{--tw-border-opacity:1;border-color:rgb(248 113 113/var(--tw-border-opacity,1))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity,1))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tracking-wider{letter-spacing:.05em}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-sky-500{--tw-text-opacity:1;color:rgb(14 165 233/var(--tw-text-opacity,1))}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.jobs-container{border-radius:.25rem;--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1));padding:1rem;--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}@media (min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:1024px){.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
// Modern Queue Dashboard JavaScript - This is a placeholder
|
2
|
+
// In a real application, this would be compiled
|
3
|
+
console.log('Modern Queue Dashboard loaded');
|
4
|
+
|
5
|
+
// Add auto-refresh functionality
|
6
|
+
document.addEventListener('turbo:load', () => {
|
7
|
+
const dashboardElement = document.querySelector('.modern-queue-dashboard');
|
8
|
+
|
9
|
+
if (dashboardElement) {
|
10
|
+
// Get refresh interval from data attribute (or default to 5000ms)
|
11
|
+
const refreshInterval = (parseInt(dashboardElement.dataset.refreshInterval) || 5) * 1000;
|
12
|
+
|
13
|
+
console.log(`Setting dashboard refresh interval: ${refreshInterval}ms`);
|
14
|
+
|
15
|
+
setInterval(() => {
|
16
|
+
Turbo.visit(window.location.href, { action: 'replace' });
|
17
|
+
}, refreshInterval);
|
18
|
+
}
|
19
|
+
});
|
@@ -0,0 +1,18 @@
|
|
1
|
+
// Modern Queue Dashboard JavaScript
|
2
|
+
console.log('Modern Queue Dashboard loaded');
|
3
|
+
|
4
|
+
// Add auto-refresh functionality
|
5
|
+
document.addEventListener('turbo:load', () => {
|
6
|
+
const dashboardElement = document.querySelector('.modern-queue-dashboard');
|
7
|
+
|
8
|
+
if (dashboardElement) {
|
9
|
+
// Get refresh interval from data attribute (or default to 5000ms)
|
10
|
+
const refreshInterval = (parseInt(dashboardElement.dataset.refreshInterval) || 5) * 1000;
|
11
|
+
|
12
|
+
console.log(`Setting dashboard refresh interval: ${refreshInterval}ms`);
|
13
|
+
|
14
|
+
setInterval(() => {
|
15
|
+
Turbo.visit(window.location.href, { action: 'replace' });
|
16
|
+
}, refreshInterval);
|
17
|
+
}
|
18
|
+
});
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ModernQueueDashboard
|
4
|
+
class QueuesController < ApplicationController
|
5
|
+
def index
|
6
|
+
@queues = QueueSummary.with_stats
|
7
|
+
end
|
8
|
+
|
9
|
+
def show
|
10
|
+
@queue_name = params[:id]
|
11
|
+
@queue = QueueSummary.with_stats.detect { |q| q.name == @queue_name }
|
12
|
+
|
13
|
+
return if @queue
|
14
|
+
|
15
|
+
flash[:error] = "Queue not found"
|
16
|
+
redirect_to queues_path
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ModernQueueDashboard
|
4
|
+
Metric = Struct.new(:key, :label, :value, keyword_init: true)
|
5
|
+
|
6
|
+
class Metrics
|
7
|
+
class << self
|
8
|
+
def summary
|
9
|
+
[
|
10
|
+
Metric.new(key: :pending, label: "Pending", value: pending_count),
|
11
|
+
Metric.new(key: :scheduled, label: "Scheduled", value: scheduled_count),
|
12
|
+
Metric.new(key: :running, label: "Running", value: running_count),
|
13
|
+
Metric.new(key: :failed, label: "Failed", value: failed_count),
|
14
|
+
Metric.new(key: :completed, label: "Completed", value: completed_count),
|
15
|
+
Metric.new(key: :latency, label: "Latency", value: latency_seconds)
|
16
|
+
]
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def pending_count
|
22
|
+
SolidQueue::Job.where("run_at <= ?", Time.current).count
|
23
|
+
end
|
24
|
+
|
25
|
+
def scheduled_count
|
26
|
+
SolidQueue::Job.where("run_at > ?", Time.current).count
|
27
|
+
end
|
28
|
+
|
29
|
+
def running_count
|
30
|
+
SolidQueue::Execution.where(finished_at: nil).count
|
31
|
+
end
|
32
|
+
|
33
|
+
def failed_count
|
34
|
+
SolidQueue::FailedExecution.count
|
35
|
+
end
|
36
|
+
|
37
|
+
def completed_count
|
38
|
+
SolidQueue::Execution.where.not(finished_at: nil).count
|
39
|
+
end
|
40
|
+
|
41
|
+
def latency_seconds
|
42
|
+
oldest = SolidQueue::Job.order(:created_at).limit(1).pick(:created_at)
|
43
|
+
return 0 unless oldest
|
44
|
+
|
45
|
+
(Time.current - oldest).to_i
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ModernQueueDashboard
|
4
|
+
QueueStat = Struct.new(:name, :pending, :scheduled, :running, :failed, :latency, keyword_init: true)
|
5
|
+
|
6
|
+
class QueueSummary
|
7
|
+
class << self
|
8
|
+
def with_stats
|
9
|
+
queue_names.map { |q| stats_for(q) }.sort_by(&:name)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def queue_names
|
15
|
+
SolidQueue::Job.distinct.pluck(:queue_name)
|
16
|
+
end
|
17
|
+
|
18
|
+
def stats_for(name)
|
19
|
+
# Pending: Jobs scheduled for now or in the past but not finished
|
20
|
+
pending = SolidQueue::Job.where(queue_name: name)
|
21
|
+
.where("scheduled_at <= ? AND finished_at IS NULL", Time.current)
|
22
|
+
.count
|
23
|
+
|
24
|
+
# Scheduled: Jobs scheduled for the future
|
25
|
+
scheduled = SolidQueue::Job.where(queue_name: name)
|
26
|
+
.where("scheduled_at > ? AND finished_at IS NULL", Time.current)
|
27
|
+
.count
|
28
|
+
|
29
|
+
# Running: Jobs that are currently claimed but not finished
|
30
|
+
running = SolidQueue::ClaimedExecution.joins("INNER JOIN solid_queue_jobs ON solid_queue_jobs.id = solid_queue_claimed_executions.job_id")
|
31
|
+
.where("solid_queue_jobs.queue_name = ?", name)
|
32
|
+
.count
|
33
|
+
|
34
|
+
# Failed: Jobs that have failed executions
|
35
|
+
failed = SolidQueue::FailedExecution.joins("INNER JOIN solid_queue_jobs ON solid_queue_jobs.id = solid_queue_failed_executions.job_id")
|
36
|
+
.where("solid_queue_jobs.queue_name = ?", name)
|
37
|
+
.count
|
38
|
+
|
39
|
+
# Latency: Time since oldest unfinished job was scheduled
|
40
|
+
oldest_scheduled_at = SolidQueue::Job.where(queue_name: name)
|
41
|
+
.where(finished_at: nil)
|
42
|
+
.order(:scheduled_at)
|
43
|
+
.limit(1)
|
44
|
+
.pick(:scheduled_at)
|
45
|
+
|
46
|
+
latency = oldest_scheduled_at ? (Time.current - oldest_scheduled_at).to_i : 0
|
47
|
+
|
48
|
+
QueueStat.new(name:, pending:, scheduled:, running:, failed:, latency:)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>ModernQueueDashboard</title>
|
5
|
+
<%= csp_meta_tag %>
|
6
|
+
<%= csrf_meta_tags %>
|
7
|
+
|
8
|
+
<%= stylesheet_link_tag "modern_queue_dashboard", media: "all", "data-turbo-track": "reload" %>
|
9
|
+
<%= javascript_include_tag "modern_queue_dashboard", "data-turbo-track": "reload", defer: true %>
|
10
|
+
</head>
|
11
|
+
|
12
|
+
<body class="bg-gray-100 min-h-screen">
|
13
|
+
<%= yield %>
|
14
|
+
</body>
|
15
|
+
</html>
|
@@ -0,0 +1,51 @@
|
|
1
|
+
<div class="container mx-auto p-6 space-y-6 modern-queue-dashboard"
|
2
|
+
data-refresh-interval="<%= ModernQueueDashboard.configuration.refresh_interval %>">
|
3
|
+
<h1 class="text-3xl font-semibold">Modern Queue Dashboard</h1>
|
4
|
+
|
5
|
+
<div class="text-sm text-gray-500 mb-4">
|
6
|
+
Refresh interval: <%= ModernQueueDashboard.configuration.refresh_interval %> seconds |
|
7
|
+
Timezone: <%= ModernQueueDashboard.configuration.time_zone %>
|
8
|
+
</div>
|
9
|
+
|
10
|
+
<!-- Metric Cards -->
|
11
|
+
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
12
|
+
<% @metrics.each do |metric| %>
|
13
|
+
<turbo-frame id="metric_<%= metric.key %>">
|
14
|
+
<div class="bg-white shadow-sm rounded p-4 text-center">
|
15
|
+
<p class="text-sm text-gray-500"><%= metric.label %></p>
|
16
|
+
<p class="text-2xl font-bold text-sky-500"><%= metric.value %></p>
|
17
|
+
</div>
|
18
|
+
</turbo-frame>
|
19
|
+
<% end %>
|
20
|
+
</div>
|
21
|
+
|
22
|
+
<!-- Queue Table -->
|
23
|
+
<div class="bg-white shadow rounded">
|
24
|
+
<table class="min-w-full divide-y divide-gray-200">
|
25
|
+
<thead class="bg-gray-50">
|
26
|
+
<tr>
|
27
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Queue</th>
|
28
|
+
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Pending</th>
|
29
|
+
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Scheduled</th>
|
30
|
+
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Running</th>
|
31
|
+
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Failed</th>
|
32
|
+
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Latency</th>
|
33
|
+
</tr>
|
34
|
+
</thead>
|
35
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
36
|
+
<% @queues.each do |queue| %>
|
37
|
+
<tr>
|
38
|
+
<td class="px-6 py-4 whitespace-nowrap font-medium text-sky-500">
|
39
|
+
<%= link_to queue.name, queue_path(queue.name), data: { turbo_frame: "_top" } %>
|
40
|
+
</td>
|
41
|
+
<td class="px-6 py-4 text-right"><%= queue.pending %></td>
|
42
|
+
<td class="px-6 py-4 text-right"><%= queue.scheduled %></td>
|
43
|
+
<td class="px-6 py-4 text-right"><%= queue.running %></td>
|
44
|
+
<td class="px-6 py-4 text-right"><%= queue.failed %></td>
|
45
|
+
<td class="px-6 py-4 text-right"><%= queue.latency %> ms</td>
|
46
|
+
</tr>
|
47
|
+
<% end %>
|
48
|
+
</tbody>
|
49
|
+
</table>
|
50
|
+
</div>
|
51
|
+
</div>
|