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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +81 -0
  3. data/app/assets/javascripts/tramway/tramway.js +140 -2
  4. data/app/controllers/tramway/entities_controller.rb +35 -6
  5. data/app/controllers/tramway/plugins/solid_queue/application_controller.rb +28 -0
  6. data/app/controllers/tramway/plugins/solid_queue/jobs_controller.rb +82 -0
  7. data/app/controllers/tramway/plugins/solid_queue/queues_controller.rb +38 -0
  8. data/app/controllers/tramway/plugins/solid_queue/recurring_tasks_controller.rb +26 -0
  9. data/app/helpers/tramway/plugins/solid_queue/jobs_helper.rb +27 -0
  10. data/app/helpers/tramway/plugins/solid_queue/queues_helper.rb +27 -0
  11. data/app/jobs/tramway/solid_queue/application_job.rb +14 -0
  12. data/app/jobs/tramway/solid_queue/bulk_destroy_job.rb +13 -0
  13. data/app/jobs/tramway/solid_queue/bulk_discard_job.rb +13 -0
  14. data/app/jobs/tramway/solid_queue/bulk_retry_job.rb +13 -0
  15. data/app/jobs/tramway/solid_queue/destroy_all_job.rb +13 -0
  16. data/app/models/tramway/solid_queue/jobs_filter.rb +53 -0
  17. data/app/views/tramway/entities/_filters.html.haml +15 -0
  18. data/app/views/tramway/entities/_list.html.haml +22 -8
  19. data/app/views/tramway/plugins/solid_queue/_subnav.html.haml +7 -0
  20. data/app/views/tramway/plugins/solid_queue/jobs/index.html.haml +89 -0
  21. data/app/views/tramway/plugins/solid_queue/jobs/show.html.haml +90 -0
  22. data/app/views/tramway/plugins/solid_queue/queues/index.html.haml +46 -0
  23. data/app/views/tramway/plugins/solid_queue/recurring_tasks/index.html.haml +33 -0
  24. data/config/locales/en.yml +64 -0
  25. data/config/routes.rb +10 -0
  26. data/config/tailwind.config.js +4 -0
  27. data/lib/generators/tramway/install/install_generator.rb +17 -3
  28. data/lib/generators/tramway/install/solid_queue_helpers.rb +54 -0
  29. data/lib/tramway/config.rb +7 -1
  30. data/lib/tramway/configs/entities/page.rb +1 -0
  31. data/lib/tramway/configs/plugins.rb +66 -0
  32. data/lib/tramway/errors.rb +56 -0
  33. data/lib/tramway/navbar.rb +23 -0
  34. data/lib/tramway/plugins/base.rb +33 -0
  35. data/lib/tramway/plugins/solid_queue/config.rb +13 -0
  36. data/lib/tramway/plugins/solid_queue/plugin.rb +37 -0
  37. data/lib/tramway/plugins/solid_queue/routes.rb +50 -0
  38. data/lib/tramway/plugins/solid_queue.rb +7 -0
  39. data/lib/tramway/plugins.rb +37 -0
  40. data/lib/tramway/version.rb +1 -1
  41. metadata +27 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d8281006bde6f8928a12ffc94cef931ff7b3c4e8bdc774ad4e690b7a529a0e8
4
- data.tar.gz: 644f44899bc6919f0ea8f08fbb9708fa4fec87b54a398668714209f905a54d87
3
+ metadata.gz: a20badcbb5b3d0d460bbadafb95816d4932ce5e8af0d41a9ce2315507220bed0
4
+ data.tar.gz: 800c9136a41a012e3b8581611afa70e666f81c4dab9d331cc7c094d096cb2b86
5
5
  SHA512:
6
- metadata.gz: 506f5ed78da723c554832b299ef0f0415f63b328fa9e4ff72e6f52b4972d699398a26b5101f869563eb91ce786cd244fd977cbab9f634d7bfdbbab7cef96dcff
7
- data.tar.gz: f68ae4382f2301f34ac572d725d41b0503f9ff5b6d2396e97a3afc95053d2c66e1f5818f2d9a0a6808f968e71ab386e3c789cbf9138f2616d0ee72f2af3c9104
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
- export { TramwaySelect, UiCheckbox, Tooltip, Navbar }
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
- if index_scope.present?
17
- model_class.public_send(index_scope)
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