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
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tramway
|
|
4
|
+
module SolidQueue
|
|
5
|
+
# Shared status/queue/class/search filtering for SolidQueue jobs, used by both the
|
|
6
|
+
# jobs controller (for the index page) and the background jobs that process a
|
|
7
|
+
# filtered set of jobs (e.g. "Destroy all"), since a job's `perform` only receives
|
|
8
|
+
# plain serializable arguments, not a live `ActionController::Parameters`.
|
|
9
|
+
class JobsFilter
|
|
10
|
+
STATUS_FILTERS = {
|
|
11
|
+
'ready' => ->(relation) { relation.joins(:ready_execution) },
|
|
12
|
+
'claimed' => ->(relation) { relation.joins(:claimed_execution) },
|
|
13
|
+
'scheduled' => ->(relation) { relation.joins(:scheduled_execution) },
|
|
14
|
+
'blocked' => ->(relation) { relation.joins(:blocked_execution) },
|
|
15
|
+
'failed' => ->(relation) { relation.joins(:failed_execution) },
|
|
16
|
+
'finished' => ->(relation) { relation.finished }
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def initialize(filters = {})
|
|
20
|
+
@filters = filters.to_h.stringify_keys
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def jobs
|
|
24
|
+
relation = apply_status_filter(::SolidQueue::Job.all)
|
|
25
|
+
relation = relation.where(queue_name: filters['queue_name']) if filters['queue_name'].present?
|
|
26
|
+
relation = relation.where(class_name: filters['class_name']) if filters['class_name'].present?
|
|
27
|
+
|
|
28
|
+
apply_search(relation).distinct
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def status_counts
|
|
32
|
+
STATUS_FILTERS.transform_values { |scope| scope.call(::SolidQueue::Job.all).count }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
attr_reader :filters
|
|
38
|
+
|
|
39
|
+
def apply_status_filter(relation)
|
|
40
|
+
STATUS_FILTERS.fetch(filters['status']) { ->(rel) { rel } }.call(relation)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def apply_search(relation)
|
|
44
|
+
query = filters['query'].to_s.strip
|
|
45
|
+
|
|
46
|
+
return relation if query.blank?
|
|
47
|
+
return relation.where(id: query) if query.match?(/\A\d+\z/)
|
|
48
|
+
|
|
49
|
+
relation.where('class_name LIKE :query OR active_job_id LIKE :query', query: "%#{query}%")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
- current_filter = params[:filter]
|
|
2
|
+
|
|
3
|
+
.overflow-hidden.duration-200.ease-out-strong.motion-reduce:duration-0{ class: 'transition-[height,opacity]', data: { 'tramway--collapsible-target': 'panel' } }
|
|
4
|
+
.flex.flex-wrap.gap-2.mt-2
|
|
5
|
+
- @filter_counts.each do |filter_name, count|
|
|
6
|
+
- filter_params = request.query_parameters.except('page').merge(filter: filter_name)
|
|
7
|
+
- button_type = filter_name.to_s == current_filter ? :primary : :inverse
|
|
8
|
+
= tramway_button text: "#{filter_name.to_s.upcase} · #{count}",
|
|
9
|
+
path: public_send(custom_path_method, filter_params), type: button_type, size: :small
|
|
10
|
+
|
|
11
|
+
- if current_filter.present?
|
|
12
|
+
- clear_params = request.query_parameters.except('page', 'filter')
|
|
13
|
+
= tramway_button tag: :a, path: public_send(custom_path_method, clear_params), type: :inverse,
|
|
14
|
+
size: :small, tooltip: { text: t('tramway.actions.clear_filter'), event: :hover } do
|
|
15
|
+
%i.fa.fa-times{ 'aria-hidden' => 'true' }
|
|
@@ -20,14 +20,28 @@
|
|
|
20
20
|
.mt-4
|
|
21
21
|
= decorator.constantize.index_header_content.call(@entities)
|
|
22
22
|
|
|
23
|
-
.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
- filters_key = "#{@entity.namespace}-#{@entity.name}-index-filters" if @filter_counts.present?
|
|
24
|
+
|
|
25
|
+
%div{ data: (@filter_counts.present? ? { controller: 'tramway--collapsible', key: filters_key } : {}) }
|
|
26
|
+
.flex.flex-row.items-center.mt-2.gap-4{ class: (@filter_counts.present? ? 'justify-between' : 'justify-end') }
|
|
27
|
+
- if @filter_counts.present?
|
|
28
|
+
= tramway_button tag: :button, type: :inverse,
|
|
29
|
+
data: { action: 'tramway--collapsible#toggle', 'tramway--collapsible-target': 'toggle' } do
|
|
30
|
+
%i.fa.fa-filter.mr-1{ 'aria-hidden' => 'true' }
|
|
31
|
+
= t('tramway.actions.filters')
|
|
32
|
+
%i.fa.fa-chevron-down.ml-1.transition-transform.duration-200.ease-out-strong.motion-reduce:duration-0{ 'aria-hidden' => 'true', data: { 'tramway--collapsible-target': 'icon' } }
|
|
33
|
+
|
|
34
|
+
.flex.flex-row.items-center.gap-4
|
|
35
|
+
- if @entity.page(:index).search
|
|
36
|
+
= form_with url: public_send(custom_path_method), method: :get, local: true, builder: Tramway::Form::Builder, horizontal: true do |f|
|
|
37
|
+
= f.text_field :query, label: false, value: params[:query], placeholder: t('tramway.actions.search')
|
|
38
|
+
= f.submit nil, 'aria-label' => t('tramway.actions.search') do
|
|
39
|
+
%i.fa.fa-search{ 'aria-hidden' => 'true' }
|
|
40
|
+
|
|
41
|
+
= paginate @entities, custom_path_method:
|
|
42
|
+
|
|
43
|
+
- if @filter_counts.present?
|
|
44
|
+
= render 'tramway/entities/filters', custom_path_method:
|
|
31
45
|
|
|
32
46
|
- if index_attributes.empty?
|
|
33
47
|
%p.text-center.mt-10
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
.flex.gap-2.mt-2
|
|
2
|
+
= tramway_button tag: :a, text: t('tramway.plugins.solid_queue.jobs.title'), path: jobs_path,
|
|
3
|
+
type: (current_page?(jobs_path) ? :primary : :inverse), size: :small
|
|
4
|
+
= tramway_button tag: :a, text: t('tramway.plugins.solid_queue.queues.title'), path: queues_path,
|
|
5
|
+
type: (current_page?(queues_path) ? :primary : :inverse), size: :small
|
|
6
|
+
= tramway_button tag: :a, text: t('tramway.plugins.solid_queue.recurring_tasks.title'), path: recurring_tasks_path,
|
|
7
|
+
type: (current_page?(recurring_tasks_path) ? :primary : :inverse), size: :small
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
- content_for :title, t('tramway.plugins.solid_queue.jobs.title', default: 'Background Jobs')
|
|
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{ data: { controller: 'tramway--auto-refresh' } }
|
|
11
|
+
#tramway-solid-queue-jobs-status-filters.flex.flex-wrap.gap-2.mt-2{ data: { 'tramway--auto-refresh-target': 'refreshable' } }
|
|
12
|
+
- @status_counts.each do |status, count|
|
|
13
|
+
- status_params = request.query_parameters.except('page').merge(status:)
|
|
14
|
+
- button_type = status == params[:status] ? :primary : :inverse
|
|
15
|
+
= tramway_button text: "#{solid_queue_status_label(status)} · #{count}",
|
|
16
|
+
path: jobs_path(status_params), type: button_type, size: :small
|
|
17
|
+
|
|
18
|
+
- if params[:status].present?
|
|
19
|
+
- clear_params = request.query_parameters.except('page', 'status')
|
|
20
|
+
= tramway_button tag: :a, path: jobs_path(clear_params), type: :inverse,
|
|
21
|
+
size: :small, tooltip: { text: t('tramway.actions.clear_filter'), event: :hover } do
|
|
22
|
+
%i.fa.fa-times{ 'aria-hidden' => 'true' }
|
|
23
|
+
|
|
24
|
+
- all_queues_option = [t('tramway.plugins.solid_queue.jobs.all_queues', default: 'All queues'), '']
|
|
25
|
+
- queue_name_options = [all_queues_option] + @queue_names.map { |name| [name, name] }
|
|
26
|
+
- all_classes_option = [t('tramway.plugins.solid_queue.jobs.all_classes', default: 'All classes'), '']
|
|
27
|
+
- class_name_options = [all_classes_option] + @class_names.map { |name| [name, name] }
|
|
28
|
+
|
|
29
|
+
= form_with url: jobs_path, method: :get, local: true, builder: Tramway::Form::Builder, horizontal: true do |f|
|
|
30
|
+
.flex.flex-wrap.items-center.gap-4.mt-2
|
|
31
|
+
= f.select :queue_name, queue_name_options, label: false, selected: params[:queue_name].to_s
|
|
32
|
+
= f.select :class_name, class_name_options, label: false, selected: params[:class_name].to_s
|
|
33
|
+
= f.text_field :query, label: false, value: params[:query], placeholder: t('tramway.actions.search')
|
|
34
|
+
= f.submit nil, 'aria-label' => t('tramway.actions.search') do
|
|
35
|
+
%i.fa.fa-search{ 'aria-hidden' => 'true' }
|
|
36
|
+
|
|
37
|
+
.flex.justify-between.gap-2.mt-4
|
|
38
|
+
= tramway_button text: t('tramway.actions.destroy_all'), path: destroy_all_jobs_path(request.query_parameters.except('page')),
|
|
39
|
+
method: :post, type: :rage, size: :small
|
|
40
|
+
|
|
41
|
+
= form_with url: bulk_destroy_jobs_path, method: :post, id: 'tramway-solid-queue-bulk-jobs-form', local: true do
|
|
42
|
+
.flex.gap-2
|
|
43
|
+
= tramway_button text: t('tramway.actions.retry'), tag: :button, type: :will, size: :small,
|
|
44
|
+
formaction: bulk_retry_jobs_path
|
|
45
|
+
= tramway_button text: t('tramway.actions.discard'), tag: :button, type: :warning, size: :small,
|
|
46
|
+
formaction: bulk_discard_jobs_path
|
|
47
|
+
= tramway_button text: t('tramway.actions.destroy'), tag: :button, type: :rage, size: :small,
|
|
48
|
+
formaction: bulk_destroy_jobs_path
|
|
49
|
+
|
|
50
|
+
#tramway-solid-queue-jobs-table{ data: { 'tramway--auto-refresh-target': 'refreshable' } }
|
|
51
|
+
= tramway_table class: 'mt-4' do
|
|
52
|
+
= tramway_header headers: ['', 'ID', t('tramway.plugins.solid_queue.jobs.class_name', default: 'Class'),
|
|
53
|
+
t('tramway.plugins.solid_queue.jobs.queue_name', default: 'Queue'),
|
|
54
|
+
t('tramway.plugins.solid_queue.jobs.status', default: 'Status'),
|
|
55
|
+
t('tramway.plugins.solid_queue.jobs.scheduled_at', default: 'Scheduled at'),
|
|
56
|
+
t('tramway.table.actions_header')]
|
|
57
|
+
- @jobs.each do |job|
|
|
58
|
+
= tramway_row do
|
|
59
|
+
= tramway_cell do
|
|
60
|
+
= check_box_tag 'job_ids[]', job.id, false, id: "tramway-solid-queue-job-#{job.id}",
|
|
61
|
+
form: 'tramway-solid-queue-bulk-jobs-form'
|
|
62
|
+
= tramway_cell do
|
|
63
|
+
= job.id
|
|
64
|
+
= tramway_cell do
|
|
65
|
+
= job.class_name
|
|
66
|
+
= tramway_cell do
|
|
67
|
+
= job.queue_name
|
|
68
|
+
= tramway_cell do
|
|
69
|
+
= tramway_badge text: solid_queue_status_label(job.status),
|
|
70
|
+
type: solid_queue_status_badge_type(job.status)
|
|
71
|
+
= tramway_cell do
|
|
72
|
+
= job.scheduled_at
|
|
73
|
+
= tramway_cell do
|
|
74
|
+
.flex.gap-2
|
|
75
|
+
= tramway_button tag: :a, path: job_path(job.id), type: :inverse, size: :small do
|
|
76
|
+
%i.fa.fa-eye{ 'aria-hidden' => 'true' }
|
|
77
|
+
- if job.status == :failed
|
|
78
|
+
= tramway_button path: retry_job_path(job.id), method: :post, type: :will, size: :small do
|
|
79
|
+
%i.fa.fa-redo{ 'aria-hidden' => 'true' }
|
|
80
|
+
- if job.status != :finished
|
|
81
|
+
= tramway_button path: discard_job_path(job.id), method: :post, type: :warning, size: :small do
|
|
82
|
+
%i.fa.fa-ban{ 'aria-hidden' => 'true' }
|
|
83
|
+
= tramway_button path: job_path(job.id), method: :delete, type: :rage, size: :small do
|
|
84
|
+
%i.fa.fa-trash{ 'aria-hidden' => 'true' }
|
|
85
|
+
|
|
86
|
+
.flex.flex-col.items-center.mt-4.gap-2
|
|
87
|
+
= paginate @jobs
|
|
88
|
+
%p.sm:hidden.text-xs.text-zinc-500
|
|
89
|
+
= page_entries_info @jobs
|
|
@@ -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,9 +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"
|
|
18
|
+
filters: "Filters"
|
|
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"
|
|
17
26
|
notices:
|
|
18
27
|
created: "The record is created"
|
|
19
28
|
updated: "The record is updated"
|
|
@@ -22,3 +31,58 @@ en:
|
|
|
22
31
|
placeholders:
|
|
23
32
|
waiting: "Waiting for an answer..."
|
|
24
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
|
@@ -61,6 +61,7 @@ module.exports = {
|
|
|
61
61
|
'ease-in-out',
|
|
62
62
|
'md:transition-[width]',
|
|
63
63
|
'transition-[padding-left]',
|
|
64
|
+
'transition-[height,opacity]',
|
|
64
65
|
'ease-in-out-strong',
|
|
65
66
|
'md:ease-in-out-strong',
|
|
66
67
|
'ease-out-strong',
|
|
@@ -281,6 +282,7 @@ module.exports = {
|
|
|
281
282
|
// === Visibility and typography helpers ===
|
|
282
283
|
'hidden',
|
|
283
284
|
'text-xl',
|
|
285
|
+
'text-2xl',
|
|
284
286
|
'font-bold',
|
|
285
287
|
'prose',
|
|
286
288
|
'prose-invert',
|
|
@@ -468,6 +470,8 @@ module.exports = {
|
|
|
468
470
|
'whitespace-nowrap',
|
|
469
471
|
'h-fit',
|
|
470
472
|
'gap-1',
|
|
473
|
+
'mr-1',
|
|
474
|
+
'ml-1',
|
|
471
475
|
'h-10',
|
|
472
476
|
'px-2.5',
|
|
473
477
|
'py-0.5',
|
|
@@ -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
|
|
@@ -115,7 +116,7 @@ module Tramway
|
|
|
115
116
|
|
|
116
117
|
def stimulus_controller_imports
|
|
117
118
|
[
|
|
118
|
-
'import { Navbar, TramwaySelect, UiCheckbox, Tooltip } from "@tramway/tramway"'
|
|
119
|
+
'import { Navbar, TramwaySelect, UiCheckbox, Tooltip, Collapsible } from "@tramway/tramway"'
|
|
119
120
|
]
|
|
120
121
|
end
|
|
121
122
|
|
|
@@ -124,7 +125,8 @@ module Tramway
|
|
|
124
125
|
"application.register('tramway-navbar', Navbar)",
|
|
125
126
|
"application.register('tramway-select', TramwaySelect)",
|
|
126
127
|
"application.register('ui--checkbox', UiCheckbox)",
|
|
127
|
-
"application.register('tramway-tooltip', Tooltip)"
|
|
128
|
+
"application.register('tramway-tooltip', Tooltip)",
|
|
129
|
+
"application.register('tramway--collapsible', Collapsible)"
|
|
128
130
|
]
|
|
129
131
|
end
|
|
130
132
|
|
|
@@ -299,7 +301,8 @@ module Tramway
|
|
|
299
301
|
"application.register('tramway-select'",
|
|
300
302
|
"application.register('table-row-preview'",
|
|
301
303
|
"application.register('ui--checkbox'",
|
|
302
|
-
"application.register('tramway-tooltip'"
|
|
304
|
+
"application.register('tramway-tooltip'",
|
|
305
|
+
"application.register('tramway--collapsible'"
|
|
303
306
|
]
|
|
304
307
|
end
|
|
305
308
|
|
|
@@ -343,6 +346,7 @@ module Tramway
|
|
|
343
346
|
include InstallGeneratorHelpers
|
|
344
347
|
include InstallGeneratorFontAwesomeHelpers
|
|
345
348
|
include InstallGeneratorLayoutHelpers
|
|
349
|
+
include InstallGeneratorSolidQueueHelpers
|
|
346
350
|
|
|
347
351
|
desc 'Installs Tramway dependencies and Tailwind safelist configuration.'
|
|
348
352
|
|
|
@@ -440,6 +444,16 @@ module Tramway
|
|
|
440
444
|
def ensure_navbar_sidebar_offset_in_application_layout
|
|
441
445
|
ensure_navbar_sidebar_offset
|
|
442
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
|
|
443
457
|
end
|
|
444
458
|
end
|
|
445
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
|