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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 714e432f39bf82408372d0cab4d74ae4f0d8b4f822bcb4efedda6aa54c375c81
4
- data.tar.gz: ad682c7866d2e88a7810c580a70e0985ff3de45c4d3c6202fc3a91d9203d9049
3
+ metadata.gz: d7f06b4e0a9296b7bde46ce1e295a3ce73a8a719e70db335527ced702a0a01e0
4
+ data.tar.gz: 534a79e9d2b01d30dadebe9e0a7f23a7b512a3eee2f06431b84761057b593e0d
5
5
  SHA512:
6
- metadata.gz: 8d3d404322ae5e866d3854a9ac680ac9bc6c8f755d8db26ebf0d97d0c0dc3839d377e54e60cce9efa3666e0d8091075cf1dc5d4ac16db11fd43a9d6f0a4fd9f2
7
- data.tar.gz: 1c67b80700c0acd3a4659a411663f7c80f1d96e2a6134bf2716cac82520ee93d3081126cdd247da8b1730194ba8d243dbe95843eed7f37a0b3f0168bc16aa2fa
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. Built with Tailwind CSS, Turbo frames, and Stimulus controllers.
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
  ![Dashboard Screenshot](screenshots/dashboard.png)
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
- @jobs = JobSummary.for_queue(@queue_name, 50)
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 message if job failed
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
- error = failed_execution&.error
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
- "None"
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
- begin
66
- arguments_data = JSON.parse(job.arguments)
67
- format_arguments(arguments_data)
68
- rescue JSON::ParserError
69
- # If we can't parse as JSON, show as is
70
- job.arguments.to_s
71
- end
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: error
190
+ error: raw_error,
191
+ exception_class: nil,
192
+ backtrace: nil
83
193
  )
84
194
  rescue => e
85
- # Handle any parsing errors
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: "Error parsing arguments",
206
+ arguments: arg_display,
91
207
  created_at: job.created_at,
92
208
  status: status,
93
- error: error || e.message
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) && args.length > 0
117
- # Truncate long arguments to prevent display issues
118
- args.map do |arg|
119
- if arg.is_a?(String) && arg.length > 100
120
- arg[0..100] + "..."
121
- else
122
- arg.to_s
123
- end
124
- end.join(", ")
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
- args.to_s
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-4 gap-4">
13
- <div class="bg-white shadow-sm rounded p-4">
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
- </div>
17
- <div class="bg-white shadow-sm rounded p-4">
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
- </div>
21
- <div class="bg-white shadow-sm rounded p-4">
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
- </div>
25
- <div class="bg-white shadow-sm rounded p-4">
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
- </div>
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
- <h3 class="text-xl font-semibold mb-4">Recent Jobs</h3>
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"><%= job.id %></td>
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
- <% status_color = case job.status
70
- when 'completed' then 'bg-green-100 text-green-800'
71
- when 'running' then 'bg-blue-100 text-blue-800'
72
- when 'scheduled' then 'bg-yellow-100 text-yellow-800'
73
- when 'pending' then 'bg-gray-100 text-gray-800'
74
- when 'failed' then 'bg-red-100 text-red-800'
75
- else 'bg-gray-100 text-gray-800'
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 inline-flex text-xs leading-5 font-semibold rounded-full <%= status_color %>">
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="5" class="px-4 py-3 text-center text-sm text-gray-500">No jobs found in this queue</td>
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
+ &laquo;
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
+ &laquo;
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
+ &raquo;
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
+ &raquo;
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ModernQueueDashboard
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.0"
5
5
  end
data/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "modern_queue_dashboard",
3
3
  "private": true,
4
- "version": "0.3.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.1
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