tramway 4.0.2 → 4.1.0.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +59 -3
  3. data/app/assets/javascripts/tramway/tramway.js +76 -5
  4. data/app/controllers/tramway/plugins/solid_queue/application_controller.rb +28 -0
  5. data/app/controllers/tramway/plugins/solid_queue/jobs_controller.rb +82 -0
  6. data/app/controllers/tramway/plugins/solid_queue/queues_controller.rb +38 -0
  7. data/app/controllers/tramway/plugins/solid_queue/recurring_tasks_controller.rb +26 -0
  8. data/app/helpers/tramway/plugins/solid_queue/jobs_helper.rb +27 -0
  9. data/app/helpers/tramway/plugins/solid_queue/queues_helper.rb +27 -0
  10. data/app/jobs/tramway/solid_queue/application_job.rb +14 -0
  11. data/app/jobs/tramway/solid_queue/bulk_destroy_job.rb +13 -0
  12. data/app/jobs/tramway/solid_queue/bulk_discard_job.rb +13 -0
  13. data/app/jobs/tramway/solid_queue/bulk_retry_job.rb +13 -0
  14. data/app/jobs/tramway/solid_queue/destroy_all_job.rb +13 -0
  15. data/app/models/tramway/solid_queue/jobs_filter.rb +53 -0
  16. data/app/views/tramway/plugins/solid_queue/_subnav.html.haml +7 -0
  17. data/app/views/tramway/plugins/solid_queue/jobs/index.html.haml +89 -0
  18. data/app/views/tramway/plugins/solid_queue/jobs/show.html.haml +90 -0
  19. data/app/views/tramway/plugins/solid_queue/queues/index.html.haml +46 -0
  20. data/app/views/tramway/plugins/solid_queue/recurring_tasks/index.html.haml +33 -0
  21. data/config/locales/en.yml +62 -0
  22. data/config/routes.rb +10 -0
  23. data/config/tailwind.config.js +1 -0
  24. data/lib/generators/tramway/install/install_generator.rb +12 -0
  25. data/lib/generators/tramway/install/solid_queue_helpers.rb +54 -0
  26. data/lib/tramway/config.rb +7 -1
  27. data/lib/tramway/configs/plugins.rb +66 -0
  28. data/lib/tramway/errors.rb +36 -0
  29. data/lib/tramway/navbar.rb +23 -0
  30. data/lib/tramway/plugins/base.rb +33 -0
  31. data/lib/tramway/plugins/solid_queue/config.rb +13 -0
  32. data/lib/tramway/plugins/solid_queue/plugin.rb +37 -0
  33. data/lib/tramway/plugins/solid_queue/routes.rb +50 -0
  34. data/lib/tramway/plugins/solid_queue.rb +7 -0
  35. data/lib/tramway/plugins.rb +37 -0
  36. data/lib/tramway/version.rb +1 -1
  37. metadata +26 -1
@@ -0,0 +1,90 @@
1
+ = tramway_container do
2
+ .flex.justify-between.items-center
3
+ = tramway_title do
4
+ = "#{@job.class_name} ##{@job.id}"
5
+
6
+ #tramway-solid-queue-job-details{ data: { controller: 'tramway--auto-refresh',
7
+ 'tramway--auto-refresh-target': 'refreshable' } }
8
+ = component 'tramway/actions_buttons_container' do
9
+ = tramway_button tag: :a, text: t('tramway.actions.cancel'), path: jobs_path, type: :inverse
10
+
11
+ - if @job.status == :failed
12
+ = tramway_button text: t('tramway.actions.retry'), path: retry_job_path(@job.id), method: :post,
13
+ type: :will
14
+
15
+ - if @job.status != :finished
16
+ = tramway_button text: t('tramway.actions.discard'), path: discard_job_path(@job.id), method: :post,
17
+ type: :warning
18
+
19
+ = tramway_button text: t('tramway.actions.destroy'), path: job_path(@job.id), method: :delete, type: :rage
20
+
21
+ = tramway_table class: 'mt-4' do
22
+ = tramway_row do
23
+ = tramway_cell do
24
+ = t('tramway.plugins.solid_queue.jobs.status', default: 'Status')
25
+ = tramway_cell do
26
+ = tramway_badge text: solid_queue_status_label(@job.status),
27
+ type: solid_queue_status_badge_type(@job.status)
28
+
29
+ = tramway_row do
30
+ = tramway_cell do
31
+ = t('tramway.plugins.solid_queue.jobs.queue_name', default: 'Queue')
32
+ = tramway_cell do
33
+ = @job.queue_name
34
+
35
+ = tramway_row do
36
+ = tramway_cell do
37
+ = t('tramway.plugins.solid_queue.jobs.active_job_id', default: 'Active Job ID')
38
+ = tramway_cell do
39
+ = @job.active_job_id
40
+
41
+ = tramway_row do
42
+ = tramway_cell do
43
+ = t('tramway.plugins.solid_queue.jobs.priority', default: 'Priority')
44
+ = tramway_cell do
45
+ = @job.priority
46
+
47
+ = tramway_row do
48
+ = tramway_cell do
49
+ = t('tramway.plugins.solid_queue.jobs.concurrency_key', default: 'Concurrency key')
50
+ = tramway_cell do
51
+ = @job.concurrency_key
52
+
53
+ = tramway_row do
54
+ = tramway_cell do
55
+ = t('tramway.plugins.solid_queue.jobs.scheduled_at', default: 'Scheduled at')
56
+ = tramway_cell do
57
+ = @job.scheduled_at
58
+
59
+ = tramway_row do
60
+ = tramway_cell do
61
+ = t('tramway.plugins.solid_queue.jobs.finished_at', default: 'Finished at')
62
+ = tramway_cell do
63
+ = @job.finished_at
64
+
65
+ = tramway_row do
66
+ = tramway_cell do
67
+ = t('tramway.plugins.solid_queue.jobs.arguments', default: 'Arguments')
68
+ = tramway_cell do
69
+ = @job.arguments.inspect
70
+
71
+ - if @job.status == :failed
72
+ .mt-8
73
+ %h3.text-2xl.font-bold.text-white
74
+ = t('tramway.plugins.solid_queue.jobs.error', default: 'Error')
75
+
76
+ = tramway_table class: 'mt-4' do
77
+ = tramway_row do
78
+ = tramway_cell do
79
+ = t('tramway.plugins.solid_queue.jobs.exception_class', default: 'Exception class')
80
+ = tramway_cell do
81
+ = @job.failed_execution.exception_class
82
+
83
+ = tramway_row do
84
+ = tramway_cell do
85
+ = t('tramway.plugins.solid_queue.jobs.message', default: 'Message')
86
+ = tramway_cell do
87
+ = @job.failed_execution.message
88
+
89
+ %pre.mt-4.overflow-x-auto.text-sm.text-zinc-400
90
+ = Array(@job.failed_execution.backtrace).join("\n")
@@ -0,0 +1,46 @@
1
+ - content_for :title, t('tramway.plugins.solid_queue.queues.title', default: 'Queues')
2
+
3
+ = tramway_container do
4
+ .flex.justify-between.items-center
5
+ = tramway_title do
6
+ = content_for :title
7
+
8
+ = render 'tramway/plugins/solid_queue/subnav'
9
+
10
+ %div#tramway-solid-queue-queues-table{ data: { controller: 'tramway--auto-refresh',
11
+ 'tramway--auto-refresh-target': 'refreshable' } }
12
+ = tramway_table class: 'mt-4' do
13
+ = tramway_header headers: [t('tramway.plugins.solid_queue.queues.name', default: 'Name'),
14
+ t('tramway.plugins.solid_queue.queues.size', default: 'Size'),
15
+ t('tramway.plugins.solid_queue.queues.running', default: 'Running'),
16
+ t('tramway.plugins.solid_queue.queues.latency', default: 'Latency'),
17
+ t('tramway.plugins.solid_queue.queues.throughput', default: 'Throughput'),
18
+ t('tramway.plugins.solid_queue.queues.status', default: 'Status'),
19
+ t('tramway.table.actions_header')]
20
+ - @queues.each do |queue|
21
+ = tramway_row do
22
+ = tramway_cell do
23
+ = queue.name
24
+ = tramway_cell do
25
+ = queue.size
26
+ = tramway_cell do
27
+ = solid_queue_running_count(queue)
28
+ = tramway_cell do
29
+ = queue.human_latency
30
+ = tramway_cell do
31
+ = solid_queue_throughput_label(queue)
32
+ = tramway_cell do
33
+ - if queue.paused?
34
+ = tramway_badge text: t('tramway.plugins.solid_queue.queues.paused', default: 'Paused'), type: :warning
35
+ - else
36
+ = tramway_badge text: t('tramway.plugins.solid_queue.queues.active', default: 'Active'), type: :will
37
+ = tramway_cell do
38
+ .flex.gap-2
39
+ - if queue.paused?
40
+ = tramway_button text: t('tramway.actions.resume'), path: resume_queue_path(queue.name),
41
+ method: :post, type: :will, size: :small
42
+ - else
43
+ = tramway_button text: t('tramway.actions.pause'), path: pause_queue_path(queue.name),
44
+ method: :post, type: :warning, size: :small
45
+ = tramway_button text: t('tramway.actions.clear'), path: clear_queue_path(queue.name),
46
+ method: :post, type: :rage, size: :small
@@ -0,0 +1,33 @@
1
+ - content_for :title, t('tramway.plugins.solid_queue.recurring_tasks.title', default: 'Recurring Tasks')
2
+
3
+ = tramway_container do
4
+ .flex.justify-between.items-center
5
+ = tramway_title do
6
+ = content_for :title
7
+
8
+ = render 'tramway/plugins/solid_queue/subnav'
9
+
10
+ %div#tramway-solid-queue-recurring-tasks-table{ data: { controller: 'tramway--auto-refresh',
11
+ 'tramway--auto-refresh-target': 'refreshable' } }
12
+ = tramway_table class: 'mt-4' do
13
+ = tramway_header headers: [t('tramway.plugins.solid_queue.recurring_tasks.key', default: 'Key'),
14
+ t('tramway.plugins.solid_queue.recurring_tasks.schedule', default: 'Schedule'),
15
+ t('tramway.plugins.solid_queue.recurring_tasks.class_name', default: 'Class'),
16
+ t('tramway.plugins.solid_queue.recurring_tasks.queue_name', default: 'Queue'),
17
+ t('tramway.plugins.solid_queue.recurring_tasks.next_time', default: 'Next run'),
18
+ t('tramway.table.actions_header')]
19
+ - @recurring_tasks.each do |task|
20
+ = tramway_row do
21
+ = tramway_cell do
22
+ = task.key
23
+ = tramway_cell do
24
+ = task.schedule
25
+ = tramway_cell do
26
+ = task.class_name
27
+ = tramway_cell do
28
+ = task.queue_name
29
+ = tramway_cell do
30
+ = task.next_time
31
+ = tramway_cell do
32
+ = tramway_button text: t('tramway.actions.enqueue_now'), path: enqueue_recurring_task_path(task.key),
33
+ method: :post, type: :will, size: :small
@@ -11,11 +11,18 @@ en:
11
11
  new: "New"
12
12
  edit: "Edit"
13
13
  destroy: "Destroy"
14
+ destroy_all: "Destroy all"
14
15
  save: "Save"
15
16
  cancel: "Cancel"
16
17
  search: "Search"
17
18
  filters: "Filters"
18
19
  clear_filter: "Clear filter"
20
+ retry: "Retry"
21
+ discard: "Discard"
22
+ pause: "Pause"
23
+ resume: "Resume"
24
+ clear: "Clear"
25
+ enqueue_now: "Run now"
19
26
  notices:
20
27
  created: "The record is created"
21
28
  updated: "The record is updated"
@@ -24,3 +31,58 @@ en:
24
31
  placeholders:
25
32
  waiting: "Waiting for an answer..."
26
33
  type: "Type your message..."
34
+ plugins:
35
+ solid_queue:
36
+ statuses:
37
+ ready: "Ready"
38
+ claimed: "Claimed"
39
+ scheduled: "Scheduled"
40
+ blocked: "Blocked"
41
+ failed: "Failed"
42
+ finished: "Finished"
43
+ notices:
44
+ destroyed: "The job is destroyed"
45
+ retried: "The job is scheduled to retry"
46
+ discarded: "The job is discarded"
47
+ paused: "The queue is paused"
48
+ resumed: "The queue is resumed"
49
+ cleared: "The queue is cleared"
50
+ enqueued: "The recurring task is enqueued"
51
+ bulk_retry_enqueued: "Retrying the selected jobs in the background"
52
+ bulk_discard_enqueued: "Discarding the selected jobs in the background"
53
+ bulk_destroy_enqueued: "Destroying the selected jobs in the background"
54
+ destroy_all_enqueued: "Destroying all matching jobs in the background"
55
+ jobs:
56
+ title: "Background Jobs"
57
+ class_name: "Class"
58
+ queue_name: "Queue"
59
+ status: "Status"
60
+ scheduled_at: "Scheduled at"
61
+ finished_at: "Finished at"
62
+ active_job_id: "Active Job ID"
63
+ priority: "Priority"
64
+ concurrency_key: "Concurrency key"
65
+ arguments: "Arguments"
66
+ error: "Error"
67
+ exception_class: "Exception class"
68
+ message: "Message"
69
+ all_queues: "All queues"
70
+ all_classes: "All classes"
71
+ queues:
72
+ title: "Queues"
73
+ name: "Name"
74
+ size: "Size"
75
+ running: "Running"
76
+ latency: "Latency"
77
+ throughput: "Throughput"
78
+ throughput_value: "%<rate>s/min"
79
+ status: "Status"
80
+ paused: "Paused"
81
+ active: "Active"
82
+ recurring_tasks:
83
+ title: "Recurring Tasks"
84
+ key: "Key"
85
+ schedule: "Schedule"
86
+ class_name: "Class"
87
+ queue_name: "Queue"
88
+ next_time: "Next run"
data/config/routes.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'tramway/helpers/routes_helper'
4
+ require 'tramway/plugins'
4
5
 
5
6
  # rubocop:disable-next Metrics/BlockLength
6
7
  Tramway::Engine.routes.draw do
@@ -54,6 +55,15 @@ Tramway::Engine.routes.draw do
54
55
  nest.call(segments)
55
56
  end
56
57
  end
58
+
59
+ Tramway.config.plugins.each do |plugin_name|
60
+ plugin = Tramway::Plugins.fetch(plugin_name)
61
+ plugin_config = Tramway.config.plugins.public_send(plugin_name)
62
+
63
+ scope path: plugin_config.path do
64
+ plugin.draw_routes(self, plugin_config)
65
+ end
66
+ end
57
67
  end
58
68
 
59
69
  Tramway::Engine.routes.routes.map(&:name).compact.each do |route_name|
@@ -282,6 +282,7 @@ module.exports = {
282
282
  // === Visibility and typography helpers ===
283
283
  'hidden',
284
284
  'text-xl',
285
+ 'text-2xl',
285
286
  'font-bold',
286
287
  'prose',
287
288
  'prose-invert',
@@ -4,6 +4,7 @@ require 'rails/generators'
4
4
  require 'fileutils'
5
5
  require_relative 'font_awesome_helpers'
6
6
  require_relative 'layout_helpers'
7
+ require_relative 'solid_queue_helpers'
7
8
 
8
9
  module Tramway
9
10
  module Generators
@@ -345,6 +346,7 @@ module Tramway
345
346
  include InstallGeneratorHelpers
346
347
  include InstallGeneratorFontAwesomeHelpers
347
348
  include InstallGeneratorLayoutHelpers
349
+ include InstallGeneratorSolidQueueHelpers
348
350
 
349
351
  desc 'Installs Tramway dependencies and Tailwind safelist configuration.'
350
352
 
@@ -442,6 +444,16 @@ module Tramway
442
444
  def ensure_navbar_sidebar_offset_in_application_layout
443
445
  ensure_navbar_sidebar_offset
444
446
  end
447
+
448
+ def ensure_solid_queue_bulk_actions_queue
449
+ return unless File.exist?(queue_yml_path)
450
+
451
+ content = File.read(queue_yml_path)
452
+ return if content.include?(TRAMWAY_SOLID_QUEUE_BULK_ACTIONS_QUEUE)
453
+
454
+ updated = insert_tramway_solid_queue_worker(content)
455
+ File.write(queue_yml_path, updated) if updated != content
456
+ end
445
457
  end
446
458
  end
447
459
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tramway
4
+ module Generators
5
+ # nodoc
6
+ module InstallGeneratorSolidQueueHelpers
7
+ TRAMWAY_SOLID_QUEUE_BULK_ACTIONS_QUEUE = 'tramway_solid_queue_bulk_actions'
8
+ TRAMWAY_SOLID_QUEUE_BULK_ACTIONS_CONCURRENCY = 20
9
+
10
+ private
11
+
12
+ def queue_yml_path
13
+ @queue_yml_path ||= File.join(destination_root, 'config/queue.yml')
14
+ end
15
+
16
+ def insert_tramway_solid_queue_worker(content)
17
+ insertion_index, item_indent = workers_list_insertion_point(content)
18
+ return content unless insertion_index
19
+
20
+ content.dup.insert(insertion_index, tramway_solid_queue_worker_entry(item_indent))
21
+ end
22
+
23
+ # Finds where the `workers:` YAML list ends, so a new worker entry can be appended
24
+ # right after the existing ones, matching their indentation. This is line-based
25
+ # (rather than a full YAML parse) so ERB tags, comments, and anchors/aliases already
26
+ # present in the host app's queue.yml are left untouched.
27
+ def workers_list_insertion_point(content)
28
+ lines = content.each_line.to_a
29
+ workers_line_index = lines.find_index { |line| line.match?(/^[ \t]*workers:[ \t]*\n?\z/) }
30
+ return [nil, nil] unless workers_line_index
31
+
32
+ item_indent = lines[workers_line_index + 1]&.[](/^[ \t]*(?=- )/)
33
+ return [nil, nil] unless item_indent
34
+
35
+ search_range = (workers_line_index + 1)...lines.length
36
+ end_index = search_range.find { |index| !lines[index].start_with?(item_indent) }
37
+ end_index ||= lines.length
38
+
39
+ [lines[0...end_index].join.length, item_indent]
40
+ end
41
+
42
+ def tramway_solid_queue_worker_entry(indentation)
43
+ sub_indentation = "#{indentation} "
44
+
45
+ <<~YAML
46
+ #{indentation}- queues: #{TRAMWAY_SOLID_QUEUE_BULK_ACTIONS_QUEUE}
47
+ #{sub_indentation}threads: #{TRAMWAY_SOLID_QUEUE_BULK_ACTIONS_CONCURRENCY}
48
+ #{sub_indentation}processes: 1
49
+ #{sub_indentation}polling_interval: 1
50
+ YAML
51
+ end
52
+ end
53
+ end
54
+ end
@@ -3,6 +3,7 @@
3
3
  require 'anyway'
4
4
  require 'singleton'
5
5
  require 'tramway/configs/entity'
6
+ require 'tramway/configs/plugins'
6
7
 
7
8
  module Tramway
8
9
  # Basic configuration of Tramway
@@ -13,7 +14,8 @@ module Tramway
13
14
  attr_config(
14
15
  entities: [],
15
16
  application_controller: 'ActionController::Base',
16
- theme: :classic
17
+ theme: :classic,
18
+ plugins: []
17
19
  )
18
20
 
19
21
  def entities=(collection)
@@ -23,5 +25,9 @@ module Tramway
23
25
  Tramway::Configs::Entity.new(**entity_options)
24
26
  end)
25
27
  end
28
+
29
+ def plugins=(names)
30
+ super(Tramway::Configs::Plugins.new(names))
31
+ end
26
32
  end
27
33
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tramway/plugins'
4
+
5
+ module Tramway
6
+ module Configs
7
+ # Value object stored at `Tramway.config.plugins`. It behaves like an array of
8
+ # enabled plugin names (`config.plugins = [:solid_queue]`, `config.plugins.include?`,
9
+ # `config.plugins.each`) while also exposing per-plugin configuration as attributes
10
+ # (`config.plugins.solid_queue = { path: '/jobs' }`, `config.plugins.solid_queue`).
11
+ class Plugins
12
+ include Enumerable
13
+
14
+ def initialize(names = [])
15
+ @names = Array(names).map(&:to_sym)
16
+ @configs = {}
17
+ end
18
+
19
+ def each(&block)
20
+ return to_enum(:each) unless block
21
+
22
+ @names.each(&block)
23
+ end
24
+
25
+ def include?(name)
26
+ @names.include?(name.to_sym)
27
+ end
28
+ alias enabled? include?
29
+
30
+ def to_a
31
+ @names.dup
32
+ end
33
+
34
+ def method_missing(method_name, *args)
35
+ plugin_name = plugin_name_from(method_name)
36
+
37
+ return super unless plugin_name
38
+
39
+ assignment?(method_name) ? assign_plugin_config(plugin_name, args.first) : plugin_config(plugin_name)
40
+ end
41
+
42
+ def respond_to_missing?(method_name, include_private = false)
43
+ plugin_name_from(method_name).present? || super
44
+ end
45
+
46
+ private
47
+
48
+ def plugin_name_from(method_name)
49
+ name = method_name.to_s.delete_suffix('=').to_sym
50
+ name if Tramway::Plugins.registered?(name)
51
+ end
52
+
53
+ def assignment?(method_name)
54
+ method_name.to_s.end_with?('=')
55
+ end
56
+
57
+ def plugin_config(name)
58
+ @configs[name] ||= Tramway::Plugins.fetch(name).config_class.new
59
+ end
60
+
61
+ def assign_plugin_config(name, options)
62
+ @configs[name] = Tramway::Plugins.fetch(name).config_class.new(**(options || {}).symbolize_keys)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -61,5 +61,41 @@ module Tramway
61
61
  MESSAGE
62
62
  end
63
63
  end
64
+
65
+ # Raised when `Tramway.config.plugins` references a plugin name that was never registered
66
+ # via `Tramway::Plugins.register`, so the developer gets a clear explanation instead of a
67
+ # raw NoMethodError/KeyError deep inside the plugin config collection.
68
+ class UnknownPluginError < StandardError
69
+ def initialize(name:, available:)
70
+ super(<<~MESSAGE)
71
+ Tramway received an unknown plugin "#{name}".
72
+
73
+ Available plugins: #{available.any? ? available.join(', ') : '(none registered)'}.
74
+
75
+ To fix this:
76
+ - use one of the available plugins above in `Tramway.config.plugins`, or
77
+ - register a custom plugin via `Tramway::Plugins.register(YourPluginClass)` before \
78
+ referencing it in the config.
79
+ MESSAGE
80
+ end
81
+ end
82
+
83
+ # Raised when a Tramway plugin depends on a gem that is not installed in the host
84
+ # application, so the developer gets a precise, actionable fix instead of a raw
85
+ # LoadError/NameError deep inside the plugin's controllers.
86
+ class MissingPluginDependencyError < StandardError
87
+ def initialize(plugin:, gem_name:, original_error:)
88
+ super(<<~MESSAGE)
89
+ The Tramway "#{plugin}" plugin requires the `#{gem_name}` gem, but it could not be \
90
+ loaded (#{original_error.class}: #{original_error.message}).
91
+
92
+ To fix this:
93
+ - add `gem "#{gem_name}"` to your Gemfile and run `bundle install`, then restart \
94
+ your server, or
95
+ - remove `:#{plugin}` from `Tramway.config.plugins` if you do not want to use this \
96
+ plugin.
97
+ MESSAGE
98
+ end
99
+ end
64
100
  end
65
101
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'tramway/plugins'
4
+
3
5
  module Tramway
4
6
  # Navbar object provides left and right elements
5
7
  class Navbar
@@ -10,6 +12,8 @@ module Tramway
10
12
  @items = { left: [], right: [] }
11
13
  @filling = nil
12
14
 
15
+ preset_plugins
16
+
13
17
  return unless with_entities
14
18
 
15
19
  entities = Tramway.config.entities
@@ -61,6 +65,25 @@ module Tramway
61
65
  reset_filling
62
66
  end
63
67
 
68
+ def preset_plugins
69
+ plugins = Tramway.config.plugins
70
+
71
+ return unless plugins.any?
72
+
73
+ filling_side :left
74
+
75
+ plugins.each do |name|
76
+ plugin = Tramway::Plugins.fetch(name)
77
+ nav_item = plugin.nav_item(plugins.public_send(name))
78
+
79
+ next if nav_item.blank?
80
+
81
+ item nav_item[:text], nav_item[:path], icon: nav_item[:icon]
82
+ end
83
+
84
+ reset_filling
85
+ end
86
+
64
87
  def provided_url_and_block?(url)
65
88
  url.present? && block_given?
66
89
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tramway
4
+ module Plugins
5
+ # Interface every Tramway plugin must implement to register itself with
6
+ # `Tramway::Plugins.register` and be referenced from `Tramway.config.plugins`.
7
+ class Base
8
+ class << self
9
+ def plugin_name
10
+ raise NotImplementedError, "#{self} must implement .plugin_name"
11
+ end
12
+
13
+ def config_class
14
+ raise NotImplementedError, "#{self} must implement .config_class"
15
+ end
16
+
17
+ def draw_routes(_mapper, _config)
18
+ raise NotImplementedError, "#{self} must implement .draw_routes"
19
+ end
20
+
21
+ # Returns a Hash with :text, :path and :icon keys used to render a navbar entry
22
+ # for this plugin, or nil to not render one.
23
+ def nav_item(_config)
24
+ nil
25
+ end
26
+
27
+ # Raises a clear, actionable error when the plugin's required infrastructure
28
+ # (typically a gem) is missing. No-op by default.
29
+ def ensure_available!; end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tramway
4
+ module Plugins
5
+ module SolidQueue
6
+ # Per-plugin configuration for the `:solid_queue` plugin, set via
7
+ # `Tramway.config.plugins.solid_queue = { path: '/jobs' }`.
8
+ class Config < Dry::Struct
9
+ attribute? :path, Types::Coercible::String.default('/jobs')
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tramway
4
+ module Plugins
5
+ module SolidQueue
6
+ # Registry entry for the `:solid_queue` plugin — a Tramway-based dashboard for
7
+ # SolidQueue jobs, queues and recurring tasks.
8
+ class Plugin < Tramway::Plugins::Base
9
+ class << self
10
+ def plugin_name
11
+ :solid_queue
12
+ end
13
+
14
+ def config_class
15
+ Tramway::Plugins::SolidQueue::Config
16
+ end
17
+
18
+ def draw_routes(mapper, config)
19
+ Tramway::Plugins::SolidQueue::Routes.draw(mapper, config)
20
+ end
21
+
22
+ def nav_item(config)
23
+ { text: 'Background Jobs', path: config.path, icon: 'fa-tasks' }
24
+ end
25
+
26
+ def ensure_available!
27
+ require 'solid_queue'
28
+ rescue LoadError => e
29
+ raise Tramway::Errors::MissingPluginDependencyError.new(
30
+ plugin: plugin_name, gem_name: 'solid_queue', original_error: e
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tramway
4
+ module Plugins
5
+ module SolidQueue
6
+ # Draws the routes for the solid_queue dashboard plugin, mounted at the
7
+ # plugin's configured `path` (defaults to `/jobs`).
8
+ module Routes
9
+ module_function
10
+
11
+ def draw(mapper, _config)
12
+ draw_queues(mapper)
13
+ draw_recurring_tasks(mapper)
14
+ draw_jobs(mapper)
15
+ end
16
+
17
+ def draw_queues(mapper)
18
+ mapper.instance_exec do
19
+ resources :queues, only: %i[index], param: :name, module: 'plugins/solid_queue' do
20
+ post :pause, on: :member
21
+ post :resume, on: :member
22
+ post :clear, on: :member
23
+ end
24
+ end
25
+ end
26
+
27
+ def draw_recurring_tasks(mapper)
28
+ mapper.instance_exec do
29
+ resources :recurring_tasks, only: %i[index], param: :key, module: 'plugins/solid_queue' do
30
+ post :enqueue, on: :member
31
+ end
32
+ end
33
+ end
34
+
35
+ def draw_jobs(mapper)
36
+ mapper.instance_exec do
37
+ resources :jobs, path: '', only: %i[index show destroy], module: 'plugins/solid_queue' do
38
+ post :retry, on: :member
39
+ post :discard, on: :member
40
+ post :bulk_retry, on: :collection
41
+ post :bulk_discard, on: :collection
42
+ post :bulk_destroy, on: :collection
43
+ post :destroy_all, on: :collection
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end