job_harbor 0.7.1 → 0.8.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/MIT-LICENSE +20 -0
- data/README.md +16 -1
- data/app/assets/stylesheets/job_harbor/application.css +1 -1
- data/app/assets/stylesheets/job_harbor/application.tailwind.css +0 -31
- data/app/components/job_harbor/badge_component.rb +12 -28
- data/app/components/job_harbor/info_line_component.rb +20 -0
- data/app/components/job_harbor/job_row_component.rb +4 -8
- data/app/components/job_harbor/job_search_form_component.html.erb +4 -0
- data/app/components/job_harbor/job_search_form_component.rb +9 -0
- data/app/components/job_harbor/jobs_table_component.html.erb +26 -0
- data/app/components/job_harbor/jobs_table_component.rb +10 -0
- data/app/components/job_harbor/nav_link_component.rb +1 -2
- data/app/components/job_harbor/pagination_component.rb +10 -10
- data/app/components/job_harbor/queue_card_component.rb +4 -6
- data/app/components/job_harbor/queue_pause_form_component.html.erb +5 -0
- data/app/components/job_harbor/queue_pause_form_component.rb +6 -0
- data/app/components/job_harbor/stat_card_component.rb +10 -13
- data/app/components/job_harbor/status_tabs_component.rb +43 -0
- data/app/components/job_harbor/worker_card_component.rb +3 -13
- data/app/controllers/job_harbor/jobs_controller.rb +18 -53
- data/app/controllers/job_harbor/queues/pauses_controller.rb +32 -0
- data/app/controllers/job_harbor/queues_controller.rb +1 -6
- data/app/models/job_harbor/chart_data.rb +10 -4
- data/app/models/job_harbor/dashboard_stats.rb +5 -31
- data/app/models/job_harbor/job_presenter.rb +5 -68
- data/app/models/job_harbor/job_query.rb +105 -0
- data/app/models/job_harbor/job_status.rb +90 -0
- data/app/models/job_harbor/queue_stats.rb +9 -38
- data/app/views/job_harbor/dashboard/index.html.erb +14 -39
- data/app/views/job_harbor/jobs/index.html.erb +5 -56
- data/app/views/job_harbor/jobs/search.html.erb +4 -27
- data/app/views/job_harbor/jobs/show.html.erb +21 -55
- data/app/views/job_harbor/queues/index.html.erb +2 -0
- data/app/views/job_harbor/queues/show.html.erb +5 -54
- data/app/views/job_harbor/recurring_tasks/show.html.erb +9 -31
- data/app/views/job_harbor/workers/index.html.erb +2 -2
- data/app/views/layouts/job_harbor/application.html.erb +27 -25
- data/config/routes.rb +9 -7
- data/lib/job_harbor/configuration.rb +6 -2
- data/lib/job_harbor/engine.rb +9 -1
- data/lib/job_harbor/time_formats.rb +12 -0
- data/lib/job_harbor/version.rb +1 -1
- data/lib/job_harbor.rb +5 -0
- metadata +15 -3
- data/app/helpers/job_harbor/application_helper.rb +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f3332e364e2b6fdeaa34782a5e991dd65d22e19769cc242b2bae74ec56e97c77
|
|
4
|
+
data.tar.gz: 21b0649426482640b7dfad3d0bca05858a062faa4115daedb9413e48ba2687cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 679c4bc2d82c6230251cb8bebecba1301f855fd5ec45c38b12edf0c06968c9fb8225cd346facf47c3fdcfe8c0ae6370571419d2ea124ca8f6f3f0f88b8921dfb
|
|
7
|
+
data.tar.gz: 4dabc6987def5ebc3aa0da908820e5a53562a089ec26ca3061e560bf8d2987883de3a6a3e3fcd6ab9d3a863183cdb70f8c1daf1490f8f1af376dacce71da1270
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2026 Evan Sparkman
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
|
@@ -107,6 +107,9 @@ JobHarbor.configure do |config|
|
|
|
107
107
|
# Theme: :dark or :light (default: :dark)
|
|
108
108
|
config.theme = :dark
|
|
109
109
|
|
|
110
|
+
# Deprecated: config.primary_color has no effect and will be removed in 1.0.
|
|
111
|
+
# Assigning it logs a deprecation warning.
|
|
112
|
+
|
|
110
113
|
# Jobs per page (default: 25)
|
|
111
114
|
config.jobs_per_page = 25
|
|
112
115
|
|
|
@@ -275,6 +278,18 @@ alert. If your app has no root route, or the request is not HTML, the response i
|
|
|
275
278
|
- Solid Queue >= 1.0, < 2
|
|
276
279
|
- ViewComponent >= 4.1
|
|
277
280
|
|
|
281
|
+
## Development
|
|
282
|
+
|
|
283
|
+
To work on the gem itself, clone the repo and run `bundle install`. The dummy Rails app the tests run against lives in `test/dummy`.
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
bin/ci # rubocop, bundler-audit, brakeman, unit tests, system tests, seed replant
|
|
287
|
+
bin/rails test # everything except the system tests
|
|
288
|
+
bin/rails test:system # browser tests only
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The system tests drive headless Chrome through selenium-webdriver, so they need Google Chrome installed locally.
|
|
292
|
+
|
|
278
293
|
## License
|
|
279
294
|
|
|
280
|
-
MIT License. See [LICENSE](LICENSE) for details.
|
|
295
|
+
MIT License. See [MIT-LICENSE](MIT-LICENSE) for details.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/*! tailwindcss v4.1.3 | MIT License | https://tailwindcss.com */
|
|
2
|
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-500:oklch(63.7% .237 25.331);--color-sky-500:oklch(68.5% .169 237.323);--spacing:.25rem;--breakpoint-2xl:96rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}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;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:root{--background:0 0% 98%;--foreground:240 10% 3.9%;--card:0 0% 100%;--card-foreground:240 10% 3.9%;--primary:240 5.9% 10%;--primary-foreground:0 0% 98%;--secondary:240 4.8% 95.9%;--secondary-foreground:240 5.9% 10%;--muted:240 4.8% 95.9%;--muted-foreground:240 3.8% 46.1%;--accent:240 4.8% 93.8%;--accent-foreground:240 5.9% 10%;--destructive:0 84.2% 60.2%;--destructive-foreground:0 0% 98%;--border:240 5.9% 90%;--input:240 5.9% 90%;--ring:240 5.9% 10%;--radius:.65rem;--chart-1:12 76% 61%;--chart-2:173 58% 39%;--chart-3:197 37% 24%;--chart-4:43 74% 66%;--chart-5:27 87% 67%}.dark{--background:240 10% 5.4%;--foreground:0 0% 92%;--card:240 10% 3.9%;--card-foreground:0 0% 92%;--primary:0 0% 95%;--primary-foreground:240 5.9% 10%;--secondary:240 3.7% 15.9%;--secondary-foreground:0 0% 92%;--muted:240 3.7% 15.9%;--muted-foreground:240 5% 64.9%;--accent:240 3.7% 15.9%;--accent-foreground:0 0% 92%;--destructive:0 62.8% 30.6%;--destructive-foreground:0 0% 92%;--border:240 3.7% 15.9%;--input:240 3.7% 15.9%;--ring:240 4.9% 83.9%;--chart-1:220 70% 50%;--chart-2:160 60% 45%;--chart-3:30 80% 55%;--chart-4:280 65% 60%;--chart-5:340 75% 55%}*,:before,:after{box-sizing:border-box}body{background-color:hsl(var(--background));color:hsl(var(--foreground));-webkit-font-smoothing:antialiased;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji}}@layer components{.navbar{border-bottom:1px solid hsl(var(--border));background-color:hsl(var(--card));align-items:center;gap:1rem;height:3.5rem;padding:0 1.5rem;display:flex}.navbar-section{align-items:center;gap:.375rem;display:flex}@media not all and (min-width:40rem){.navbar{flex-wrap:wrap;gap:.5rem;height:auto;min-height:3.5rem;padding:.5rem 1rem}.navbar-section{flex-wrap:wrap;flex-basis:100%;order:3}}.navbar-item{color:hsl(var(--muted-foreground));border-radius:calc(var(--radius) - 2px);white-space:nowrap;align-items:center;gap:.375rem;padding:.375rem .75rem;font-size:.875rem;font-weight:500;text-decoration:none;transition:color .15s,background-color .15s;display:inline-flex}.navbar-item:hover{color:hsl(var(--foreground));background-color:hsl(var(--accent))}.navbar-item-current{color:hsl(var(--primary-foreground));background-color:hsl(var(--primary))}.navbar-item-current:hover{color:hsl(var(--primary-foreground));background-color:hsl(var(--primary));opacity:.9}.navbar-badge{border-radius:calc(var(--radius) - 4px);background-color:hsl(var(--muted));color:hsl(var(--muted-foreground));padding:.125rem .375rem;font-size:.75rem;font-weight:600}.navbar-item-current .navbar-badge{background-color:hsl(var(--primary-foreground)/.2);color:hsl(var(--primary-foreground))}.card{border-radius:var(--radius);border:1px solid hsl(var(--border));background-color:hsl(var(--card));color:hsl(var(--card-foreground));box-shadow:0 1px 2px #0000000d}.card-header{flex-direction:column;gap:.375rem;padding:1.5rem;display:flex}.card-title{letter-spacing:-.025em;margin:0;font-size:1rem;font-weight:600;line-height:1}.card-description{color:hsl(var(--muted-foreground));font-size:.875rem}.card-content{padding:1.5rem;padding-top:0}.card-footer{align-items:center;padding:0 1.5rem 1.5rem;display:flex}.badge{border-radius:calc(var(--radius) - 2px);white-space:nowrap;align-items:center;gap:.375rem;padding:.125rem .625rem;font-size:.75rem;font-weight:500;transition:color .15s,background-color .15s;display:inline-flex}.badge-primary{background-color:hsl(var(--primary));color:hsl(var(--primary-foreground))}.badge-secondary{background-color:hsl(var(--secondary));color:hsl(var(--secondary-foreground))}.badge-destructive{background-color:hsl(var(--destructive));color:hsl(var(--destructive-foreground))}.badge-outline{border:1px solid hsl(var(--border));color:hsl(var(--foreground));background-color:#0000}.badge-red{color:#ef4444;background-color:#ef44441a}.dark .badge-red{color:#fca5a5;background-color:#ef444426}.badge-amber{color:#d97706;background-color:#fbbf241a}.dark .badge-amber{color:#fcd34d;background-color:#fbbf2426}.badge-yellow{color:#a16207;background-color:#facc151a}.dark .badge-yellow{color:#fde047;background-color:#facc1526}.badge-green{color:#16a34a;background-color:#22c55e1a}.dark .badge-green{color:#86efac;background-color:#22c55e26}.badge-sky{color:#0284c7;background-color:#0ea5e91a}.dark .badge-sky{color:#7dd3fc;background-color:#0ea5e926}.badge-zinc{color:#52525b;background-color:#52525b1a}.dark .badge-zinc{color:#a1a1aa;background-color:#52525b33}.circle{border-radius:9999px;flex-shrink:0;width:.5rem;height:.5rem;display:inline-block}.circle-red{background-color:#ef4444}.circle-amber{background-color:#fbbf24}.circle-green{background-color:#22c55e}.circle-yellow{background-color:#facc15}.circle-sky{background-color:#0ea5e9}.circle-zinc{background-color:#52525b}.btn{white-space:nowrap;border-radius:calc(var(--radius) - 2px);cursor:pointer;border:none;justify-content:center;align-items:center;gap:.5rem;font-size:.875rem;font-weight:500;line-height:1;text-decoration:none;transition:color .15s,background-color .15s,opacity .15s;display:inline-flex}.btn:disabled{opacity:.5;cursor:not-allowed}.btn-default{background-color:hsl(var(--primary));color:hsl(var(--primary-foreground));height:2.5rem;padding:.5rem 1rem}.btn-default:hover{opacity:.9}.btn-secondary{background-color:hsl(var(--secondary));color:hsl(var(--secondary-foreground));border:1px solid hsl(var(--border));height:2.5rem;padding:.5rem 1rem}.btn-secondary:hover{background-color:hsl(var(--accent))}.btn-destructive{background-color:hsl(var(--destructive));color:hsl(var(--destructive-foreground));height:2.5rem;padding:.5rem 1rem}.btn-destructive:hover{opacity:.9}.btn-outline{border:1px solid hsl(var(--border));color:hsl(var(--foreground));background-color:#0000;height:2.5rem;padding:.5rem 1rem}.btn-outline:hover{background-color:hsl(var(--accent))}.btn-ghost{color:hsl(var(--foreground));background-color:#0000;height:2.5rem;padding:.5rem 1rem}.btn-ghost:hover{background-color:hsl(var(--accent))}.btn-icon{width:2.5rem;height:2.5rem;color:hsl(var(--foreground));border:1px solid hsl(var(--border));background-color:#0000;padding:0}.btn-icon:hover{background-color:hsl(var(--accent))}.btn-sm{height:1.75rem;padding:.25rem .5rem;font-size:.75rem}.btn-xs{height:1.5rem;padding:.125rem .5rem;font-size:.75rem}.select{border-radius:calc(var(--radius) - 2px);border:1px solid hsl(var(--input));background-color:hsl(var(--card));width:12rem;height:2.25rem;color:hsl(var(--foreground));cursor:pointer;align-items:center;padding:.375rem .75rem;font-size:.875rem;display:inline-flex}.select:focus{outline:2px solid hsl(var(--ring));outline-offset:2px}.select option{background-color:hsl(var(--card));color:hsl(var(--foreground))}.table-wrapper{overflow-x:auto}table.sqd-table{border-collapse:collapse;caption-side:bottom;width:100%;font-size:.875rem}table.sqd-table thead tr{border-bottom:1px solid hsl(var(--border))}table.sqd-table th{text-align:left;vertical-align:middle;text-transform:uppercase;letter-spacing:.05em;height:2.5rem;color:hsl(var(--muted-foreground));white-space:nowrap;padding:0 1rem;font-size:.75rem;font-weight:500}table.sqd-table tbody tr{border-bottom:1px solid hsl(var(--border));transition:background-color .15s}table.sqd-table tbody tr:hover{background-color:hsl(var(--muted)/.5)}table.sqd-table td{vertical-align:middle;padding:.75rem 1rem}.info-line{align-items:center;gap:1rem;padding:.5rem 0;display:flex}.info-line-label{color:hsl(var(--muted-foreground));white-space:nowrap;flex-shrink:0;font-size:.875rem}.info-line-separator{background-color:hsl(var(--border));flex:1;height:1px}.info-line-value{color:hsl(var(--foreground));text-align:right;white-space:nowrap;font-size:.875rem;font-weight:500}@media not all and (min-width:40rem){.info-line-value{white-space:normal;overflow-wrap:anywhere;min-width:0}}.alert{border-radius:var(--radius);border:1px solid;align-items:center;gap:.75rem;padding:1rem;font-size:.875rem;display:flex;position:relative}.alert-success{color:#16a34a;background-color:#22c55e0d;border-color:#22c55e4d}.dark .alert-success{color:#86efac;background-color:#22c55e1a}.alert-error{color:#dc2626;background-color:#ef44440d;border-color:#ef44444d}.dark .alert-error{color:#fca5a5;background-color:#ef44441a}.alert-warning{color:#d97706;background-color:#fbbf240d;border-color:#fbbf244d}.dark .alert-warning{color:#fcd34d;background-color:#fbbf241a}.alert-info{color:#0284c7;background-color:#0ea5e90d;border-color:#0ea5e94d}.dark .alert-info{color:#7dd3fc;background-color:#0ea5e91a}.pagination-nav{justify-content:center;align-items:center;gap:.25rem;display:flex}.pagination-link{min-width:2rem;height:2rem;color:hsl(var(--muted-foreground));border-radius:calc(var(--radius) - 2px);justify-content:center;align-items:center;padding:0 .5rem;font-size:.875rem;font-weight:500;text-decoration:none;transition:background-color .15s;display:inline-flex}.pagination-link:hover{background-color:hsl(var(--accent));color:hsl(var(--foreground))}.pagination-active{background-color:hsl(var(--primary));color:hsl(var(--primary-foreground));border:1px solid hsl(var(--primary))}.pagination-disabled{opacity:.5;cursor:not-allowed}.link{color:hsl(var(--foreground));text-underline-offset:4px;text-decoration:underline}.link:hover{opacity:.75}.sqd-chart{width:100%;height:200px;position:relative}.sqd-chart canvas{width:100%!important;height:100%!important}.code-block{background-color:hsl(var(--muted));border-radius:calc(var(--radius) - 2px);white-space:pre-wrap;word-break:break-word;padding:.75rem;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.8125rem;overflow-x:auto}.stat-card{border-radius:var(--radius);border:1px solid hsl(var(--border));background-color:hsl(var(--card));flex-direction:column;gap:.25rem;padding:1.5rem;text-decoration:none;transition:border-color .15s;display:flex}a.stat-card:hover{border-color:hsl(var(--foreground)/.2)}.stat-card-label{color:hsl(var(--muted-foreground));align-items:center;gap:.5rem;font-size:.875rem;font-weight:500;display:flex}.stat-card-value{color:hsl(var(--foreground));font-size:2.25rem;font-weight:700;line-height:1}.sqd-rate-low{color:#16a34a;background-color:#22c55e1a}.dark .sqd-rate-low{color:#86efac;background-color:#22c55e26}.sqd-rate-medium{color:#d97706;background-color:#fbbf241a}.dark .sqd-rate-medium{color:#fcd34d;background-color:#fbbf2426}.sqd-rate-high{color:#dc2626;background-color:#ef44441a}.dark .sqd-rate-high{color:#fca5a5;background-color:#ef444426}.sqd-retry-badge{border-radius:calc(var(--radius) - 4px);color:#d97706;background-color:#fbbf2426;padding:.125rem .375rem;font-size:.6875rem;font-weight:600}.dark .sqd-retry-badge{color:#fcd34d}html:not(.dark) .sqd-theme-icon-sun{display:none}html:not(.dark) .sqd-theme-icon-moon,html.dark .sqd-theme-icon-sun{display:block}html.dark .sqd-theme-icon-moon{display:none}}@layer utilities{.collapse{visibility:collapse}.relative{position:relative}.static{position:static}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-6{margin-top:calc(var(--spacing)*6)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-2{margin-left:calc(var(--spacing)*2)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.table{display:table}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-4{height:calc(var(--spacing)*4)}.h-6{height:calc(var(--spacing)*6)}.h-12{height:calc(var(--spacing)*12)}.max-h-100{max-height:calc(var(--spacing)*100)}.min-h-screen{min-height:100vh}.w-3\.5{width:calc(var(--spacing)*3.5)}.w-4{width:calc(var(--spacing)*4)}.w-6{width:calc(var(--spacing)*6)}.w-12{width:calc(var(--spacing)*12)}.w-20{width:calc(var(--spacing)*20)}.w-full{width:100%}.max-w-screen-2xl{max-width:var(--breakpoint-2xl)}.flex-shrink-0{flex-shrink:0}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.border{border-style:var(--tw-border-style);border-width:1px}.border-t-4{border-top-style:var(--tw-border-style);border-top-width:4px}.border-t-red-500{border-top-color:var(--color-red-500)}.px-4{padding-inline:calc(var(--spacing)*4)}.py-6{padding-block:calc(var(--spacing)*6)}.py-12{padding-block:calc(var(--spacing)*12)}.pl-1{padding-left:calc(var(--spacing)*1)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-foreground{color:hsl(var(--foreground))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-primary{color:hsl(var(--primary))}.text-red-500{color:var(--color-red-500)}.text-sky-500{color:var(--color-sky-500)}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.no-underline{text-decoration-line:none}.opacity-75{opacity:.75}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}@media (min-width:40rem){.sm\:ml-4{margin-left:calc(var(--spacing)*4)}.sm\:inline{display:inline}.sm\:w-64{width:calc(var(--spacing)*64)}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.sm\:px-6{padding-inline:calc(var(--spacing)*6)}}@media (min-width:64rem){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-500:oklch(63.7% .237 25.331);--color-sky-500:oklch(68.5% .169 237.323);--spacing:.25rem;--breakpoint-2xl:96rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}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;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:root{--background:0 0% 98%;--foreground:240 10% 3.9%;--card:0 0% 100%;--card-foreground:240 10% 3.9%;--primary:240 5.9% 10%;--primary-foreground:0 0% 98%;--secondary:240 4.8% 95.9%;--secondary-foreground:240 5.9% 10%;--muted:240 4.8% 95.9%;--muted-foreground:240 3.8% 46.1%;--accent:240 4.8% 93.8%;--accent-foreground:240 5.9% 10%;--destructive:0 84.2% 60.2%;--destructive-foreground:0 0% 98%;--border:240 5.9% 90%;--input:240 5.9% 90%;--ring:240 5.9% 10%;--radius:.65rem;--chart-1:12 76% 61%;--chart-2:173 58% 39%;--chart-3:197 37% 24%;--chart-4:43 74% 66%;--chart-5:27 87% 67%}.dark{--background:240 10% 5.4%;--foreground:0 0% 92%;--card:240 10% 3.9%;--card-foreground:0 0% 92%;--primary:0 0% 95%;--primary-foreground:240 5.9% 10%;--secondary:240 3.7% 15.9%;--secondary-foreground:0 0% 92%;--muted:240 3.7% 15.9%;--muted-foreground:240 5% 64.9%;--accent:240 3.7% 15.9%;--accent-foreground:0 0% 92%;--destructive:0 62.8% 30.6%;--destructive-foreground:0 0% 92%;--border:240 3.7% 15.9%;--input:240 3.7% 15.9%;--ring:240 4.9% 83.9%;--chart-1:220 70% 50%;--chart-2:160 60% 45%;--chart-3:30 80% 55%;--chart-4:280 65% 60%;--chart-5:340 75% 55%}*,:before,:after{box-sizing:border-box}body{background-color:hsl(var(--background));color:hsl(var(--foreground));-webkit-font-smoothing:antialiased;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji}}@layer components{.navbar{border-bottom:1px solid hsl(var(--border));background-color:hsl(var(--card));align-items:center;gap:1rem;height:3.5rem;padding:0 1.5rem;display:flex}.navbar-section{align-items:center;gap:.375rem;display:flex}@media not all and (min-width:40rem){.navbar{flex-wrap:wrap;gap:.5rem;height:auto;min-height:3.5rem;padding:.5rem 1rem}.navbar-section{flex-wrap:wrap;flex-basis:100%;order:3}}.navbar-item{color:hsl(var(--muted-foreground));border-radius:calc(var(--radius) - 2px);white-space:nowrap;align-items:center;gap:.375rem;padding:.375rem .75rem;font-size:.875rem;font-weight:500;text-decoration:none;transition:color .15s,background-color .15s;display:inline-flex}.navbar-item:hover{color:hsl(var(--foreground));background-color:hsl(var(--accent))}.navbar-item-current{color:hsl(var(--primary-foreground));background-color:hsl(var(--primary))}.navbar-item-current:hover{color:hsl(var(--primary-foreground));background-color:hsl(var(--primary));opacity:.9}.navbar-badge{border-radius:calc(var(--radius) - 4px);background-color:hsl(var(--muted));color:hsl(var(--muted-foreground));padding:.125rem .375rem;font-size:.75rem;font-weight:600}.navbar-item-current .navbar-badge{background-color:hsl(var(--primary-foreground)/.2);color:hsl(var(--primary-foreground))}.card{border-radius:var(--radius);border:1px solid hsl(var(--border));background-color:hsl(var(--card));color:hsl(var(--card-foreground));box-shadow:0 1px 2px #0000000d}.card-header{flex-direction:column;gap:.375rem;padding:1.5rem;display:flex}.card-title{letter-spacing:-.025em;margin:0;font-size:1rem;font-weight:600;line-height:1}.card-content{padding:1.5rem;padding-top:0}.card-footer{align-items:center;padding:0 1.5rem 1.5rem;display:flex}.badge{border-radius:calc(var(--radius) - 2px);white-space:nowrap;align-items:center;gap:.375rem;padding:.125rem .625rem;font-size:.75rem;font-weight:500;transition:color .15s,background-color .15s;display:inline-flex}.badge-primary{background-color:hsl(var(--primary));color:hsl(var(--primary-foreground))}.badge-secondary{background-color:hsl(var(--secondary));color:hsl(var(--secondary-foreground))}.badge-red{color:#ef4444;background-color:#ef44441a}.dark .badge-red{color:#fca5a5;background-color:#ef444426}.badge-amber{color:#d97706;background-color:#fbbf241a}.dark .badge-amber{color:#fcd34d;background-color:#fbbf2426}.badge-yellow{color:#a16207;background-color:#facc151a}.dark .badge-yellow{color:#fde047;background-color:#facc1526}.badge-green{color:#16a34a;background-color:#22c55e1a}.dark .badge-green{color:#86efac;background-color:#22c55e26}.badge-sky{color:#0284c7;background-color:#0ea5e91a}.dark .badge-sky{color:#7dd3fc;background-color:#0ea5e926}.badge-zinc{color:#52525b;background-color:#52525b1a}.dark .badge-zinc{color:#a1a1aa;background-color:#52525b33}.circle{border-radius:9999px;flex-shrink:0;width:.5rem;height:.5rem;display:inline-block}.circle-red{background-color:#ef4444}.circle-amber{background-color:#fbbf24}.circle-green{background-color:#22c55e}.circle-yellow{background-color:#facc15}.circle-sky{background-color:#0ea5e9}.circle-zinc{background-color:#52525b}.btn{white-space:nowrap;border-radius:calc(var(--radius) - 2px);cursor:pointer;border:none;justify-content:center;align-items:center;gap:.5rem;font-size:.875rem;font-weight:500;line-height:1;text-decoration:none;transition:color .15s,background-color .15s,opacity .15s;display:inline-flex}.btn:disabled{opacity:.5;cursor:not-allowed}.btn-default{background-color:hsl(var(--primary));color:hsl(var(--primary-foreground));height:2.5rem;padding:.5rem 1rem}.btn-default:hover{opacity:.9}.btn-secondary{background-color:hsl(var(--secondary));color:hsl(var(--secondary-foreground));border:1px solid hsl(var(--border));height:2.5rem;padding:.5rem 1rem}.btn-secondary:hover{background-color:hsl(var(--accent))}.btn-destructive{background-color:hsl(var(--destructive));color:hsl(var(--destructive-foreground));height:2.5rem;padding:.5rem 1rem}.btn-destructive:hover{opacity:.9}.btn-outline{border:1px solid hsl(var(--border));color:hsl(var(--foreground));background-color:#0000;height:2.5rem;padding:.5rem 1rem}.btn-outline:hover{background-color:hsl(var(--accent))}.btn-icon{width:2.5rem;height:2.5rem;color:hsl(var(--foreground));border:1px solid hsl(var(--border));background-color:#0000;padding:0}.btn-icon:hover{background-color:hsl(var(--accent))}.btn-sm{height:1.75rem;padding:.25rem .5rem;font-size:.75rem}.btn-xs{height:1.5rem;padding:.125rem .5rem;font-size:.75rem}.select{border-radius:calc(var(--radius) - 2px);border:1px solid hsl(var(--input));background-color:hsl(var(--card));width:12rem;height:2.25rem;color:hsl(var(--foreground));cursor:pointer;align-items:center;padding:.375rem .75rem;font-size:.875rem;display:inline-flex}.select:focus{outline:2px solid hsl(var(--ring));outline-offset:2px}.select option{background-color:hsl(var(--card));color:hsl(var(--foreground))}.table-wrapper{overflow-x:auto}table.sqd-table{border-collapse:collapse;caption-side:bottom;width:100%;font-size:.875rem}table.sqd-table thead tr{border-bottom:1px solid hsl(var(--border))}table.sqd-table th{text-align:left;vertical-align:middle;text-transform:uppercase;letter-spacing:.05em;height:2.5rem;color:hsl(var(--muted-foreground));white-space:nowrap;padding:0 1rem;font-size:.75rem;font-weight:500}table.sqd-table tbody tr{border-bottom:1px solid hsl(var(--border));transition:background-color .15s}table.sqd-table tbody tr:hover{background-color:hsl(var(--muted)/.5)}table.sqd-table td{vertical-align:middle;padding:.75rem 1rem}.info-line{align-items:center;gap:1rem;padding:.5rem 0;display:flex}.info-line-label{color:hsl(var(--muted-foreground));white-space:nowrap;flex-shrink:0;font-size:.875rem}.info-line-separator{background-color:hsl(var(--border));flex:1;height:1px}.info-line-value{color:hsl(var(--foreground));text-align:right;white-space:nowrap;font-size:.875rem;font-weight:500}@media not all and (min-width:40rem){.info-line-value{white-space:normal;overflow-wrap:anywhere;min-width:0}}.alert{border-radius:var(--radius);border:1px solid;align-items:center;gap:.75rem;padding:1rem;font-size:.875rem;display:flex;position:relative}.alert-success{color:#16a34a;background-color:#22c55e0d;border-color:#22c55e4d}.dark .alert-success{color:#86efac;background-color:#22c55e1a}.alert-error{color:#dc2626;background-color:#ef44440d;border-color:#ef44444d}.dark .alert-error{color:#fca5a5;background-color:#ef44441a}.alert-warning{color:#d97706;background-color:#fbbf240d;border-color:#fbbf244d}.dark .alert-warning{color:#fcd34d;background-color:#fbbf241a}.pagination-nav{justify-content:center;align-items:center;gap:.25rem;display:flex}.pagination-link{min-width:2rem;height:2rem;color:hsl(var(--muted-foreground));border-radius:calc(var(--radius) - 2px);justify-content:center;align-items:center;padding:0 .5rem;font-size:.875rem;font-weight:500;text-decoration:none;transition:background-color .15s;display:inline-flex}.pagination-link:hover{background-color:hsl(var(--accent));color:hsl(var(--foreground))}.pagination-active{background-color:hsl(var(--primary));color:hsl(var(--primary-foreground));border:1px solid hsl(var(--primary))}.pagination-disabled{opacity:.5;cursor:not-allowed}.link{color:hsl(var(--foreground));text-underline-offset:4px;text-decoration:underline}.link:hover{opacity:.75}.sqd-chart{width:100%;height:200px;position:relative}.sqd-chart canvas{width:100%!important;height:100%!important}.code-block{background-color:hsl(var(--muted));border-radius:calc(var(--radius) - 2px);white-space:pre-wrap;word-break:break-word;padding:.75rem;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.8125rem;overflow-x:auto}.stat-card{border-radius:var(--radius);border:1px solid hsl(var(--border));background-color:hsl(var(--card));flex-direction:column;gap:.25rem;padding:1.5rem;text-decoration:none;transition:border-color .15s;display:flex}a.stat-card:hover{border-color:hsl(var(--foreground)/.2)}.stat-card-label{color:hsl(var(--muted-foreground));align-items:center;gap:.5rem;font-size:.875rem;font-weight:500;display:flex}.stat-card-value{color:hsl(var(--foreground));font-size:2.25rem;font-weight:700;line-height:1}.sqd-rate-low{color:#16a34a;background-color:#22c55e1a}.dark .sqd-rate-low{color:#86efac;background-color:#22c55e26}.sqd-rate-medium{color:#d97706;background-color:#fbbf241a}.dark .sqd-rate-medium{color:#fcd34d;background-color:#fbbf2426}.sqd-rate-high{color:#dc2626;background-color:#ef44441a}.dark .sqd-rate-high{color:#fca5a5;background-color:#ef444426}.sqd-retry-badge{border-radius:calc(var(--radius) - 4px);color:#d97706;background-color:#fbbf2426;padding:.125rem .375rem;font-size:.6875rem;font-weight:600}.dark .sqd-retry-badge{color:#fcd34d}html:not(.dark) .sqd-theme-icon-sun{display:none}html:not(.dark) .sqd-theme-icon-moon,html.dark .sqd-theme-icon-sun{display:block}html.dark .sqd-theme-icon-moon{display:none}}@layer utilities{.collapse{visibility:collapse}.relative{position:relative}.static{position:static}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-6{margin-top:calc(var(--spacing)*6)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-2{margin-left:calc(var(--spacing)*2)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.table{display:table}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-4{height:calc(var(--spacing)*4)}.h-6{height:calc(var(--spacing)*6)}.h-12{height:calc(var(--spacing)*12)}.max-h-100{max-height:calc(var(--spacing)*100)}.min-h-screen{min-height:100vh}.w-3\.5{width:calc(var(--spacing)*3.5)}.w-4{width:calc(var(--spacing)*4)}.w-6{width:calc(var(--spacing)*6)}.w-12{width:calc(var(--spacing)*12)}.w-20{width:calc(var(--spacing)*20)}.w-full{width:100%}.max-w-screen-2xl{max-width:var(--breakpoint-2xl)}.flex-shrink-0{flex-shrink:0}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.border{border-style:var(--tw-border-style);border-width:1px}.border-t-4{border-top-style:var(--tw-border-style);border-top-width:4px}.border-t-red-500{border-top-color:var(--color-red-500)}.px-4{padding-inline:calc(var(--spacing)*4)}.py-6{padding-block:calc(var(--spacing)*6)}.py-12{padding-block:calc(var(--spacing)*12)}.pl-1{padding-left:calc(var(--spacing)*1)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-foreground{color:hsl(var(--foreground))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-primary{color:hsl(var(--primary))}.text-red-500{color:var(--color-red-500)}.text-sky-500{color:var(--color-sky-500)}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.no-underline{text-decoration-line:none}.opacity-75{opacity:.75}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}@media (min-width:40rem){.sm\:ml-4{margin-left:calc(var(--spacing)*4)}.sm\:inline{display:inline}.sm\:w-64{width:calc(var(--spacing)*64)}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.sm\:px-6{padding-inline:calc(var(--spacing)*6)}}@media (min-width:64rem){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
|
|
@@ -142,10 +142,6 @@
|
|
|
142
142
|
line-height: 1;
|
|
143
143
|
letter-spacing: -0.025em;
|
|
144
144
|
}
|
|
145
|
-
.card-description {
|
|
146
|
-
font-size: 0.875rem;
|
|
147
|
-
color: hsl(var(--muted-foreground));
|
|
148
|
-
}
|
|
149
145
|
.card-content { padding: 1.5rem; padding-top: 0; }
|
|
150
146
|
.card-footer { display: flex; align-items: center; padding: 1.5rem; padding-top: 0; }
|
|
151
147
|
|
|
@@ -163,8 +159,6 @@
|
|
|
163
159
|
}
|
|
164
160
|
.badge-primary { background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); }
|
|
165
161
|
.badge-secondary { background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); }
|
|
166
|
-
.badge-destructive { background-color: hsl(var(--destructive)); color: hsl(var(--destructive-foreground)); }
|
|
167
|
-
.badge-outline { border: 1px solid hsl(var(--border)); color: hsl(var(--foreground)); background-color: transparent; }
|
|
168
162
|
|
|
169
163
|
.badge-red { background-color: rgb(239 68 68 / 0.1); color: rgb(239 68 68); }
|
|
170
164
|
.dark .badge-red { background-color: rgb(239 68 68 / 0.15); color: rgb(252 165 165); }
|
|
@@ -235,13 +229,6 @@
|
|
|
235
229
|
height: 2.5rem;
|
|
236
230
|
}
|
|
237
231
|
.btn-outline:hover { background-color: hsl(var(--accent)); }
|
|
238
|
-
.btn-ghost {
|
|
239
|
-
background-color: transparent;
|
|
240
|
-
color: hsl(var(--foreground));
|
|
241
|
-
padding: 0.5rem 1rem;
|
|
242
|
-
height: 2.5rem;
|
|
243
|
-
}
|
|
244
|
-
.btn-ghost:hover { background-color: hsl(var(--accent)); }
|
|
245
232
|
.btn-icon {
|
|
246
233
|
padding: 0;
|
|
247
234
|
width: 2.5rem;
|
|
@@ -349,12 +336,6 @@
|
|
|
349
336
|
color: rgb(217 119 6);
|
|
350
337
|
}
|
|
351
338
|
.dark .alert-warning { color: rgb(252 211 77); background-color: rgb(251 191 36 / 0.1); }
|
|
352
|
-
.alert-info {
|
|
353
|
-
border-color: rgb(14 165 233 / 0.3);
|
|
354
|
-
background-color: rgb(14 165 233 / 0.05);
|
|
355
|
-
color: rgb(2 132 199);
|
|
356
|
-
}
|
|
357
|
-
.dark .alert-info { color: rgb(125 211 252); background-color: rgb(14 165 233 / 0.1); }
|
|
358
339
|
|
|
359
340
|
/* Pagination */
|
|
360
341
|
.pagination-nav { display: flex; align-items: center; justify-content: center; gap: 0.25rem; }
|
|
@@ -437,18 +418,6 @@
|
|
|
437
418
|
.dark .sqd-rate-high { background-color: rgb(239 68 68 / 0.15); color: rgb(252 165 165); }
|
|
438
419
|
|
|
439
420
|
/* Legacy compat: sqd- prefixed classes used by existing tests */
|
|
440
|
-
.sqd-nav-link { }
|
|
441
|
-
.sqd-nav-badge { }
|
|
442
|
-
.sqd-theme-toggle { }
|
|
443
|
-
.sqd-theme-icon-sun, .sqd-theme-icon-moon { }
|
|
444
|
-
.sqd-refresh-selector { }
|
|
445
|
-
.sqd-refresh-select { }
|
|
446
|
-
.sqd-per-page-selector { }
|
|
447
|
-
.sqd-per-page-select { }
|
|
448
|
-
.sqd-filters { }
|
|
449
|
-
.sqd-filter-select { }
|
|
450
|
-
.sqd-failure-rates { }
|
|
451
|
-
.sqd-failure-table { }
|
|
452
421
|
.sqd-retry-badge {
|
|
453
422
|
padding: 0.125rem 0.375rem;
|
|
454
423
|
font-size: 0.6875rem;
|
|
@@ -2,30 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module JobHarbor
|
|
4
4
|
class BadgeComponent < ApplicationComponent
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"in_progress" => "badge-amber",
|
|
11
|
-
"failed" => "badge-red",
|
|
12
|
-
"finished" => "badge-green",
|
|
13
|
-
"blocked" => "badge-zinc",
|
|
14
|
-
"active" => "badge-green",
|
|
15
|
-
"paused" => "badge-amber",
|
|
16
|
-
"stale" => "badge-zinc"
|
|
17
|
-
}.freeze
|
|
18
|
-
|
|
19
|
-
STATUS_CIRCLES = {
|
|
20
|
-
"pending" => "circle-sky",
|
|
21
|
-
"scheduled" => "circle-yellow",
|
|
22
|
-
"in_progress" => "circle-amber",
|
|
23
|
-
"failed" => "circle-red",
|
|
24
|
-
"finished" => "circle-green",
|
|
25
|
-
"blocked" => "circle-zinc",
|
|
26
|
-
"active" => "circle-green",
|
|
27
|
-
"paused" => "circle-amber",
|
|
28
|
-
"stale" => "circle-zinc"
|
|
5
|
+
# Statuses that belong to workers and queues rather than jobs.
|
|
6
|
+
OTHER_COLORS = {
|
|
7
|
+
"active" => "green",
|
|
8
|
+
"paused" => "amber",
|
|
9
|
+
"stale" => "zinc"
|
|
29
10
|
}.freeze
|
|
30
11
|
|
|
31
12
|
def initialize(status:)
|
|
@@ -44,13 +25,16 @@ module JobHarbor
|
|
|
44
25
|
private
|
|
45
26
|
|
|
46
27
|
def css_classes
|
|
47
|
-
|
|
48
|
-
"badge #{STATUS_COLORS[status_key]}"
|
|
28
|
+
"badge badge-#{color}"
|
|
49
29
|
end
|
|
50
30
|
|
|
51
31
|
def circle_class
|
|
52
|
-
|
|
53
|
-
|
|
32
|
+
"circle-#{color}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Anything unrecognised renders as the first job status.
|
|
36
|
+
def color
|
|
37
|
+
JobStatus.find(@status)&.color || OTHER_COLORS.fetch(@status) { JobStatus.all.first.color }
|
|
54
38
|
end
|
|
55
39
|
|
|
56
40
|
def display_text
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JobHarbor
|
|
4
|
+
class InfoLineComponent < ApplicationComponent
|
|
5
|
+
def initialize(label:, value: nil)
|
|
6
|
+
@label = label
|
|
7
|
+
@value = value
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
content_tag(:div, class: "info-line") do
|
|
12
|
+
safe_join([
|
|
13
|
+
content_tag(:span, @label, class: "info-line-label"),
|
|
14
|
+
content_tag(:span, "", class: "info-line-separator"),
|
|
15
|
+
content_tag(:span, content? ? content : @value, class: "info-line-value")
|
|
16
|
+
])
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -38,7 +38,7 @@ module JobHarbor
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def retry_badge_tag
|
|
41
|
-
return nil unless @job.
|
|
41
|
+
return nil unless @job.retry_badge.present?
|
|
42
42
|
|
|
43
43
|
content_tag(:span, @job.retry_badge, class: "sqd-retry-badge ml-1")
|
|
44
44
|
end
|
|
@@ -59,7 +59,7 @@ module JobHarbor
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def running_duration_tag
|
|
62
|
-
return nil unless @job.
|
|
62
|
+
return nil unless @job.running_duration.present?
|
|
63
63
|
|
|
64
64
|
content_tag(:span, " (#{@job.running_duration})", class: "text-xs text-sky-500")
|
|
65
65
|
end
|
|
@@ -67,7 +67,7 @@ module JobHarbor
|
|
|
67
67
|
def scheduled_cell
|
|
68
68
|
content_tag(:td) do
|
|
69
69
|
if @job.scheduled_at
|
|
70
|
-
time_tag(@job.scheduled_at, @job.scheduled_at.
|
|
70
|
+
time_tag(@job.scheduled_at, @job.scheduled_at.to_fs(:job_harbor_short))
|
|
71
71
|
else
|
|
72
72
|
relative_time_tag
|
|
73
73
|
end
|
|
@@ -75,11 +75,7 @@ module JobHarbor
|
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
def relative_time_tag
|
|
78
|
-
|
|
79
|
-
content_tag(:span, @job.relative_created_at, class: "text-xs text-muted-foreground")
|
|
80
|
-
else
|
|
81
|
-
content_tag(:span, "\u2014", class: "text-muted-foreground")
|
|
82
|
-
end
|
|
78
|
+
content_tag(:span, @job.relative_created_at, class: "text-xs text-muted-foreground")
|
|
83
79
|
end
|
|
84
80
|
|
|
85
81
|
def actions_cell
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<%= form_with url: search_path, method: :get, class: "flex flex-wrap items-center gap-2" do |f| %>
|
|
2
|
+
<%= f.text_field :q, placeholder: "Search jobs...", class: "select w-full sm:w-64", value: @query %>
|
|
3
|
+
<%= f.submit "Search", class: "btn btn-secondary btn-sm" %>
|
|
4
|
+
<% end %>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<div class="card">
|
|
2
|
+
<% if @title %>
|
|
3
|
+
<div class="card-header">
|
|
4
|
+
<h2 class="card-title"><%= @title %></h2>
|
|
5
|
+
</div>
|
|
6
|
+
<% end %>
|
|
7
|
+
<div class="table-wrapper">
|
|
8
|
+
<table class="sqd-table">
|
|
9
|
+
<thead>
|
|
10
|
+
<tr>
|
|
11
|
+
<th>ID</th>
|
|
12
|
+
<th>Job Class</th>
|
|
13
|
+
<th>Queue</th>
|
|
14
|
+
<th>Status</th>
|
|
15
|
+
<th>Scheduled At</th>
|
|
16
|
+
<th>Actions</th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<% @jobs.each do |job| %>
|
|
21
|
+
<%= render JobHarbor::JobRowComponent.new(job: job) %>
|
|
22
|
+
<% end %>
|
|
23
|
+
</tbody>
|
|
24
|
+
</table>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module JobHarbor
|
|
4
4
|
class NavLinkComponent < ApplicationComponent
|
|
5
|
-
def initialize(path:, label:,
|
|
5
|
+
def initialize(path:, label:, active: false, badge: nil)
|
|
6
6
|
@path = path
|
|
7
7
|
@label = label
|
|
8
|
-
@icon = icon.to_sym
|
|
9
8
|
@active = active
|
|
10
9
|
@badge = badge
|
|
11
10
|
end
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module JobHarbor
|
|
4
4
|
class PaginationComponent < ApplicationComponent
|
|
5
|
-
def initialize(
|
|
6
|
-
@
|
|
5
|
+
def initialize(page_info:)
|
|
6
|
+
@page_info = page_info
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def render?
|
|
10
|
-
@
|
|
10
|
+
@page_info.pages > 1
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def call
|
|
@@ -23,16 +23,16 @@ module JobHarbor
|
|
|
23
23
|
private
|
|
24
24
|
|
|
25
25
|
def prev_link
|
|
26
|
-
if @
|
|
27
|
-
link_to "\u2190 Prev", url_for(page: @
|
|
26
|
+
if @page_info.prev
|
|
27
|
+
link_to "\u2190 Prev", url_for(page: @page_info.prev), class: "pagination-link"
|
|
28
28
|
else
|
|
29
29
|
content_tag(:span, "\u2190 Prev", class: "pagination-link pagination-disabled")
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def next_link
|
|
34
|
-
if @
|
|
35
|
-
link_to "Next \u2192", url_for(page: @
|
|
34
|
+
if @page_info.next
|
|
35
|
+
link_to "Next \u2192", url_for(page: @page_info.next), class: "pagination-link"
|
|
36
36
|
else
|
|
37
37
|
content_tag(:span, "Next \u2192", class: "pagination-link pagination-disabled")
|
|
38
38
|
end
|
|
@@ -42,7 +42,7 @@ module JobHarbor
|
|
|
42
42
|
series.map do |item|
|
|
43
43
|
case item
|
|
44
44
|
when Integer
|
|
45
|
-
if item == @
|
|
45
|
+
if item == @page_info.page
|
|
46
46
|
content_tag(:span, item, class: "pagination-link pagination-active")
|
|
47
47
|
else
|
|
48
48
|
link_to item, url_for(page: item), class: "pagination-link"
|
|
@@ -54,8 +54,8 @@ module JobHarbor
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def series
|
|
57
|
-
pages = @
|
|
58
|
-
current = @
|
|
57
|
+
pages = @page_info.pages
|
|
58
|
+
current = @page_info.page
|
|
59
59
|
|
|
60
60
|
if pages <= 7
|
|
61
61
|
(1..pages).to_a
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module JobHarbor
|
|
4
4
|
class QueueCardComponent < ApplicationComponent
|
|
5
|
+
# Failed is left off the card; the queue page shows it.
|
|
6
|
+
STATUSES = %w[pending scheduled in_progress blocked].map { |key| JobStatus.find(key) }.freeze
|
|
7
|
+
|
|
5
8
|
def initialize(queue:)
|
|
6
9
|
@queue = queue
|
|
7
10
|
end
|
|
@@ -31,12 +34,7 @@ module JobHarbor
|
|
|
31
34
|
|
|
32
35
|
def stats
|
|
33
36
|
content_tag(:div, class: "grid grid-cols-4 gap-4 mb-4") do
|
|
34
|
-
safe_join(
|
|
35
|
-
stat("Pending", @queue.pending_count),
|
|
36
|
-
stat("Scheduled", @queue.scheduled_count),
|
|
37
|
-
stat("In Progress", @queue.in_progress_count),
|
|
38
|
-
stat("Blocked", @queue.blocked_count)
|
|
39
|
-
])
|
|
37
|
+
safe_join(STATUSES.map { |status| stat(status.label, @queue.count(status)) })
|
|
40
38
|
end
|
|
41
39
|
end
|
|
42
40
|
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<%= form_with url: queues_pauses_path, method: :post, class: "flex flex-wrap items-center gap-2 mb-4" do |f| %>
|
|
2
|
+
<%= f.text_field :name, placeholder: "Queue name", maxlength: JobHarbor::QueueStats::MAX_NAME_LENGTH, required: true, autocomplete: "off",
|
|
3
|
+
"aria-label": "Queue name to pause", class: "select w-full sm:w-64" %>
|
|
4
|
+
<%= f.button "Pause", type: "submit", class: "btn btn-secondary btn-sm" %>
|
|
5
|
+
<% end %>
|
|
@@ -2,16 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module JobHarbor
|
|
4
4
|
class StatCardComponent < ApplicationComponent
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
workers: "circle-green",
|
|
13
|
-
queues: "circle-sky",
|
|
14
|
-
throughput: "circle-green"
|
|
5
|
+
# Cards that are not about a job status.
|
|
6
|
+
OTHER_COLORS = {
|
|
7
|
+
workers: "green",
|
|
8
|
+
queues: "sky",
|
|
9
|
+
throughput: "green",
|
|
10
|
+
live: "green",
|
|
11
|
+
stale: "zinc"
|
|
15
12
|
}.freeze
|
|
16
13
|
|
|
17
14
|
def initialize(label:, value:, type: nil, link: nil)
|
|
@@ -48,10 +45,10 @@ module JobHarbor
|
|
|
48
45
|
def circle_indicator
|
|
49
46
|
return nil unless @type
|
|
50
47
|
|
|
51
|
-
|
|
52
|
-
return nil unless
|
|
48
|
+
color = JobStatus.find(@type)&.color || OTHER_COLORS[@type]
|
|
49
|
+
return nil unless color
|
|
53
50
|
|
|
54
|
-
content_tag(:span, "", class: "circle
|
|
51
|
+
content_tag(:span, "", class: "circle circle-#{color}")
|
|
55
52
|
end
|
|
56
53
|
|
|
57
54
|
def value_display
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JobHarbor
|
|
4
|
+
class StatusTabsComponent < ApplicationComponent
|
|
5
|
+
# status is the key of the open tab, or nil for "All"; counts is keyed "all" and by status key.
|
|
6
|
+
def initialize(status:, counts:)
|
|
7
|
+
@status = status
|
|
8
|
+
@counts = counts
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
content_tag(:div, class: "flex gap-1 mb-4 flex-wrap") do
|
|
13
|
+
safe_join([ all_tab, *JobStatus.all.map { |job_status| status_tab(job_status) } ])
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def all_tab
|
|
20
|
+
link_to jobs_path, class: tab_classes(selected: !@status) do
|
|
21
|
+
safe_join([ "All ", count_tag("all", "ml-1 opacity-75") ])
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def status_tab(job_status)
|
|
26
|
+
link_to public_send(job_status.path_helper), class: tab_classes(selected: @status == job_status.key) do
|
|
27
|
+
safe_join([
|
|
28
|
+
content_tag(:span, "", class: "circle #{job_status.circle_class}"),
|
|
29
|
+
" #{job_status.label} ",
|
|
30
|
+
count_tag(job_status.key, "opacity-75")
|
|
31
|
+
])
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def count_tag(key, css_class)
|
|
36
|
+
content_tag(:span, @counts[key], class: css_class)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def tab_classes(selected:)
|
|
40
|
+
"badge #{selected ? 'badge-primary' : 'badge-secondary'}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -50,19 +50,9 @@ module JobHarbor
|
|
|
50
50
|
def details
|
|
51
51
|
content_tag(:div, class: "space-y-1") do
|
|
52
52
|
safe_join([
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
])
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def info_line(label, value)
|
|
61
|
-
content_tag(:div, class: "info-line") do
|
|
62
|
-
safe_join([
|
|
63
|
-
content_tag(:span, label, class: "info-line-label"),
|
|
64
|
-
content_tag(:span, "", class: "info-line-separator"),
|
|
65
|
-
content_tag(:span, value, class: "info-line-value")
|
|
53
|
+
render(InfoLineComponent.new(label: "Hostname", value: @worker.hostname)),
|
|
54
|
+
render(InfoLineComponent.new(label: "PID", value: @worker.pid)),
|
|
55
|
+
render(InfoLineComponent.new(label: "Last Heartbeat", value: heartbeat_time))
|
|
66
56
|
])
|
|
67
57
|
end
|
|
68
58
|
end
|