tramway 4.0.2 → 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 +47 -0
- data/app/assets/javascripts/tramway/tramway.js +58 -2
- 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/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 +62 -0
- data/config/routes.rb +10 -0
- data/config/tailwind.config.js +1 -0
- data/lib/generators/tramway/install/install_generator.rb +12 -0
- data/lib/generators/tramway/install/solid_queue_helpers.rb +54 -0
- data/lib/tramway/config.rb +7 -1
- data/lib/tramway/configs/plugins.rb +66 -0
- data/lib/tramway/errors.rb +36 -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 +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
|
data/config/locales/en.yml
CHANGED
|
@@ -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|
|
data/config/tailwind.config.js
CHANGED
|
@@ -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
|
data/lib/tramway/config.rb
CHANGED
|
@@ -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
|
data/lib/tramway/errors.rb
CHANGED
|
@@ -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
|
data/lib/tramway/navbar.rb
CHANGED
|
@@ -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
|