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
@@ -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
- .flex.flex-row.justify-end.mt-2.gap-4
24
- - if @entity.page(:index).search
25
- = form_with url: public_send(custom_path_method), method: :get, local: true, builder: Tramway::Form::Builder, horizontal: true do |f|
26
- = f.text_field :query, label: false, value: params[:query], placeholder: t('tramway.actions.search')
27
- = f.submit nil, 'aria-label' => t('tramway.actions.search') do
28
- %i.fa.fa-search{ 'aria-hidden' => 'true' }
29
-
30
- = paginate @entities, custom_path_method:
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
@@ -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|
@@ -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
@@ -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
@@ -10,6 +10,7 @@ module Tramway
10
10
  attribute? :scope, Types::Coercible::String
11
11
  attribute? :search, Types::Bool
12
12
  attribute? :includes, Types::Array.default([].freeze)
13
+ attribute? :filters, Types::Array.default([].freeze)
13
14
  end
14
15
  end
15
16
  end