modern_queue_dashboard 0.3.1 → 0.5.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 +4 -4
- data/CHANGELOG.md +34 -4
- data/README.md +13 -13
- data/app/assets/builds/modern_queue_dashboard.css +1 -1
- data/app/controllers/modern_queue_dashboard/dashboard_controller.rb +1 -0
- data/app/controllers/modern_queue_dashboard/jobs_controller.rb +114 -0
- data/app/controllers/modern_queue_dashboard/queues_controller.rb +119 -3
- data/app/models/modern_queue_dashboard/job_summary.rb +272 -0
- data/app/models/modern_queue_dashboard/metrics.rb +43 -9
- data/app/models/modern_queue_dashboard/queue_summary.rb +102 -33
- data/app/views/modern_queue_dashboard/dashboard/index.html.erb +53 -2
- data/app/views/modern_queue_dashboard/jobs/show.html.erb +102 -0
- data/app/views/modern_queue_dashboard/queues/index.html.erb +1 -1
- data/app/views/modern_queue_dashboard/queues/show.html.erb +175 -14
- data/config/routes.rb +17 -1
- data/debug_load_order.rb +17 -0
- data/lib/modern_queue_dashboard/engine.rb +14 -0
- data/lib/modern_queue_dashboard/metrics.rb +8 -31
- data/lib/modern_queue_dashboard/version.rb +1 -1
- data/lib/modern_queue_dashboard.rb +7 -4
- data/package.json +1 -1
- data/screenshots/dashboard.png +0 -0
- metadata +20 -2
- data/lib/modern_queue_dashboard/queue_summary.rb +0 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d7f06b4e0a9296b7bde46ce1e295a3ce73a8a719e70db335527ced702a0a01e0
|
4
|
+
data.tar.gz: 534a79e9d2b01d30dadebe9e0a7f23a7b512a3eee2f06431b84761057b593e0d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9c8bdf048c53525cbfe2029e8f72b84ae41e0d90685fecf26a819b10897479625533bd72612f1f8f2ec4e7fd6c5b64d816ccb7d11329edd0e58c05e0c0ac6157
|
7
|
+
data.tar.gz: e9f56187f870dcb7fe98442405cb85e1c83e27f9daefcda2147c628a35c498e3675af3e784009679e1f05af3e8da4e7a943007c148010e21e492c4117df0cce9
|
data/CHANGELOG.md
CHANGED
@@ -1,10 +1,40 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
-
## [0.
|
3
|
+
## [0.5.0] - 2025-05-20
|
4
4
|
|
5
|
-
-
|
6
|
-
-
|
7
|
-
-
|
5
|
+
- Added job retry and discard functionality
|
6
|
+
- Implemented status filtering with pagination
|
7
|
+
- Updated job summary display with enhanced information
|
8
|
+
- Added pagination support using Pagy
|
9
|
+
- Updated queue and job models to accommodate new features
|
10
|
+
- Improved error handling throughout the application
|
11
|
+
|
12
|
+
## [0.4.1] - 2025-05-19
|
13
|
+
|
14
|
+
- Fixed issue with jobs that have no arguments showing "Error parsing arguments"
|
15
|
+
- Improved argument handling and display in job tables
|
16
|
+
|
17
|
+
## [0.4.0] - 2025-05-19
|
18
|
+
|
19
|
+
- Changed Tailwind's Sky color to Blue color for better contrast
|
20
|
+
- Added jobs table to queue view showing up to 50 most recent jobs with their status
|
21
|
+
- Added recent jobs table to main dashboard showing 10 most recent jobs across all queues
|
22
|
+
- Added color-coded status indicators for jobs
|
23
|
+
- Display detailed error information for failed jobs
|
24
|
+
|
25
|
+
## [0.3.2] - 2025-05-19
|
26
|
+
|
27
|
+
- Fixed issue with load order
|
28
|
+
|
29
|
+
## [0.3.1] - 2025-05-19
|
30
|
+
|
31
|
+
- Fixed compatibility with Solid Queue's database schema
|
32
|
+
- Updated queries to properly read from Solid Queue's execution tables:
|
33
|
+
- Ready jobs from `solid_queue_ready_executions`
|
34
|
+
- Scheduled jobs from `solid_queue_scheduled_executions`
|
35
|
+
- Running jobs from `solid_queue_claimed_executions`
|
36
|
+
- Failed jobs from `solid_queue_failed_executions`
|
37
|
+
- Added error handling for cases where tables might not be available
|
8
38
|
- Updated README to clarify that this dashboard is exclusively for Solid Queue
|
9
39
|
|
10
40
|
## [0.3.0] - 2025-05-19
|
data/README.md
CHANGED
@@ -1,9 +1,17 @@
|
|
1
1
|
# Modern Queue Dashboard
|
2
2
|
|
3
|
-
A mountable Rails engine that provides a clean
|
3
|
+
A mountable Rails engine that provides a clean dashboard **specifically designed for monitoring [Solid Queue](https://github.com/basecamp/solid_queue)** jobs.
|
4
4
|
|
5
5
|

|
6
6
|
|
7
|
+
## Why not use Mission Control?
|
8
|
+
|
9
|
+
[Mission Control](https://github.com/rails/mission_control-jobs) is a great tool for monitoring Active Job in Rails. I mostly forgot that it existed and built this for fun. You should probably just use that. 😄
|
10
|
+
|
11
|
+
## Why the "Modern Queue Dashboard"?
|
12
|
+
|
13
|
+
I didn't want to give the impression that there is any association with Solid Queue and because this dashboard could be used with other job backends in the future.
|
14
|
+
|
7
15
|
## Features
|
8
16
|
|
9
17
|
* High-level metrics - counts for pending, scheduled, running, completed and failed jobs
|
@@ -79,18 +87,6 @@ class AuthenticatedConstraint
|
|
79
87
|
end
|
80
88
|
```
|
81
89
|
|
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
90
|
## Configuration
|
95
91
|
|
96
92
|
You can configure the dashboard by creating an initializer:
|
@@ -166,3 +162,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
166
162
|
## Code of Conduct
|
167
163
|
|
168
164
|
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).
|
165
|
+
|
166
|
+
## Acknowledgements
|
167
|
+
|
168
|
+
This project was inspired by [active_storage_dashboard](https://github.com/giovapanasiti/active_storage_dashboard)
|
@@ -1 +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))}}
|
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.13 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;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;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}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-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-feature-settings:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height: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{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{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}}.sr-only{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border-width:0;white-space:nowrap}.relative{position:relative}.z-0{z-index:0}.mx-auto{margin-left:auto;margin-right:auto}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.max-h-40{max-height:10rem}.max-h-\[500px\]{max-height:500px}.min-h-screen{min-height:100vh}.min-w-full{min-width:100%}.max-w-xs{max-width:20rem}.cursor-not-allowed{cursor:not-allowed}.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}.justify-between{justify-content:space-between}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.-space-x-px>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(-1px*(1 - var(--tw-space-x-reverse)));margin-right:calc(-1px*var(--tw-space-x-reverse))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.75rem*var(--tw-space-x-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.75rem*var(--tw-space-y-reverse));margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - 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))}.overflow-auto{overflow:auto}.overflow-x-auto{overflow-x:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-l-md{border-bottom-left-radius:.375rem;border-top-left-radius:.375rem}.rounded-r-md{border-bottom-right-radius:.375rem;border-top-right-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-blue-500{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-red-400{--tw-border-opacity:1;border-color:rgb(248 113 113/var(--tw-border-opacity))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(220 252 231/var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity))}.bg-purple-50{--tw-bg-opacity:1;background-color:rgb(250 245 255/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(254 249 195/var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(254 252 232/var(--tw-bg-opacity))}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-4{padding-top:1rem}.pb-4,.py-4{padding-bottom:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.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}.capitalize{text-transform:capitalize}.italic{font-style:italic}.leading-5{line-height:1.25rem}.tracking-wider{letter-spacing:.05em}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity:1;color:rgb(30 64 175/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-green-700{--tw-text-opacity:1;color:rgb(21 128 61/var(--tw-text-opacity))}.text-green-800{--tw-text-opacity:1;color:rgb(22 101 52/var(--tw-text-opacity))}.text-purple-700{--tw-text-opacity:1;color:rgb(126 34 206/var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-red-800{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity))}.text-yellow-800{--tw-text-opacity:1;color:rgb(133 77 14/var(--tw-text-opacity))}.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)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-inset{--tw-ring-inset:inset}.ring-blue-300{--tw-ring-opacity:1;--tw-ring-color:rgb(147 197 253/var(--tw-ring-opacity))}.ring-blue-700\/10{--tw-ring-color:rgba(29,78,216,.1)}.ring-gray-300{--tw-ring-opacity:1;--tw-ring-color:rgb(209 213 219/var(--tw-ring-opacity))}.ring-gray-500\/10{--tw-ring-color:hsla(220,9%,46%,.1)}.ring-green-600\/20{--tw-ring-color:rgba(22,163,74,.2)}.ring-purple-700\/10{--tw-ring-color:rgba(126,34,206,.1)}.ring-red-300{--tw-ring-opacity:1;--tw-ring-color:rgb(252 165 165/var(--tw-ring-opacity))}.ring-red-600\/10{--tw-ring-color:rgba(220,38,38,.1)}.ring-yellow-600\/20{--tw-ring-color:rgba(202,138,4,.2)}.transition-shadow{transition-duration:.15s;transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-200{transition-duration:.2s}.jobs-container{border-radius:.25rem;--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));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)}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.hover\:text-blue-700:hover{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}.hover\:text-blue-800:hover{--tw-text-opacity:1;color:rgb(30 64 175/var(--tw-text-opacity))}.hover\:text-red-700:hover{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px 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:640px){.sm\:flex{display:flex}.sm\:flex-1{flex:1 1 0%}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}}@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-5{grid-template-columns:repeat(5,minmax(0,1fr))}}@media (min-width:1024px){.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ModernQueueDashboard
|
4
|
+
class JobsController < ApplicationController
|
5
|
+
def show
|
6
|
+
@job_id = params[:id]
|
7
|
+
job = SolidQueue::Job.find_by(id: @job_id)
|
8
|
+
|
9
|
+
if job
|
10
|
+
# Check if this is a failed job and get error details
|
11
|
+
if SolidQueue::FailedExecution.exists?(job_id: job.id)
|
12
|
+
@failed_execution = SolidQueue::FailedExecution.find_by(job_id: job.id)
|
13
|
+
|
14
|
+
# Make the raw error string available to the view
|
15
|
+
@raw_error = @failed_execution.error if @failed_execution&.error.present?
|
16
|
+
end
|
17
|
+
|
18
|
+
@job_stat = JobSummary.job_details(job)
|
19
|
+
@queue_name = job.queue_name
|
20
|
+
else
|
21
|
+
flash[:error] = "Job not found"
|
22
|
+
redirect_to root_path
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def retry
|
27
|
+
@job_id = params[:id]
|
28
|
+
job = SolidQueue::Job.find_by(id: @job_id)
|
29
|
+
|
30
|
+
if job
|
31
|
+
# Use the same job retry logic from queues controller
|
32
|
+
if perform_job_retry(job)
|
33
|
+
flash[:notice] = "Job #{@job_id} has been requeued for retry"
|
34
|
+
else
|
35
|
+
flash[:error] = "Unable to retry job #{@job_id}"
|
36
|
+
end
|
37
|
+
else
|
38
|
+
flash[:error] = "Job #{@job_id} not found"
|
39
|
+
end
|
40
|
+
|
41
|
+
redirect_to job_path(@job_id)
|
42
|
+
end
|
43
|
+
|
44
|
+
def discard
|
45
|
+
@job_id = params[:id]
|
46
|
+
job = SolidQueue::Job.find_by(id: @job_id)
|
47
|
+
|
48
|
+
if job
|
49
|
+
# Use the same job discard logic from queues controller
|
50
|
+
if perform_job_discard(job)
|
51
|
+
flash[:notice] = "Job #{@job_id} has been discarded"
|
52
|
+
else
|
53
|
+
flash[:error] = "Unable to discard job #{@job_id}"
|
54
|
+
end
|
55
|
+
else
|
56
|
+
flash[:error] = "Job #{@job_id} not found"
|
57
|
+
end
|
58
|
+
|
59
|
+
redirect_to job_path(@job_id)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def perform_job_retry(job)
|
65
|
+
ActiveRecord::Base.transaction do
|
66
|
+
# Remove the job from failed executions if it failed
|
67
|
+
SolidQueue::FailedExecution.where(job_id: job.id).delete_all
|
68
|
+
|
69
|
+
# Check if the job is in any execution table and handle accordingly
|
70
|
+
case
|
71
|
+
when SolidQueue::ClaimedExecution.exists?(job_id: job.id)
|
72
|
+
# Running job - can't retry while running
|
73
|
+
return false
|
74
|
+
when SolidQueue::ScheduledExecution.exists?(job_id: job.id)
|
75
|
+
# If scheduled, we'll just reschedule it to run immediately
|
76
|
+
SolidQueue::ScheduledExecution.where(job_id: job.id).delete_all
|
77
|
+
end
|
78
|
+
|
79
|
+
# Re-create a ReadyExecution for the job to make it pending again
|
80
|
+
SolidQueue::ReadyExecution.create!(
|
81
|
+
job_id: job.id,
|
82
|
+
queue_name: job.queue_name,
|
83
|
+
priority: job.priority || 0
|
84
|
+
)
|
85
|
+
|
86
|
+
# Reset the job's finished_at to allow it to run again
|
87
|
+
job.update!(finished_at: nil)
|
88
|
+
|
89
|
+
return true
|
90
|
+
rescue => e
|
91
|
+
# Failed to retry job
|
92
|
+
return false
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def perform_job_discard(job)
|
97
|
+
ActiveRecord::Base.transaction do
|
98
|
+
# Remove from all execution tables
|
99
|
+
SolidQueue::FailedExecution.where(job_id: job.id).delete_all
|
100
|
+
SolidQueue::ReadyExecution.where(job_id: job.id).delete_all
|
101
|
+
SolidQueue::ScheduledExecution.where(job_id: job.id).delete_all
|
102
|
+
SolidQueue::ClaimedExecution.where(job_id: job.id).delete_all
|
103
|
+
|
104
|
+
# Mark job as completed (discarded)
|
105
|
+
job.update!(finished_at: Time.current)
|
106
|
+
|
107
|
+
return true
|
108
|
+
rescue => e
|
109
|
+
# Failed to discard job
|
110
|
+
return false
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -9,11 +9,127 @@ module ModernQueueDashboard
|
|
9
9
|
def show
|
10
10
|
@queue_name = params[:id]
|
11
11
|
@queue = QueueSummary.with_stats.detect { |q| q.name == @queue_name }
|
12
|
+
@status = params[:status]
|
12
13
|
|
13
|
-
|
14
|
+
if @queue
|
15
|
+
# Set up pagination with Pagy
|
16
|
+
page = params[:page] || 1
|
17
|
+
per_page = 50
|
14
18
|
|
15
|
-
|
16
|
-
|
19
|
+
if @status.present?
|
20
|
+
# Get the accurate count for this status
|
21
|
+
@total_count = JobSummary.count_jobs_by_status(@queue_name, @status)
|
22
|
+
|
23
|
+
# Use our specialized method to get jobs with this status
|
24
|
+
@jobs_collection, _ = JobSummary.for_queue_with_status(@queue_name, @status, page.to_i, per_page)
|
25
|
+
@pagy = Pagy.new(count: @total_count, page: page.to_i, items: per_page)
|
26
|
+
@jobs = @jobs_collection
|
27
|
+
else
|
28
|
+
# When no status filter, use regular pagination
|
29
|
+
@total_count = SolidQueue::Job.where(queue_name: @queue_name).count
|
30
|
+
@pagy = Pagy.new(count: @total_count, page: page.to_i, items: per_page)
|
31
|
+
@jobs = JobSummary.for_queue(@queue_name, per_page)
|
32
|
+
end
|
33
|
+
else
|
34
|
+
flash[:error] = "Queue not found"
|
35
|
+
redirect_to queues_path
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def retry_job
|
40
|
+
@queue_name = params[:id]
|
41
|
+
job_id = params[:job_id]
|
42
|
+
|
43
|
+
# Find the job in the database
|
44
|
+
job = SolidQueue::Job.find_by(id: job_id)
|
45
|
+
|
46
|
+
if job
|
47
|
+
# Handle retry logic based on job status
|
48
|
+
if perform_job_retry(job)
|
49
|
+
flash[:notice] = "Job #{job_id} has been requeued for retry"
|
50
|
+
else
|
51
|
+
flash[:error] = "Unable to retry job #{job_id}"
|
52
|
+
end
|
53
|
+
else
|
54
|
+
flash[:error] = "Job #{job_id} not found"
|
55
|
+
end
|
56
|
+
|
57
|
+
redirect_to queue_path(@queue_name)
|
58
|
+
end
|
59
|
+
|
60
|
+
def discard_job
|
61
|
+
@queue_name = params[:id]
|
62
|
+
job_id = params[:job_id]
|
63
|
+
|
64
|
+
# Find the job in the database
|
65
|
+
job = SolidQueue::Job.find_by(id: job_id)
|
66
|
+
|
67
|
+
if job
|
68
|
+
# Handle discard logic
|
69
|
+
if perform_job_discard(job)
|
70
|
+
flash[:notice] = "Job #{job_id} has been discarded"
|
71
|
+
else
|
72
|
+
flash[:error] = "Unable to discard job #{job_id}"
|
73
|
+
end
|
74
|
+
else
|
75
|
+
flash[:error] = "Job #{job_id} not found"
|
76
|
+
end
|
77
|
+
|
78
|
+
redirect_to queue_path(@queue_name)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def perform_job_retry(job)
|
84
|
+
ActiveRecord::Base.transaction do
|
85
|
+
# Remove the job from failed executions if it failed
|
86
|
+
SolidQueue::FailedExecution.where(job_id: job.id).delete_all
|
87
|
+
|
88
|
+
# Check if the job is in any execution table and handle accordingly
|
89
|
+
case
|
90
|
+
when SolidQueue::ClaimedExecution.exists?(job_id: job.id)
|
91
|
+
# Running job - can't retry while running
|
92
|
+
return false
|
93
|
+
when SolidQueue::ScheduledExecution.exists?(job_id: job.id)
|
94
|
+
# If scheduled, we'll just reschedule it to run immediately
|
95
|
+
SolidQueue::ScheduledExecution.where(job_id: job.id).delete_all
|
96
|
+
end
|
97
|
+
|
98
|
+
# Re-create a ReadyExecution for the job to make it pending again
|
99
|
+
SolidQueue::ReadyExecution.create!(
|
100
|
+
job_id: job.id,
|
101
|
+
queue_name: job.queue_name,
|
102
|
+
priority: job.priority || 0
|
103
|
+
)
|
104
|
+
|
105
|
+
# Reset the job's finished_at to allow it to run again
|
106
|
+
job.update!(finished_at: nil)
|
107
|
+
|
108
|
+
return true
|
109
|
+
rescue => e
|
110
|
+
# Log the error for debugging
|
111
|
+
Rails.logger.error("Error retrying job #{job.id}: #{e.message}")
|
112
|
+
return false
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def perform_job_discard(job)
|
117
|
+
ActiveRecord::Base.transaction do
|
118
|
+
# Remove from all execution tables
|
119
|
+
SolidQueue::FailedExecution.where(job_id: job.id).delete_all
|
120
|
+
SolidQueue::ReadyExecution.where(job_id: job.id).delete_all
|
121
|
+
SolidQueue::ScheduledExecution.where(job_id: job.id).delete_all
|
122
|
+
SolidQueue::ClaimedExecution.where(job_id: job.id).delete_all
|
123
|
+
|
124
|
+
# Mark job as completed (discarded)
|
125
|
+
job.update!(finished_at: Time.current)
|
126
|
+
|
127
|
+
return true
|
128
|
+
rescue => e
|
129
|
+
# Log the error for debugging
|
130
|
+
Rails.logger.error("Error discarding job #{job.id}: #{e.message}")
|
131
|
+
return false
|
132
|
+
end
|
17
133
|
end
|
18
134
|
end
|
19
135
|
end
|
@@ -0,0 +1,272 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ModernQueueDashboard
|
4
|
+
JobStat = Struct.new(:id, :class_name, :queue_name, :arguments, :created_at, :status, :error, :exception_class, :backtrace, keyword_init: true)
|
5
|
+
|
6
|
+
# Collection class for JobStat objects
|
7
|
+
class JobStatCollection
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
def initialize(stats)
|
11
|
+
@stats = stats
|
12
|
+
end
|
13
|
+
|
14
|
+
def each(&block)
|
15
|
+
@stats.each(&block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def limit(num)
|
19
|
+
JobStatCollection.new(@stats.take(num))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class JobSummary
|
24
|
+
class << self
|
25
|
+
def for_queue(queue_name, limit = 50)
|
26
|
+
return JobStatCollection.new([]) if test_environment?
|
27
|
+
|
28
|
+
# Get jobs from SolidQueue::Job, ordered by most recently created
|
29
|
+
jobs = SolidQueue::Job.where(queue_name: queue_name)
|
30
|
+
.order(created_at: :desc)
|
31
|
+
.limit(limit)
|
32
|
+
.map { |job| job_to_stat(job) }
|
33
|
+
|
34
|
+
JobStatCollection.new(jobs)
|
35
|
+
end
|
36
|
+
|
37
|
+
def for_queue_with_status(queue_name, status, page = 1, items = 50)
|
38
|
+
return JobStatCollection.new([]) if test_environment?
|
39
|
+
|
40
|
+
# Base query for jobs in this queue
|
41
|
+
base_query = SolidQueue::Job.where(queue_name: queue_name)
|
42
|
+
|
43
|
+
# First, determine how to filter by status
|
44
|
+
if status.present?
|
45
|
+
# Pre-filter records based on status to get accurate counts
|
46
|
+
case status.to_s
|
47
|
+
when "completed"
|
48
|
+
jobs = base_query.where.not(finished_at: nil)
|
49
|
+
filtered_count = jobs.count
|
50
|
+
when "failed"
|
51
|
+
# Get job IDs that have failed executions
|
52
|
+
failed_job_ids = SolidQueue::FailedExecution.pluck(:job_id)
|
53
|
+
jobs = base_query.where(id: failed_job_ids)
|
54
|
+
filtered_count = jobs.count
|
55
|
+
when "running"
|
56
|
+
# Get job IDs that have claimed executions
|
57
|
+
running_job_ids = SolidQueue::ClaimedExecution.pluck(:job_id)
|
58
|
+
jobs = base_query.where(id: running_job_ids)
|
59
|
+
filtered_count = jobs.count
|
60
|
+
when "scheduled"
|
61
|
+
# Get job IDs that have scheduled executions
|
62
|
+
scheduled_job_ids = SolidQueue::ScheduledExecution.pluck(:job_id)
|
63
|
+
jobs = base_query.where(id: scheduled_job_ids)
|
64
|
+
filtered_count = jobs.count
|
65
|
+
when "pending"
|
66
|
+
# Get job IDs that have ready executions
|
67
|
+
pending_job_ids = SolidQueue::ReadyExecution.pluck(:job_id)
|
68
|
+
jobs = base_query.where(id: pending_job_ids)
|
69
|
+
filtered_count = jobs.count
|
70
|
+
else
|
71
|
+
# Unknown status - return all jobs but will be filtered later
|
72
|
+
jobs = base_query
|
73
|
+
filtered_count = jobs.count
|
74
|
+
end
|
75
|
+
else
|
76
|
+
# No status filter - use all jobs
|
77
|
+
jobs = base_query
|
78
|
+
filtered_count = jobs.count
|
79
|
+
end
|
80
|
+
|
81
|
+
# Apply pagination
|
82
|
+
offset = (page - 1) * items
|
83
|
+
paginated_jobs = jobs.order(created_at: :desc).offset(offset).limit(items)
|
84
|
+
|
85
|
+
# Convert to JobStat objects
|
86
|
+
job_stats = paginated_jobs.map { |job| job_to_stat(job) }
|
87
|
+
|
88
|
+
# Double-check status if we couldn't pre-filter accurately
|
89
|
+
if status.present? && status.to_s != "completed" && status.to_s != "unknown"
|
90
|
+
# For statuses that might need more accurate filtering (though we pre-filtered above)
|
91
|
+
job_stats = job_stats.select { |job| job.status == status.to_s }
|
92
|
+
end
|
93
|
+
|
94
|
+
# Return collection and filtered count for pagination
|
95
|
+
[ JobStatCollection.new(job_stats), filtered_count ]
|
96
|
+
end
|
97
|
+
|
98
|
+
def all_jobs(limit = 50)
|
99
|
+
return JobStatCollection.new([]) if test_environment?
|
100
|
+
|
101
|
+
# Get all jobs, ordered by most recently created
|
102
|
+
jobs = SolidQueue::Job.order(created_at: :desc)
|
103
|
+
.limit(limit)
|
104
|
+
.map { |job| job_to_stat(job) }
|
105
|
+
|
106
|
+
JobStatCollection.new(jobs)
|
107
|
+
end
|
108
|
+
|
109
|
+
def count_jobs_by_status(queue_name, status)
|
110
|
+
return 0 if test_environment?
|
111
|
+
|
112
|
+
base_query = SolidQueue::Job.where(queue_name: queue_name)
|
113
|
+
|
114
|
+
case status.to_s
|
115
|
+
when "completed"
|
116
|
+
base_query.where.not(finished_at: nil).count
|
117
|
+
when "failed"
|
118
|
+
failed_job_ids = SolidQueue::FailedExecution.pluck(:job_id)
|
119
|
+
base_query.where(id: failed_job_ids).count
|
120
|
+
when "running"
|
121
|
+
running_job_ids = SolidQueue::ClaimedExecution.pluck(:job_id)
|
122
|
+
base_query.where(id: running_job_ids).count
|
123
|
+
when "scheduled"
|
124
|
+
scheduled_job_ids = SolidQueue::ScheduledExecution.pluck(:job_id)
|
125
|
+
base_query.where(id: scheduled_job_ids).count
|
126
|
+
when "pending"
|
127
|
+
pending_job_ids = SolidQueue::ReadyExecution.pluck(:job_id)
|
128
|
+
base_query.where(id: pending_job_ids).count
|
129
|
+
else
|
130
|
+
base_query.count
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def job_details(job)
|
135
|
+
# This method returns a single JobStat object with all details for a specific job
|
136
|
+
return nil unless job
|
137
|
+
|
138
|
+
# Get detailed information about the job
|
139
|
+
job_stat = job_to_stat(job)
|
140
|
+
|
141
|
+
# Add additional details you might want to include in the job details page
|
142
|
+
# For example, you could add execution history, timing information, etc.
|
143
|
+
|
144
|
+
job_stat
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def job_to_stat(job)
|
150
|
+
# Determine job status
|
151
|
+
status = determine_status(job)
|
152
|
+
|
153
|
+
# Get error data if job failed
|
154
|
+
error = nil
|
155
|
+
exception_class = nil
|
156
|
+
backtrace = nil
|
157
|
+
raw_error = nil
|
158
|
+
|
159
|
+
if status == "failed"
|
160
|
+
failed_execution = SolidQueue::FailedExecution.find_by(job_id: job.id)
|
161
|
+
if failed_execution&.error.present?
|
162
|
+
# Store the raw error for display
|
163
|
+
raw_error = failed_execution.error
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Parse arguments - handle cases with no arguments properly
|
168
|
+
arguments_display = if job.arguments.nil? || !job.arguments.is_a?(String) || job.arguments.empty? || (job.arguments.is_a?(String) && job.arguments.strip == "")
|
169
|
+
"None"
|
170
|
+
else
|
171
|
+
begin
|
172
|
+
# Try to parse as JSON first
|
173
|
+
arguments_data = JSON.parse(job.arguments)
|
174
|
+
format_arguments(arguments_data)
|
175
|
+
rescue JSON::ParserError
|
176
|
+
# If we can't parse as JSON, show as is but check if it's meaningful
|
177
|
+
raw_value = job.arguments.to_s.strip
|
178
|
+
raw_value.present? ? raw_value : "None"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Create job stat with the raw error data
|
183
|
+
JobStat.new(
|
184
|
+
id: job.id,
|
185
|
+
class_name: job.class_name,
|
186
|
+
queue_name: job.queue_name,
|
187
|
+
arguments: arguments_display,
|
188
|
+
created_at: job.created_at,
|
189
|
+
status: status,
|
190
|
+
error: raw_error,
|
191
|
+
exception_class: nil,
|
192
|
+
backtrace: nil
|
193
|
+
)
|
194
|
+
rescue => e
|
195
|
+
# Only show parsing error if there were actually arguments to parse
|
196
|
+
arg_display = if job.arguments.present? && job.arguments.is_a?(String) && job.arguments.strip != ""
|
197
|
+
"Error parsing arguments"
|
198
|
+
else
|
199
|
+
"None"
|
200
|
+
end
|
201
|
+
|
202
|
+
JobStat.new(
|
203
|
+
id: job.id,
|
204
|
+
class_name: job.class_name,
|
205
|
+
queue_name: job.queue_name,
|
206
|
+
arguments: arg_display,
|
207
|
+
created_at: job.created_at,
|
208
|
+
status: status,
|
209
|
+
error: e.message,
|
210
|
+
exception_class: nil,
|
211
|
+
backtrace: nil
|
212
|
+
)
|
213
|
+
end
|
214
|
+
|
215
|
+
def determine_status(job)
|
216
|
+
if job.finished_at.present?
|
217
|
+
"completed"
|
218
|
+
elsif SolidQueue::FailedExecution.exists?(job_id: job.id)
|
219
|
+
"failed"
|
220
|
+
elsif SolidQueue::ClaimedExecution.exists?(job_id: job.id)
|
221
|
+
"running"
|
222
|
+
elsif SolidQueue::ScheduledExecution.exists?(job_id: job.id)
|
223
|
+
"scheduled"
|
224
|
+
elsif SolidQueue::ReadyExecution.exists?(job_id: job.id)
|
225
|
+
"pending"
|
226
|
+
else
|
227
|
+
"unknown"
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def format_arguments(args)
|
232
|
+
return "None" if args.blank?
|
233
|
+
|
234
|
+
if args.is_a?(Array)
|
235
|
+
if args.empty?
|
236
|
+
"None"
|
237
|
+
else
|
238
|
+
# Truncate long arguments to prevent display issues
|
239
|
+
args.map do |arg|
|
240
|
+
if arg.nil?
|
241
|
+
"nil"
|
242
|
+
elsif arg.is_a?(String) && arg.length > 100
|
243
|
+
arg[0..100] + "..."
|
244
|
+
else
|
245
|
+
arg.to_s
|
246
|
+
end
|
247
|
+
end.join(", ")
|
248
|
+
end
|
249
|
+
elsif args.is_a?(Hash)
|
250
|
+
if args.empty?
|
251
|
+
"None"
|
252
|
+
else
|
253
|
+
# Format hash in a more readable way
|
254
|
+
args.map do |k, v|
|
255
|
+
v_str = v.to_s
|
256
|
+
value = v_str.length > 50 ? "#{v_str[0..50]}..." : v_str
|
257
|
+
"#{k}: #{value}"
|
258
|
+
end.join(", ")
|
259
|
+
end
|
260
|
+
else
|
261
|
+
# For other types, just convert to string
|
262
|
+
str = args.to_s
|
263
|
+
str.present? ? str : "None"
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def test_environment?
|
268
|
+
ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test"
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|