tramway 4.0.1.4 → 4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +81 -0
- data/app/assets/javascripts/tramway/tramway.js +140 -2
- data/app/controllers/tramway/entities_controller.rb +35 -6
- data/app/controllers/tramway/plugins/solid_queue/application_controller.rb +28 -0
- data/app/controllers/tramway/plugins/solid_queue/jobs_controller.rb +82 -0
- data/app/controllers/tramway/plugins/solid_queue/queues_controller.rb +38 -0
- data/app/controllers/tramway/plugins/solid_queue/recurring_tasks_controller.rb +26 -0
- data/app/helpers/tramway/plugins/solid_queue/jobs_helper.rb +27 -0
- data/app/helpers/tramway/plugins/solid_queue/queues_helper.rb +27 -0
- data/app/jobs/tramway/solid_queue/application_job.rb +14 -0
- data/app/jobs/tramway/solid_queue/bulk_destroy_job.rb +13 -0
- data/app/jobs/tramway/solid_queue/bulk_discard_job.rb +13 -0
- data/app/jobs/tramway/solid_queue/bulk_retry_job.rb +13 -0
- data/app/jobs/tramway/solid_queue/destroy_all_job.rb +13 -0
- data/app/models/tramway/solid_queue/jobs_filter.rb +53 -0
- data/app/views/tramway/entities/_filters.html.haml +15 -0
- data/app/views/tramway/entities/_list.html.haml +22 -8
- data/app/views/tramway/plugins/solid_queue/_subnav.html.haml +7 -0
- data/app/views/tramway/plugins/solid_queue/jobs/index.html.haml +89 -0
- data/app/views/tramway/plugins/solid_queue/jobs/show.html.haml +90 -0
- data/app/views/tramway/plugins/solid_queue/queues/index.html.haml +46 -0
- data/app/views/tramway/plugins/solid_queue/recurring_tasks/index.html.haml +33 -0
- data/config/locales/en.yml +64 -0
- data/config/routes.rb +10 -0
- data/config/tailwind.config.js +4 -0
- data/lib/generators/tramway/install/install_generator.rb +17 -3
- data/lib/generators/tramway/install/solid_queue_helpers.rb +54 -0
- data/lib/tramway/config.rb +7 -1
- data/lib/tramway/configs/entities/page.rb +1 -0
- data/lib/tramway/configs/plugins.rb +66 -0
- data/lib/tramway/errors.rb +56 -0
- data/lib/tramway/navbar.rb +23 -0
- data/lib/tramway/plugins/base.rb +33 -0
- data/lib/tramway/plugins/solid_queue/config.rb +13 -0
- data/lib/tramway/plugins/solid_queue/plugin.rb +37 -0
- data/lib/tramway/plugins/solid_queue/routes.rb +50 -0
- data/lib/tramway/plugins/solid_queue.rb +7 -0
- data/lib/tramway/plugins.rb +37 -0
- data/lib/tramway/version.rb +1 -1
- metadata +27 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a20badcbb5b3d0d460bbadafb95816d4932ce5e8af0d41a9ce2315507220bed0
|
|
4
|
+
data.tar.gz: 800c9136a41a012e3b8581611afa70e666f81c4dab9d331cc7c094d096cb2b86
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 84e42fc4f2acf64a31007add38185688e84d9728873a528a1d1b787b5b7383e94b5b7ff22ee62c0040eb75f0daae50d0d1c182f68424a39919e3f4ee2ef90a92
|
|
7
|
+
data.tar.gz: de23a5e5b76877268ce42d2371f5db9907c3ce6a517b15ea7b234a2ecc944c6a04fb3b70001b098ab06b07d52f25fdeae1a36ec9112eb2530be0394f719354d2
|
data/README.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Tramway
|
|
2
2
|
Unite Ruby on Rails brilliance. Streamline development with Tramway.
|
|
3
3
|
|
|
4
|
+
> **AI agents: read this first.** Before writing or changing any code in a Rails app that uses Tramway, install and
|
|
5
|
+
> use the [`tramway-skill`](https://github.com/Purple-Magic/tramway-skill/) skill — it is mandatory for Tramway work
|
|
6
|
+
> and is the authoritative, up-to-date playbook for Tramway's conventions (CRUD, Forms, Decorators, Navbar, Plugins,
|
|
7
|
+
> and more). Do not generate Tramway-related code from memory alone.
|
|
8
|
+
|
|
4
9
|
Tramway ships with full CRUD (index, show, create, update, destroy) out of the box. Its install generator also adds a
|
|
5
10
|
Codex instruction that points agents to the Tramway skill for Tramway-native code generation.
|
|
6
11
|
|
|
@@ -261,6 +266,40 @@ In this example, Tramway calls `Campaign.includes(:user, :comments)` before appl
|
|
|
261
266
|
`campaign.user` or `campaign.comments` in the decorator's `index_attributes` does not issue an extra query per row.
|
|
262
267
|
Omitting `includes` produces identical behavior to before this option existed.
|
|
263
268
|
|
|
269
|
+
**filters**
|
|
270
|
+
|
|
271
|
+
Filters are disabled by default on index pages. Enable them by listing the scope names you want selectable under
|
|
272
|
+
`filters` on the `:index` page entry:
|
|
273
|
+
|
|
274
|
+
*config/initializers/tramway.rb*
|
|
275
|
+
```ruby
|
|
276
|
+
Tramway.configure do |config|
|
|
277
|
+
config.entities = [
|
|
278
|
+
{
|
|
279
|
+
name: :campaign,
|
|
280
|
+
namespace: :admin,
|
|
281
|
+
pages: [
|
|
282
|
+
{
|
|
283
|
+
action: :index,
|
|
284
|
+
filters: [:active, :archived]
|
|
285
|
+
}
|
|
286
|
+
]
|
|
287
|
+
}
|
|
288
|
+
]
|
|
289
|
+
end
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
When filters are configured, Tramway adds a "Filters" toggle to the same row as the search box and pagination, above
|
|
293
|
+
the index listing. Clicking it reveals a list of small buttons, one per configured filter, each showing how many
|
|
294
|
+
records currently match it (e.g. "ACTIVE · 34"). Clicking a button navigates to the index page with that filter
|
|
295
|
+
applied, keeping any existing search query. The currently applied filter is highlighted, and the toggle remembers
|
|
296
|
+
whether the list was last left open or closed.
|
|
297
|
+
|
|
298
|
+
Visiting the index page with `?filter=active` calls `Campaign.active` (the scope must already exist on the model) in
|
|
299
|
+
addition to any configured `scope`. If `filter` does not match one of the configured filters, Tramway raises
|
|
300
|
+
`Tramway::Errors::InvalidFilterError` explaining which filters are available. Omitting `filters` (the default) makes
|
|
301
|
+
Tramway ignore `params[:filter]` entirely.
|
|
302
|
+
|
|
264
303
|
**show page**
|
|
265
304
|
|
|
266
305
|
To render a show page for an entity, declare a `:show` action inside the `pages` array in
|
|
@@ -1732,6 +1771,48 @@ Tramway.configure do |config|
|
|
|
1732
1771
|
end
|
|
1733
1772
|
```
|
|
1734
1773
|
|
|
1774
|
+
## Plugins
|
|
1775
|
+
|
|
1776
|
+
Plugins add optional, ready-made dashboards to your app, mounted and styled with Tramway. Enable one by name and,
|
|
1777
|
+
optionally, configure it:
|
|
1778
|
+
|
|
1779
|
+
```ruby
|
|
1780
|
+
Tramway.configure do |config|
|
|
1781
|
+
config.plugins = [:solid_queue]
|
|
1782
|
+
config.plugins.solid_queue = {
|
|
1783
|
+
path: '/jobs'
|
|
1784
|
+
}
|
|
1785
|
+
end
|
|
1786
|
+
```
|
|
1787
|
+
|
|
1788
|
+
Enabling a plugin also adds a navbar entry for it automatically (see [Tramway
|
|
1789
|
+
Navbar](#tramway-navbar)).
|
|
1790
|
+
|
|
1791
|
+
### SolidQueue
|
|
1792
|
+
|
|
1793
|
+
The `:solid_queue` plugin is a dashboard for inspecting and managing [SolidQueue](https://github.com/rails/solid_queue)
|
|
1794
|
+
jobs. It requires the `solid_queue` gem in your Gemfile — if it's missing, Tramway raises a clear error explaining
|
|
1795
|
+
how to fix it instead of failing deep inside a controller.
|
|
1796
|
+
|
|
1797
|
+
It gives you:
|
|
1798
|
+
|
|
1799
|
+
- A paginated, filterable jobs list (by status, queue, class name, or free-text search by class name/active job
|
|
1800
|
+
ID/numeric ID), with per-status counts.
|
|
1801
|
+
- A job detail page, including failure details (exception class, message, backtrace) for failed jobs.
|
|
1802
|
+
- Retry, discard and destroy for a single job, or in bulk for a selection of jobs — or "Destroy all" to destroy
|
|
1803
|
+
every job matching the currently active filters at once. Bulk actions and "Destroy all" run in the background
|
|
1804
|
+
(you'll see a flash message confirming the action was queued, not completed instantly) on their own dedicated
|
|
1805
|
+
worker queue, which `tramway:install` sets up for you.
|
|
1806
|
+
- A queues list with size/running/latency/throughput, and pause/resume/clear actions per queue. "Size" only counts
|
|
1807
|
+
jobs waiting to run — "Running" shows jobs currently executing on that queue, so a queue processing one big job
|
|
1808
|
+
(e.g. a large "Destroy all") doesn't look idle just because nothing is waiting behind it.
|
|
1809
|
+
- A recurring tasks list with the ability to enqueue a task immediately.
|
|
1810
|
+
- Jobs, queues, and recurring tasks lists (and the job detail page) refresh themselves in the background every few
|
|
1811
|
+
seconds, so job status changes show up without reloading the page.
|
|
1812
|
+
|
|
1813
|
+
See [docs/users/tramway_solid_queue_plugin.md](docs/users/tramway_solid_queue_plugin.md) for more on what "discard"
|
|
1814
|
+
does and other SolidQueue-specific behavior.
|
|
1815
|
+
|
|
1735
1816
|
## Lantern Color Palette
|
|
1736
1817
|
|
|
1737
1818
|
| Type | Color |
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
/* global console */
|
|
2
|
+
/* global console, fetch, setInterval, clearInterval, DOMParser */
|
|
3
3
|
|
|
4
4
|
class TramwaySelect extends Controller {
|
|
5
5
|
static targets = ["dropdown", "showSelectedArea", "hiddenInput", "caretDown", "caretUp"]
|
|
@@ -482,4 +482,142 @@ class Navbar extends Controller {
|
|
|
482
482
|
}
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
-
|
|
485
|
+
class Collapsible extends Controller {
|
|
486
|
+
static targets = ["panel", "icon", "toggle"]
|
|
487
|
+
|
|
488
|
+
connect() {
|
|
489
|
+
this.expanded = true
|
|
490
|
+
|
|
491
|
+
const stored = this.readStored()
|
|
492
|
+
const expanded = stored === null ? true : stored === 'true'
|
|
493
|
+
|
|
494
|
+
this.setExpanded(expanded, { animate: false })
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
toggle(event) {
|
|
498
|
+
event.preventDefault()
|
|
499
|
+
|
|
500
|
+
this.setExpanded(!this.expanded, { persist: true })
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
setExpanded(expanded, { persist = false, animate = true } = {}) {
|
|
504
|
+
this.expanded = expanded
|
|
505
|
+
|
|
506
|
+
if (animate) {
|
|
507
|
+
this.animateTo(expanded)
|
|
508
|
+
} else {
|
|
509
|
+
this.panelTarget.style.height = expanded ? 'auto' : '0px'
|
|
510
|
+
this.panelTarget.style.opacity = expanded ? '1' : '0'
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const expandedCarrier = this.hasToggleTarget ? this.toggleTarget : this.element
|
|
514
|
+
expandedCarrier.setAttribute('aria-expanded', expanded ? 'true' : 'false')
|
|
515
|
+
|
|
516
|
+
if (this.hasIconTarget) {
|
|
517
|
+
this.iconTarget.classList.toggle('rotate-180', !expanded)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (persist) {
|
|
521
|
+
this.writeStored(expanded)
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Animates between the panel's current rendered height and its target height, rather than
|
|
526
|
+
// to/from `auto` directly (which cannot be transitioned). `height` is one of the few
|
|
527
|
+
// properties animate.md tolerates outside transform/opacity, since an accordion has no
|
|
528
|
+
// transform equivalent.
|
|
529
|
+
animateTo(expanded) {
|
|
530
|
+
const panel = this.panelTarget
|
|
531
|
+
const targetHeight = expanded ? panel.scrollHeight : 0
|
|
532
|
+
|
|
533
|
+
panel.style.height = `${panel.getBoundingClientRect().height}px`
|
|
534
|
+
panel.getBoundingClientRect() // force reflow so the browser registers the start value
|
|
535
|
+
|
|
536
|
+
requestAnimationFrame(() => {
|
|
537
|
+
panel.style.height = `${targetHeight}px`
|
|
538
|
+
panel.style.opacity = expanded ? '1' : '0'
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
panel.addEventListener('transitionend', () => {
|
|
542
|
+
if (this.expanded) panel.style.height = 'auto'
|
|
543
|
+
}, { once: true })
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
readStored() {
|
|
547
|
+
try {
|
|
548
|
+
return window.localStorage.getItem(this.storageKey)
|
|
549
|
+
} catch {
|
|
550
|
+
return null
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
writeStored(expanded) {
|
|
555
|
+
try {
|
|
556
|
+
window.localStorage.setItem(this.storageKey, expanded ? 'true' : 'false')
|
|
557
|
+
} catch {
|
|
558
|
+
/* localStorage unavailable (e.g. private mode) — state simply won't persist */
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
get storageKey() {
|
|
563
|
+
return `tramway-collapsible-${this.element.dataset.key || 'default'}`
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Periodically re-fetches the current page and swaps the innerHTML of each marked
|
|
568
|
+
// "refreshable" target by id, so pages reflect background changes (e.g. job status moving
|
|
569
|
+
// through SolidQueue) without a full reload. A target is skipped while it has a checked
|
|
570
|
+
// checkbox or focus inside it, so an in-progress bulk selection or form entry isn't clobbered.
|
|
571
|
+
class AutoRefresh extends Controller {
|
|
572
|
+
static targets = ["refreshable"]
|
|
573
|
+
|
|
574
|
+
static values = {
|
|
575
|
+
interval: { type: Number, default: 4000 }
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
connect() {
|
|
579
|
+
this.refreshing = false
|
|
580
|
+
this.timer = setInterval(() => this.refresh(), this.intervalValue)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
disconnect() {
|
|
584
|
+
clearInterval(this.timer)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async refresh() {
|
|
588
|
+
if (this.refreshing || document.hidden) return
|
|
589
|
+
|
|
590
|
+
this.refreshing = true
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
const response = await fetch(window.location.href, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
|
594
|
+
|
|
595
|
+
if (!response.ok) return
|
|
596
|
+
|
|
597
|
+
const next = new DOMParser().parseFromString(await response.text(), 'text/html')
|
|
598
|
+
|
|
599
|
+
this.refreshableTargets.forEach((target) => this.updateTarget(target, next))
|
|
600
|
+
} finally {
|
|
601
|
+
this.refreshing = false
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
updateTarget(target, next) {
|
|
606
|
+
if (!target.id || this.hasPendingInput(target)) return
|
|
607
|
+
|
|
608
|
+
const replacement = next.getElementById(target.id)
|
|
609
|
+
|
|
610
|
+
if (replacement) {
|
|
611
|
+
target.innerHTML = replacement.innerHTML
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
hasPendingInput(target) {
|
|
616
|
+
const focused = target.contains(document.activeElement) &&
|
|
617
|
+
document.activeElement.matches('input, textarea, select')
|
|
618
|
+
|
|
619
|
+
return target.querySelector('input:checked') !== null || focused
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export { TramwaySelect, UiCheckbox, Tooltip, Navbar, Collapsible, AutoRefresh }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'tramway/warnings'
|
|
4
|
+
require 'tramway/errors'
|
|
4
5
|
|
|
5
6
|
module Tramway
|
|
6
7
|
# Main controller for entities pages
|
|
@@ -13,18 +14,15 @@ module Tramway
|
|
|
13
14
|
include Rails.application.routes.url_helpers
|
|
14
15
|
|
|
15
16
|
def index
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
else
|
|
19
|
-
model_class.order(id: :desc)
|
|
20
|
-
end => entities
|
|
21
|
-
|
|
17
|
+
entities = base_entities
|
|
18
|
+
entities = apply_filter(entities)
|
|
22
19
|
entities = preload(entities)
|
|
23
20
|
entities = search(entities)
|
|
24
21
|
entities = entities.page(params[:page])
|
|
25
22
|
@entities = entities
|
|
26
23
|
|
|
27
24
|
@namespace = entity.namespace
|
|
25
|
+
@filter_counts = filter_counts if filters.present?
|
|
28
26
|
end
|
|
29
27
|
|
|
30
28
|
def show
|
|
@@ -91,6 +89,37 @@ module Tramway
|
|
|
91
89
|
entity.page(:index).scope
|
|
92
90
|
end
|
|
93
91
|
|
|
92
|
+
def base_entities
|
|
93
|
+
if index_scope.present?
|
|
94
|
+
model_class.public_send(index_scope)
|
|
95
|
+
else
|
|
96
|
+
model_class.order(id: :desc)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def filters
|
|
101
|
+
entity.page(:index).filters
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def apply_filter(entities)
|
|
105
|
+
return entities if params[:filter].blank? || filters.blank?
|
|
106
|
+
|
|
107
|
+
filter = params[:filter].to_s
|
|
108
|
+
|
|
109
|
+
unless filters.map(&:to_s).include?(filter)
|
|
110
|
+
raise Tramway::Errors::InvalidFilterError.new(filter:, available_filters: filters, model_class:)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
entities.public_send(filter)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def filter_counts
|
|
117
|
+
# Count by :id explicitly: a bare `.count` reuses any custom `select` values from the
|
|
118
|
+
# index page's `scope` (e.g. a raw-SQL aliased subquery column), which breaks the
|
|
119
|
+
# generated SQL. Counting a real column side-steps that entirely.
|
|
120
|
+
filters.index_with { |filter| base_entities.public_send(filter).count(:id) }
|
|
121
|
+
end
|
|
122
|
+
|
|
94
123
|
def preload(entities)
|
|
95
124
|
includes = entity.page(:index).includes
|
|
96
125
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tramway/plugins'
|
|
4
|
+
|
|
5
|
+
module Tramway
|
|
6
|
+
module Plugins
|
|
7
|
+
module SolidQueue
|
|
8
|
+
# Base controller for the solid_queue plugin dashboard
|
|
9
|
+
class ApplicationController < Tramway.config.application_controller.constantize
|
|
10
|
+
prepend_view_path "#{Gem::Specification.find_by_name('tramway').gem_dir}/app/views"
|
|
11
|
+
|
|
12
|
+
layout 'tramway/layouts/application'
|
|
13
|
+
|
|
14
|
+
helper Tramway::ApplicationHelper
|
|
15
|
+
helper Tramway::Plugins::SolidQueue::JobsHelper
|
|
16
|
+
include Rails.application.routes.url_helpers
|
|
17
|
+
|
|
18
|
+
before_action :ensure_solid_queue_available!
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def ensure_solid_queue_available!
|
|
23
|
+
Tramway::Plugins.fetch(:solid_queue).ensure_available!
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tramway
|
|
4
|
+
module Plugins
|
|
5
|
+
module SolidQueue
|
|
6
|
+
# Lists, filters and manages SolidQueue jobs for the plugin dashboard
|
|
7
|
+
class JobsController < ApplicationController
|
|
8
|
+
def index
|
|
9
|
+
@jobs = jobs_filter.jobs.order(id: :desc).page(params[:page])
|
|
10
|
+
@status_counts = jobs_filter.status_counts
|
|
11
|
+
@queue_names = ::SolidQueue::Job.distinct.order(:queue_name).pluck(:queue_name)
|
|
12
|
+
@class_names = ::SolidQueue::Job.distinct.order(:class_name).pluck(:class_name)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def show
|
|
16
|
+
@job = job
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def destroy
|
|
20
|
+
job.destroy
|
|
21
|
+
|
|
22
|
+
redirect_to jobs_path, notice: t('tramway.plugins.solid_queue.notices.destroyed')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def retry
|
|
26
|
+
job.retry
|
|
27
|
+
|
|
28
|
+
redirect_to jobs_path, notice: t('tramway.plugins.solid_queue.notices.retried')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def discard
|
|
32
|
+
job.discard
|
|
33
|
+
|
|
34
|
+
redirect_to jobs_path, notice: t('tramway.plugins.solid_queue.notices.discarded')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def bulk_retry
|
|
38
|
+
Tramway::SolidQueue::BulkRetryJob.perform_later(selected_job_ids)
|
|
39
|
+
|
|
40
|
+
redirect_to jobs_path, notice: t('tramway.plugins.solid_queue.notices.bulk_retry_enqueued')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def bulk_discard
|
|
44
|
+
Tramway::SolidQueue::BulkDiscardJob.perform_later(selected_job_ids)
|
|
45
|
+
|
|
46
|
+
redirect_to jobs_path, notice: t('tramway.plugins.solid_queue.notices.bulk_discard_enqueued')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def bulk_destroy
|
|
50
|
+
Tramway::SolidQueue::BulkDestroyJob.perform_later(selected_job_ids)
|
|
51
|
+
|
|
52
|
+
redirect_to jobs_path, notice: t('tramway.plugins.solid_queue.notices.bulk_destroy_enqueued')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def destroy_all
|
|
56
|
+
Tramway::SolidQueue::DestroyAllJob.perform_later(filter_params)
|
|
57
|
+
|
|
58
|
+
redirect_to jobs_path(request.query_parameters.except('page')),
|
|
59
|
+
notice: t('tramway.plugins.solid_queue.notices.destroy_all_enqueued')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def job
|
|
65
|
+
@job ||= ::SolidQueue::Job.find(params.expect(:id))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def jobs_filter
|
|
69
|
+
@jobs_filter ||= Tramway::SolidQueue::JobsFilter.new(filter_params)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def filter_params
|
|
73
|
+
params.permit(:status, :queue_name, :class_name, :query).to_h
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def selected_job_ids
|
|
77
|
+
Array(params[:job_ids]).map(&:to_i)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tramway
|
|
4
|
+
module Plugins
|
|
5
|
+
module SolidQueue
|
|
6
|
+
# Lists SolidQueue queues and lets the user pause/resume/clear them
|
|
7
|
+
class QueuesController < ApplicationController
|
|
8
|
+
def index
|
|
9
|
+
@queues = ::SolidQueue::Queue.all.sort_by(&:name)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def pause
|
|
13
|
+
queue.pause
|
|
14
|
+
|
|
15
|
+
redirect_to queues_path, notice: t('tramway.plugins.solid_queue.notices.paused')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def resume
|
|
19
|
+
queue.resume
|
|
20
|
+
|
|
21
|
+
redirect_to queues_path, notice: t('tramway.plugins.solid_queue.notices.resumed')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def clear
|
|
25
|
+
queue.clear
|
|
26
|
+
|
|
27
|
+
redirect_to queues_path, notice: t('tramway.plugins.solid_queue.notices.cleared')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def queue
|
|
33
|
+
@queue ||= ::SolidQueue::Queue.find_by_name(params.expect(:name))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tramway
|
|
4
|
+
module Plugins
|
|
5
|
+
module SolidQueue
|
|
6
|
+
# Lists SolidQueue recurring tasks and lets the user enqueue one immediately
|
|
7
|
+
class RecurringTasksController < ApplicationController
|
|
8
|
+
def index
|
|
9
|
+
@recurring_tasks = ::SolidQueue::RecurringTask.order(:key)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def enqueue
|
|
13
|
+
recurring_task.enqueue(at: Time.current)
|
|
14
|
+
|
|
15
|
+
redirect_to recurring_tasks_path, notice: t('tramway.plugins.solid_queue.notices.enqueued')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def recurring_task
|
|
21
|
+
@recurring_task ||= ::SolidQueue::RecurringTask.find_by!(key: params.expect(:key))
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tramway
|
|
4
|
+
module Plugins
|
|
5
|
+
module SolidQueue
|
|
6
|
+
# View helpers for rendering SolidQueue job statuses in the plugin dashboard
|
|
7
|
+
module JobsHelper
|
|
8
|
+
STATUS_BADGE_TYPES = {
|
|
9
|
+
ready: :hope,
|
|
10
|
+
claimed: :love,
|
|
11
|
+
scheduled: :fear,
|
|
12
|
+
blocked: :warning,
|
|
13
|
+
failed: :rage,
|
|
14
|
+
finished: :will
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def solid_queue_status_badge_type(status)
|
|
18
|
+
STATUS_BADGE_TYPES.fetch(status&.to_sym, :default)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def solid_queue_status_label(status)
|
|
22
|
+
t("tramway.plugins.solid_queue.statuses.#{status}", default: status.to_s.humanize)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tramway
|
|
4
|
+
module Plugins
|
|
5
|
+
module SolidQueue
|
|
6
|
+
# View helpers for rendering SolidQueue queue throughput and running-job counts in the plugin dashboard
|
|
7
|
+
module QueuesHelper
|
|
8
|
+
THROUGHPUT_WINDOW = 5.minutes
|
|
9
|
+
|
|
10
|
+
def solid_queue_throughput_label(queue)
|
|
11
|
+
t('tramway.plugins.solid_queue.queues.throughput_value', default: '%<rate>s/min',
|
|
12
|
+
rate: solid_queue_throughput(queue))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def solid_queue_throughput(queue)
|
|
16
|
+
finished_count = ::SolidQueue::Job.where(queue_name: queue.name, finished_at: THROUGHPUT_WINDOW.ago..).count
|
|
17
|
+
|
|
18
|
+
(finished_count / THROUGHPUT_WINDOW.in_minutes).round(1)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def solid_queue_running_count(queue)
|
|
22
|
+
::SolidQueue::Job.where(queue_name: queue.name).joins(:claimed_execution).count
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tramway
|
|
4
|
+
module SolidQueue
|
|
5
|
+
# Base job for the plugin's bulk jobs actions (retry/discard/destroy), routed to their
|
|
6
|
+
# own dedicated worker queue so a large bulk action can't starve other application jobs.
|
|
7
|
+
# The `tramway:install` generator configures this queue with 20 worker threads.
|
|
8
|
+
class ApplicationJob < ActiveJob::Base
|
|
9
|
+
BATCH_SIZE = 1000
|
|
10
|
+
|
|
11
|
+
queue_as :tramway_solid_queue_bulk_actions
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tramway
|
|
4
|
+
module SolidQueue
|
|
5
|
+
# Destroys the given jobs, in batches so a large selection doesn't load/destroy
|
|
6
|
+
# everything in a single pass.
|
|
7
|
+
class BulkDestroyJob < ApplicationJob
|
|
8
|
+
def perform(job_ids)
|
|
9
|
+
::SolidQueue::Job.where(id: job_ids).in_batches(of: BATCH_SIZE, &:destroy_all)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tramway
|
|
4
|
+
module SolidQueue
|
|
5
|
+
# Discards the given jobs, in batches so a large selection doesn't load everything
|
|
6
|
+
# in a single pass. `discard` is a per-record SolidQueue operation, not a bulk SQL one.
|
|
7
|
+
class BulkDiscardJob < ApplicationJob
|
|
8
|
+
def perform(job_ids)
|
|
9
|
+
::SolidQueue::Job.where(id: job_ids).find_each(batch_size: BATCH_SIZE, &:discard)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tramway
|
|
4
|
+
module SolidQueue
|
|
5
|
+
# Retries the given jobs, in batches so a large selection doesn't load everything
|
|
6
|
+
# in a single pass. `retry` is a per-record SolidQueue operation, not a bulk SQL one.
|
|
7
|
+
class BulkRetryJob < ApplicationJob
|
|
8
|
+
def perform(job_ids)
|
|
9
|
+
::SolidQueue::Job.where(id: job_ids).find_each(batch_size: BATCH_SIZE, &:retry)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tramway
|
|
4
|
+
module SolidQueue
|
|
5
|
+
# Destroys every job matching the given filters, in batches so a large match set
|
|
6
|
+
# doesn't load/destroy everything in a single pass.
|
|
7
|
+
class DestroyAllJob < ApplicationJob
|
|
8
|
+
def perform(filters = {})
|
|
9
|
+
Tramway::SolidQueue::JobsFilter.new(filters).jobs.in_batches(of: BATCH_SIZE, &:destroy_all)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|