modern_queue_dashboard 0.4.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 +9 -0
- data/README.md +9 -1
- data/app/assets/builds/modern_queue_dashboard.css +1 -1
- data/app/controllers/modern_queue_dashboard/jobs_controller.rb +114 -0
- data/app/controllers/modern_queue_dashboard/queues_controller.rb +115 -1
- data/app/models/modern_queue_dashboard/job_summary.rb +164 -27
- data/app/models/modern_queue_dashboard/queue_summary.rb +12 -2
- data/app/views/modern_queue_dashboard/jobs/show.html.erb +102 -0
- data/app/views/modern_queue_dashboard/queues/show.html.erb +132 -28
- data/config/routes.rb +14 -1
- data/lib/modern_queue_dashboard/engine.rb +11 -0
- data/lib/modern_queue_dashboard/version.rb +1 -1
- data/package.json +1 -1
- metadata +17 -1
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,5 +1,14 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.5.0] - 2025-05-20
|
4
|
+
|
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
|
+
|
3
12
|
## [0.4.1] - 2025-05-19
|
4
13
|
|
5
14
|
- Fixed issue with jobs that have no arguments showing "Error parsing arguments"
|
data/README.md
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
# Modern Queue Dashboard
|
2
2
|
|
3
|
-
A mountable Rails engine that provides a clean dashboard **specifically designed for monitoring [Solid Queue](https://github.com/basecamp/solid_queue)** jobs.
|
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
|
+
|
7
11
|
## Why the "Modern Queue Dashboard"?
|
8
12
|
|
9
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.
|
@@ -158,3 +162,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
158
162
|
## Code of Conduct
|
159
163
|
|
160
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,13 +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
|
14
|
-
|
15
|
+
# Set up pagination with Pagy
|
16
|
+
page = params[:page] || 1
|
17
|
+
per_page = 50
|
18
|
+
|
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
|
15
33
|
else
|
16
34
|
flash[:error] = "Queue not found"
|
17
35
|
redirect_to queues_path
|
18
36
|
end
|
19
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
|
133
|
+
end
|
20
134
|
end
|
21
135
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ModernQueueDashboard
|
4
|
-
JobStat = Struct.new(:id, :class_name, :queue_name, :arguments, :created_at, :status, :error, keyword_init: true)
|
4
|
+
JobStat = Struct.new(:id, :class_name, :queue_name, :arguments, :created_at, :status, :error, :exception_class, :backtrace, keyword_init: true)
|
5
5
|
|
6
6
|
# Collection class for JobStat objects
|
7
7
|
class JobStatCollection
|
@@ -34,6 +34,67 @@ module ModernQueueDashboard
|
|
34
34
|
JobStatCollection.new(jobs)
|
35
35
|
end
|
36
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
|
+
|
37
98
|
def all_jobs(limit = 50)
|
38
99
|
return JobStatCollection.new([]) if test_environment?
|
39
100
|
|
@@ -45,33 +106,80 @@ module ModernQueueDashboard
|
|
45
106
|
JobStatCollection.new(jobs)
|
46
107
|
end
|
47
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
|
+
|
48
147
|
private
|
49
148
|
|
50
149
|
def job_to_stat(job)
|
51
150
|
# Determine job status
|
52
151
|
status = determine_status(job)
|
53
152
|
|
54
|
-
# Get error
|
153
|
+
# Get error data if job failed
|
55
154
|
error = nil
|
155
|
+
exception_class = nil
|
156
|
+
backtrace = nil
|
157
|
+
raw_error = nil
|
158
|
+
|
56
159
|
if status == "failed"
|
57
160
|
failed_execution = SolidQueue::FailedExecution.find_by(job_id: job.id)
|
58
|
-
|
161
|
+
if failed_execution&.error.present?
|
162
|
+
# Store the raw error for display
|
163
|
+
raw_error = failed_execution.error
|
164
|
+
end
|
59
165
|
end
|
60
166
|
|
61
167
|
# Parse arguments - handle cases with no arguments properly
|
62
|
-
arguments_display = if job.arguments.nil? || job.arguments.empty?
|
63
|
-
|
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"
|
64
170
|
else
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
72
180
|
end
|
73
181
|
|
74
|
-
# Create job stat
|
182
|
+
# Create job stat with the raw error data
|
75
183
|
JobStat.new(
|
76
184
|
id: job.id,
|
77
185
|
class_name: job.class_name,
|
@@ -79,18 +187,28 @@ module ModernQueueDashboard
|
|
79
187
|
arguments: arguments_display,
|
80
188
|
created_at: job.created_at,
|
81
189
|
status: status,
|
82
|
-
error:
|
190
|
+
error: raw_error,
|
191
|
+
exception_class: nil,
|
192
|
+
backtrace: nil
|
83
193
|
)
|
84
194
|
rescue => e
|
85
|
-
#
|
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
|
+
|
86
202
|
JobStat.new(
|
87
203
|
id: job.id,
|
88
204
|
class_name: job.class_name,
|
89
205
|
queue_name: job.queue_name,
|
90
|
-
arguments:
|
206
|
+
arguments: arg_display,
|
91
207
|
created_at: job.created_at,
|
92
208
|
status: status,
|
93
|
-
error:
|
209
|
+
error: e.message,
|
210
|
+
exception_class: nil,
|
211
|
+
backtrace: nil
|
94
212
|
)
|
95
213
|
end
|
96
214
|
|
@@ -113,17 +231,36 @@ module ModernQueueDashboard
|
|
113
231
|
def format_arguments(args)
|
114
232
|
return "None" if args.blank?
|
115
233
|
|
116
|
-
if args.is_a?(Array)
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
arg.
|
123
|
-
|
124
|
-
|
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
|
125
260
|
else
|
126
|
-
|
261
|
+
# For other types, just convert to string
|
262
|
+
str = args.to_s
|
263
|
+
str.present? ? str : "None"
|
127
264
|
end
|
128
265
|
end
|
129
266
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ModernQueueDashboard
|
4
|
-
QueueStat = Struct.new(:name, :pending, :scheduled, :running, :failed, :latency, keyword_init: true)
|
4
|
+
QueueStat = Struct.new(:name, :pending, :scheduled, :running, :failed, :completed, :latency, keyword_init: true)
|
5
5
|
|
6
6
|
# Collection class for QueueStat objects
|
7
7
|
class QueueStatCollection
|
@@ -86,6 +86,16 @@ module ModernQueueDashboard
|
|
86
86
|
0
|
87
87
|
end
|
88
88
|
|
89
|
+
# Completed: Jobs in completed_executions table
|
90
|
+
completed = begin
|
91
|
+
SolidQueue::Job
|
92
|
+
.where(queue_name: name)
|
93
|
+
.where.not(finished_at: nil)
|
94
|
+
.count
|
95
|
+
rescue
|
96
|
+
0
|
97
|
+
end
|
98
|
+
|
89
99
|
# Latency: Time since oldest job in ready_executions was created
|
90
100
|
oldest_ready_job = begin
|
91
101
|
SolidQueue::ReadyExecution
|
@@ -100,7 +110,7 @@ module ModernQueueDashboard
|
|
100
110
|
|
101
111
|
latency = oldest_ready_job ? (Time.now - oldest_ready_job).to_i : 0
|
102
112
|
|
103
|
-
QueueStat.new(name:, pending:, scheduled:, running:, failed:, latency:)
|
113
|
+
QueueStat.new(name:, pending:, scheduled:, running:, failed:, completed:, latency:)
|
104
114
|
end
|
105
115
|
|
106
116
|
def test_environment?
|
@@ -0,0 +1,102 @@
|
|
1
|
+
<div class="container mx-auto p-6 space-y-6">
|
2
|
+
<h1 class="text-3xl font-semibold">Job Details</h1>
|
3
|
+
|
4
|
+
<div class="flex items-center space-x-2 mb-4">
|
5
|
+
<%= link_to "← Back to Queue", queue_path(@queue_name), class: "text-blue-500" %>
|
6
|
+
</div>
|
7
|
+
|
8
|
+
<% if @job_stat %>
|
9
|
+
<!-- Job Overview Card -->
|
10
|
+
<div class="bg-white shadow rounded-lg p-6">
|
11
|
+
<div class="border-b border-gray-200 pb-4 mb-4">
|
12
|
+
<h2 class="text-2xl font-semibold">Job #<%= @job_stat.id %></h2>
|
13
|
+
<p class="text-gray-600"><%= @job_stat.class_name %></p>
|
14
|
+
</div>
|
15
|
+
|
16
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
17
|
+
<div>
|
18
|
+
<h3 class="text-lg font-medium mb-3">Job Information</h3>
|
19
|
+
|
20
|
+
<div class="space-y-3">
|
21
|
+
<div>
|
22
|
+
<p class="text-sm text-gray-500">Status</p>
|
23
|
+
<% status_style = case @job_stat.status
|
24
|
+
when 'completed' then 'bg-green-50 text-green-700 ring-1 ring-green-600/20 ring-inset'
|
25
|
+
when 'running' then 'bg-blue-50 text-blue-700 ring-1 ring-blue-700/10 ring-inset'
|
26
|
+
when 'scheduled' then 'bg-yellow-50 text-yellow-800 ring-1 ring-yellow-600/20 ring-inset'
|
27
|
+
when 'pending' then 'bg-gray-50 text-gray-600 ring-1 ring-gray-500/10 ring-inset'
|
28
|
+
when 'failed' then 'bg-red-50 text-red-700 ring-1 ring-red-600/10 ring-inset'
|
29
|
+
else 'bg-purple-50 text-purple-700 ring-1 ring-purple-700/10 ring-inset'
|
30
|
+
end %>
|
31
|
+
<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium <%= status_style %>">
|
32
|
+
<%= @job_stat.status %>
|
33
|
+
</span>
|
34
|
+
</div>
|
35
|
+
|
36
|
+
<div>
|
37
|
+
<p class="text-sm text-gray-500">Queue</p>
|
38
|
+
<p class="font-medium"><%= @job_stat.queue_name %></p>
|
39
|
+
</div>
|
40
|
+
|
41
|
+
<div>
|
42
|
+
<p class="text-sm text-gray-500">Created At</p>
|
43
|
+
<p class="font-medium"><%= @job_stat.created_at.strftime('%Y-%m-%d %H:%M:%S') %></p>
|
44
|
+
</div>
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
|
48
|
+
<div>
|
49
|
+
<h3 class="text-lg font-medium mb-3">Arguments</h3>
|
50
|
+
<div class="bg-gray-50 p-4 rounded-md overflow-auto max-h-40">
|
51
|
+
<pre class="text-sm text-gray-700 font-mono"><%= @job_stat.arguments %></pre>
|
52
|
+
</div>
|
53
|
+
|
54
|
+
<div class="mt-6 flex space-x-3">
|
55
|
+
<% if ['failed', 'scheduled', 'pending', 'completed'].include?(@job_stat.status) %>
|
56
|
+
<%= button_to "Retry Job", retry_job_path(@job_stat.id),
|
57
|
+
method: :post,
|
58
|
+
class: "rounded-md bg-white px-4 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50",
|
59
|
+
form: { data: { turbo_confirm: "Are you sure you want to retry this job?" } },
|
60
|
+
disabled: @job_stat.status == 'running' %>
|
61
|
+
<% end %>
|
62
|
+
|
63
|
+
<% if @job_stat.status != 'completed' %>
|
64
|
+
<%= button_to "Discard Job", discard_job_path(@job_stat.id),
|
65
|
+
method: :delete,
|
66
|
+
class: "rounded-md bg-white px-4 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-red-300 hover:bg-gray-50 hover:text-red-700",
|
67
|
+
form: { data: { turbo_confirm: "Are you sure you want to discard this job?" } } %>
|
68
|
+
<% end %>
|
69
|
+
</div>
|
70
|
+
</div>
|
71
|
+
</div>
|
72
|
+
</div>
|
73
|
+
|
74
|
+
<!-- Error Information Section -->
|
75
|
+
<% if @job_stat.status == 'failed' %>
|
76
|
+
<div class="bg-white shadow rounded-lg p-6">
|
77
|
+
<h3 class="text-lg font-medium text-red-700 mb-4">Error Information</h3>
|
78
|
+
|
79
|
+
<div class="space-y-6">
|
80
|
+
<!-- Error Data -->
|
81
|
+
<% if @job_stat.error.present? %>
|
82
|
+
<div>
|
83
|
+
<h4 class="text-sm font-medium text-gray-500 mb-2">Error Details</h4>
|
84
|
+
<div class="bg-red-50 p-4 rounded-md overflow-auto max-h-[500px]">
|
85
|
+
<pre class="font-mono text-xs text-red-700 whitespace-pre-wrap break-all"><%= @job_stat.error %></pre>
|
86
|
+
</div>
|
87
|
+
</div>
|
88
|
+
<% else %>
|
89
|
+
<div class="text-red-600 italic">
|
90
|
+
No error data available
|
91
|
+
</div>
|
92
|
+
<% end %>
|
93
|
+
</div>
|
94
|
+
</div>
|
95
|
+
<% end %>
|
96
|
+
<% else %>
|
97
|
+
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
98
|
+
Job not found.
|
99
|
+
</div>
|
100
|
+
<% end %>
|
101
|
+
</div>
|
102
|
+
</div>
|
@@ -6,26 +6,37 @@
|
|
6
6
|
</div>
|
7
7
|
|
8
8
|
<h2 class="text-2xl font-semibold">Queue: <%= @queue_name %></h2>
|
9
|
+
<% if @status.present? %>
|
10
|
+
<p class="text-gray-600 mb-2">Filtered by status: <span class="font-medium"><%= @status %></span></p>
|
11
|
+
<% end %>
|
9
12
|
|
10
13
|
<% if @queue %>
|
11
14
|
<!-- Queue Stats -->
|
12
|
-
<div class="grid grid-cols-2 md:grid-cols-
|
13
|
-
|
15
|
+
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
|
16
|
+
<%= link_to status_queue_path(id: @queue_name, status: "pending"), class: "bg-white shadow-sm rounded p-4 hover:shadow-md transition-shadow duration-200" do %>
|
14
17
|
<p class="text-sm text-gray-500">Pending Jobs</p>
|
15
18
|
<p class="text-2xl font-bold text-blue-500"><%= @queue.pending %></p>
|
16
|
-
|
17
|
-
|
19
|
+
<% end %>
|
20
|
+
|
21
|
+
<%= link_to status_queue_path(id: @queue_name, status: "scheduled"), class: "bg-white shadow-sm rounded p-4 hover:shadow-md transition-shadow duration-200" do %>
|
18
22
|
<p class="text-sm text-gray-500">Scheduled Jobs</p>
|
19
23
|
<p class="text-2xl font-bold text-blue-500"><%= @queue.scheduled %></p>
|
20
|
-
|
21
|
-
|
24
|
+
<% end %>
|
25
|
+
|
26
|
+
<%= link_to status_queue_path(id: @queue_name, status: "running"), class: "bg-white shadow-sm rounded p-4 hover:shadow-md transition-shadow duration-200" do %>
|
22
27
|
<p class="text-sm text-gray-500">Running Jobs</p>
|
23
28
|
<p class="text-2xl font-bold text-blue-500"><%= @queue.running %></p>
|
24
|
-
|
25
|
-
|
29
|
+
<% end %>
|
30
|
+
|
31
|
+
<%= link_to status_queue_path(id: @queue_name, status: "failed"), class: "bg-white shadow-sm rounded p-4 hover:shadow-md transition-shadow duration-200" do %>
|
26
32
|
<p class="text-sm text-gray-500">Failed Jobs</p>
|
27
33
|
<p class="text-2xl font-bold text-blue-500"><%= @queue.failed %></p>
|
28
|
-
|
34
|
+
<% end %>
|
35
|
+
|
36
|
+
<%= link_to status_queue_path(id: @queue_name, status: "completed"), class: "bg-white shadow-sm rounded p-4 hover:shadow-md transition-shadow duration-200" do %>
|
37
|
+
<p class="text-sm text-gray-500">Completed Jobs</p>
|
38
|
+
<p class="text-2xl font-bold text-blue-500"><%= @queue.completed %></p>
|
39
|
+
<% end %>
|
29
40
|
</div>
|
30
41
|
|
31
42
|
<!-- Queue Details -->
|
@@ -46,7 +57,18 @@
|
|
46
57
|
|
47
58
|
<!-- Jobs Table -->
|
48
59
|
<div class="bg-white shadow rounded p-6">
|
49
|
-
<
|
60
|
+
<div class="flex justify-between items-center mb-4">
|
61
|
+
<h3 class="text-xl font-semibold">
|
62
|
+
<% if @status.present? %>
|
63
|
+
Recent <%= @status.capitalize %> Jobs
|
64
|
+
<% else %>
|
65
|
+
Recent Jobs
|
66
|
+
<% end %>
|
67
|
+
</h3>
|
68
|
+
<% if @status.present? %>
|
69
|
+
<%= link_to "Clear Filter", queue_path(@queue_name), class: "text-blue-500 text-sm hover:underline" %>
|
70
|
+
<% end %>
|
71
|
+
</div>
|
50
72
|
|
51
73
|
<div class="overflow-x-auto">
|
52
74
|
<table class="min-w-full divide-y divide-gray-200">
|
@@ -57,24 +79,27 @@
|
|
57
79
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
58
80
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Arguments</th>
|
59
81
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created At</th>
|
82
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
60
83
|
</tr>
|
61
84
|
</thead>
|
62
85
|
<tbody class="bg-white divide-y divide-gray-200">
|
63
86
|
<% if @jobs.any? %>
|
64
87
|
<% @jobs.each do |job| %>
|
65
|
-
<tr>
|
66
|
-
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500"
|
88
|
+
<tr id="job-<%= job.id %>" class="<%= 'hover:bg-gray-50' %>">
|
89
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
90
|
+
<%= link_to job.id, job_path(job.id), class: "text-blue-600 hover:text-blue-800 hover:underline" %>
|
91
|
+
</td>
|
67
92
|
<td class="px-4 py-3 whitespace-nowrap text-sm font-medium"><%= job.class_name %></td>
|
68
93
|
<td class="px-4 py-3 whitespace-nowrap text-sm">
|
69
|
-
<%
|
70
|
-
when 'completed' then 'bg-green-
|
71
|
-
when 'running' then 'bg-blue-
|
72
|
-
when 'scheduled' then 'bg-yellow-
|
73
|
-
when 'pending' then 'bg-gray-
|
74
|
-
when 'failed' then 'bg-red-
|
75
|
-
else 'bg-
|
94
|
+
<% status_style = case job.status
|
95
|
+
when 'completed' then 'bg-green-50 text-green-700 ring-1 ring-green-600/20 ring-inset'
|
96
|
+
when 'running' then 'bg-blue-50 text-blue-700 ring-1 ring-blue-700/10 ring-inset'
|
97
|
+
when 'scheduled' then 'bg-yellow-50 text-yellow-800 ring-1 ring-yellow-600/20 ring-inset'
|
98
|
+
when 'pending' then 'bg-gray-50 text-gray-600 ring-1 ring-gray-500/10 ring-inset'
|
99
|
+
when 'failed' then 'bg-red-50 text-red-700 ring-1 ring-red-600/10 ring-inset'
|
100
|
+
else 'bg-purple-50 text-purple-700 ring-1 ring-purple-700/10 ring-inset'
|
76
101
|
end %>
|
77
|
-
<span class="px-2
|
102
|
+
<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium <%= status_style %>">
|
78
103
|
<%= job.status %>
|
79
104
|
</span>
|
80
105
|
</td>
|
@@ -82,23 +107,102 @@
|
|
82
107
|
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
83
108
|
<%= job.created_at.strftime('%Y-%m-%d %H:%M:%S') %>
|
84
109
|
</td>
|
110
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm">
|
111
|
+
<div class="flex space-x-2">
|
112
|
+
<% if job.status == 'failed' && (job.error.present? || job.backtrace.present?) %>
|
113
|
+
<%= link_to "View Details", job_path(job.id),
|
114
|
+
class: "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-blue-300 ring-inset hover:bg-gray-50 hover:text-blue-700" %>
|
115
|
+
<% else %>
|
116
|
+
<% if ['failed', 'scheduled', 'pending', 'completed'].include?(job.status) %>
|
117
|
+
<%= button_to "Retry", retry_job_queue_path(id: @queue_name, job_id: job.id),
|
118
|
+
method: :post,
|
119
|
+
class: "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-gray-300 ring-inset hover:bg-gray-50",
|
120
|
+
form: { data: { turbo_confirm: "Are you sure you want to retry this job?" } },
|
121
|
+
disabled: job.status == 'running' %>
|
122
|
+
<% end %>
|
123
|
+
|
124
|
+
<% if job.status != 'completed' %>
|
125
|
+
<%= button_to "Discard", discard_job_queue_path(id: @queue_name, job_id: job.id),
|
126
|
+
method: :delete,
|
127
|
+
class: "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-red-300 ring-inset hover:bg-gray-50 hover:text-red-700",
|
128
|
+
form: { data: { turbo_confirm: "Are you sure you want to discard this job?" } } %>
|
129
|
+
<% end %>
|
130
|
+
<% end %>
|
131
|
+
</div>
|
132
|
+
</td>
|
85
133
|
</tr>
|
86
|
-
<% if job.status == 'failed' && job.error.present? %>
|
87
|
-
<tr class="bg-red-50">
|
88
|
-
<td colspan="5" class="px-4 py-2 text-xs text-red-700 font-mono">
|
89
|
-
<%= job.error %>
|
90
|
-
</td>
|
91
|
-
</tr>
|
92
|
-
<% end %>
|
93
134
|
<% end %>
|
94
135
|
<% else %>
|
95
136
|
<tr>
|
96
|
-
<td colspan="
|
137
|
+
<td colspan="6" class="px-4 py-3 text-center text-sm text-gray-500">
|
138
|
+
<% if @status.present? %>
|
139
|
+
No <%= @status %> jobs found in this queue
|
140
|
+
<% else %>
|
141
|
+
No jobs found in this queue
|
142
|
+
<% end %>
|
143
|
+
</td>
|
97
144
|
</tr>
|
98
145
|
<% end %>
|
99
146
|
</tbody>
|
100
147
|
</table>
|
101
148
|
</div>
|
149
|
+
|
150
|
+
<!-- Pagination -->
|
151
|
+
<% if @pagy&.pages && @pagy.pages > 1 %>
|
152
|
+
<div class="px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
|
153
|
+
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
154
|
+
<div>
|
155
|
+
<p class="text-sm text-gray-700">
|
156
|
+
<% if @total_count > 0 %>
|
157
|
+
Showing <span class="font-medium"><%= @pagy.from %></span> to <span class="font-medium"><%= @pagy.to %></span> of <span class="font-medium"><%= @pagy.count %></span> results
|
158
|
+
<% else %>
|
159
|
+
No matching jobs found
|
160
|
+
<% end %>
|
161
|
+
</p>
|
162
|
+
</div>
|
163
|
+
<div>
|
164
|
+
<% if @total_count > 0 %>
|
165
|
+
<nav class="relative z-0 inline-flex shadow-sm -space-x-px" aria-label="Pagination">
|
166
|
+
<% if @pagy.prev %>
|
167
|
+
<%= link_to @status.present? ? status_queue_path(id: @queue_name, status: @status, page: @pagy.prev) : queue_path(@queue_name, page: @pagy.prev),
|
168
|
+
class: "relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50" do %>
|
169
|
+
<span class="sr-only">Previous</span>
|
170
|
+
«
|
171
|
+
<% end %>
|
172
|
+
<% else %>
|
173
|
+
<span class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-gray-100 text-sm font-medium text-gray-400 cursor-not-allowed">
|
174
|
+
<span class="sr-only">Previous</span>
|
175
|
+
«
|
176
|
+
</span>
|
177
|
+
<% end %>
|
178
|
+
|
179
|
+
<% @pagy.series.each do |item| %>
|
180
|
+
<% if item.is_a?(Integer) %>
|
181
|
+
<%= link_to item, @status.present? ? status_queue_path(id: @queue_name, status: @status, page: item) : queue_path(@queue_name, page: item),
|
182
|
+
class: "relative inline-flex items-center px-4 py-2 border #{item == @pagy.page ? 'border-blue-500 bg-blue-50 text-blue-600' : 'border-gray-300 bg-white text-gray-500 hover:bg-gray-50'} text-sm font-medium" %>
|
183
|
+
<% elsif item == :gap %>
|
184
|
+
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">...</span>
|
185
|
+
<% end %>
|
186
|
+
<% end %>
|
187
|
+
|
188
|
+
<% if @pagy.next %>
|
189
|
+
<%= link_to @status.present? ? status_queue_path(id: @queue_name, status: @status, page: @pagy.next) : queue_path(@queue_name, page: @pagy.next),
|
190
|
+
class: "relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50" do %>
|
191
|
+
<span class="sr-only">Next</span>
|
192
|
+
»
|
193
|
+
<% end %>
|
194
|
+
<% else %>
|
195
|
+
<span class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-gray-100 text-sm font-medium text-gray-400 cursor-not-allowed">
|
196
|
+
<span class="sr-only">Next</span>
|
197
|
+
»
|
198
|
+
</span>
|
199
|
+
<% end %>
|
200
|
+
</nav>
|
201
|
+
<% end %>
|
202
|
+
</div>
|
203
|
+
</div>
|
204
|
+
</div>
|
205
|
+
<% end %>
|
102
206
|
</div>
|
103
207
|
<% else %>
|
104
208
|
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
data/config/routes.rb
CHANGED
@@ -3,7 +3,20 @@
|
|
3
3
|
ModernQueueDashboard::Engine.routes.draw do
|
4
4
|
root to: "dashboard#index"
|
5
5
|
|
6
|
-
resources :queues, only: %i[index show]
|
6
|
+
resources :queues, only: %i[index show] do
|
7
|
+
member do
|
8
|
+
post "retry_job/:job_id", to: "queues#retry_job", as: "retry_job"
|
9
|
+
delete "discard_job/:job_id", to: "queues#discard_job", as: "discard_job"
|
10
|
+
get "status/:status", to: "queues#show", as: "status"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
resources :jobs, only: [ :show ] do
|
15
|
+
member do
|
16
|
+
post "retry", to: "jobs#retry"
|
17
|
+
delete "discard", to: "jobs#discard"
|
18
|
+
end
|
19
|
+
end
|
7
20
|
|
8
21
|
# Debug route for troubleshooting
|
9
22
|
get "debug", to: "dashboard#debug"
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "rails/engine"
|
4
4
|
require "tailwindcss-rails" if defined?(TailwindCss)
|
5
|
+
require "pagy"
|
5
6
|
|
6
7
|
module ModernQueueDashboard
|
7
8
|
class Engine < ::Rails::Engine
|
@@ -23,5 +24,15 @@ module ModernQueueDashboard
|
|
23
24
|
app.config.tailwindcss.content_includes = [ "./views/**/*.html.erb", "./helpers/**/*.rb" ]
|
24
25
|
end
|
25
26
|
end
|
27
|
+
|
28
|
+
initializer "modern_queue_dashboard.include_pagy_module" do
|
29
|
+
ActiveSupport.on_load :action_controller_base do
|
30
|
+
include Pagy::Backend
|
31
|
+
end
|
32
|
+
|
33
|
+
ActiveSupport.on_load :action_view do
|
34
|
+
include Pagy::Frontend
|
35
|
+
end
|
36
|
+
end
|
26
37
|
end
|
27
38
|
end
|
data/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "modern_queue_dashboard",
|
3
3
|
"private": true,
|
4
|
-
"version": "0.
|
4
|
+
"version": "0.5.0",
|
5
5
|
"description": "Dashboard for Rails Solid Queue jobs",
|
6
6
|
"scripts": {
|
7
7
|
"build:css": "tailwindcss -i ./app/assets/stylesheets/modern_queue_dashboard.css -o ./app/assets/builds/modern_queue_dashboard.css --minify",
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: modern_queue_dashboard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Clayton Lengel-Zigich
|
@@ -71,6 +71,20 @@ dependencies:
|
|
71
71
|
- - "~>"
|
72
72
|
- !ruby/object:Gem::Version
|
73
73
|
version: '2.0'
|
74
|
+
- !ruby/object:Gem::Dependency
|
75
|
+
name: pagy
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - "~>"
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '7.0'
|
81
|
+
type: :runtime
|
82
|
+
prerelease: false
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - "~>"
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '7.0'
|
74
88
|
- !ruby/object:Gem::Dependency
|
75
89
|
name: minitest
|
76
90
|
requirement: !ruby/object:Gem::Requirement
|
@@ -149,12 +163,14 @@ files:
|
|
149
163
|
- app/assets/stylesheets/modern_queue_dashboard.css
|
150
164
|
- app/controllers/modern_queue_dashboard/application_controller.rb
|
151
165
|
- app/controllers/modern_queue_dashboard/dashboard_controller.rb
|
166
|
+
- app/controllers/modern_queue_dashboard/jobs_controller.rb
|
152
167
|
- app/controllers/modern_queue_dashboard/queues_controller.rb
|
153
168
|
- app/models/modern_queue_dashboard/job_summary.rb
|
154
169
|
- app/models/modern_queue_dashboard/metrics.rb
|
155
170
|
- app/models/modern_queue_dashboard/queue_summary.rb
|
156
171
|
- app/views/layouts/modern_queue_dashboard/application.html.erb
|
157
172
|
- app/views/modern_queue_dashboard/dashboard/index.html.erb
|
173
|
+
- app/views/modern_queue_dashboard/jobs/show.html.erb
|
158
174
|
- app/views/modern_queue_dashboard/queues/index.html.erb
|
159
175
|
- app/views/modern_queue_dashboard/queues/show.html.erb
|
160
176
|
- config/routes.rb
|