solid_web_ui 0.1.0

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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +74 -0
  3. data/app/assets/stylesheets/solid_web_ui.css +232 -0
  4. data/app/components/solid_web_ui/ui/page_component.html.erb +15 -0
  5. data/app/components/solid_web_ui/ui/page_component.rb +22 -0
  6. data/app/components/solid_web_ui/ui/paginator_component.html.erb +21 -0
  7. data/app/components/solid_web_ui/ui/paginator_component.rb +23 -0
  8. data/app/components/solid_web_ui/ui/stat_card_component.html.erb +11 -0
  9. data/app/components/solid_web_ui/ui/stat_card_component.rb +24 -0
  10. data/app/components/solid_web_ui/ui/status_badge_component.rb +37 -0
  11. data/app/components/solid_web_ui/ui/table_component.html.erb +18 -0
  12. data/app/components/solid_web_ui/ui/table_component.rb +19 -0
  13. data/app/controllers/solid_web_ui/cable/application_controller.rb +14 -0
  14. data/app/controllers/solid_web_ui/cable/channels_controller.rb +13 -0
  15. data/app/controllers/solid_web_ui/cable/dashboard_controller.rb +13 -0
  16. data/app/controllers/solid_web_ui/cable/messages_controller.rb +18 -0
  17. data/app/controllers/solid_web_ui/cache/application_controller.rb +14 -0
  18. data/app/controllers/solid_web_ui/cache/dashboard_controller.rb +13 -0
  19. data/app/controllers/solid_web_ui/cache/entries_controller.rb +24 -0
  20. data/app/controllers/solid_web_ui/queue/application_controller.rb +17 -0
  21. data/app/controllers/solid_web_ui/queue/dashboard_controller.rb +18 -0
  22. data/app/controllers/solid_web_ui/queue/failed_executions_controller.rb +34 -0
  23. data/app/controllers/solid_web_ui/queue/jobs_controller.rb +24 -0
  24. data/app/controllers/solid_web_ui/queue/processes_controller.rb +9 -0
  25. data/app/controllers/solid_web_ui/queue/queues_controller.rb +27 -0
  26. data/app/controllers/solid_web_ui/queue/recurring_tasks_controller.rb +9 -0
  27. data/app/helpers/solid_web_ui/cable/application_helper.rb +22 -0
  28. data/app/helpers/solid_web_ui/cache/application_helper.rb +23 -0
  29. data/app/helpers/solid_web_ui/component_helper.rb +28 -0
  30. data/app/helpers/solid_web_ui/queue/application_helper.rb +38 -0
  31. data/app/views/layouts/solid_web_ui.html.erb +33 -0
  32. data/app/views/solid_web_ui/cable/channels/index.html.erb +12 -0
  33. data/app/views/solid_web_ui/cable/dashboard/index.html.erb +24 -0
  34. data/app/views/solid_web_ui/cache/dashboard/index.html.erb +15 -0
  35. data/app/views/solid_web_ui/cache/entries/index.html.erb +15 -0
  36. data/app/views/solid_web_ui/queue/dashboard/index.html.erb +13 -0
  37. data/app/views/solid_web_ui/queue/failed_executions/index.html.erb +26 -0
  38. data/app/views/solid_web_ui/queue/jobs/index.html.erb +23 -0
  39. data/app/views/solid_web_ui/queue/processes/index.html.erb +14 -0
  40. data/app/views/solid_web_ui/queue/queues/index.html.erb +25 -0
  41. data/app/views/solid_web_ui/queue/recurring_tasks/index.html.erb +17 -0
  42. data/lib/solid_web_ui/cable/engine.rb +13 -0
  43. data/lib/solid_web_ui/cable/routes.rb +7 -0
  44. data/lib/solid_web_ui/cable.rb +18 -0
  45. data/lib/solid_web_ui/cache/engine.rb +13 -0
  46. data/lib/solid_web_ui/cache/routes.rb +7 -0
  47. data/lib/solid_web_ui/cache.rb +17 -0
  48. data/lib/solid_web_ui/configurable.rb +20 -0
  49. data/lib/solid_web_ui/engine.rb +13 -0
  50. data/lib/solid_web_ui/paginator.rb +49 -0
  51. data/lib/solid_web_ui/queue/engine.rb +15 -0
  52. data/lib/solid_web_ui/queue/routes.rb +18 -0
  53. data/lib/solid_web_ui/queue.rb +19 -0
  54. data/lib/solid_web_ui/theme.rb +71 -0
  55. data/lib/solid_web_ui/version.rb +5 -0
  56. data/lib/solid_web_ui.rb +33 -0
  57. metadata +193 -0
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidWebUi::Queue
4
+ class FailedExecutionsController < ApplicationController
5
+ before_action :ensure_retry_enabled, only: :retry
6
+ before_action :ensure_discard_enabled, only: :discard
7
+
8
+ def index
9
+ scope = SolidQueue::FailedExecution.includes(:job).order(id: :desc)
10
+ @paginator = SolidWebUi::Paginator.new(scope, page: params[:page], per_page: per_page)
11
+ @failed = @paginator.records
12
+ end
13
+
14
+ def retry
15
+ SolidQueue::FailedExecution.find(params[:id]).retry
16
+ redirect_to failed_executions_path, notice: "Job re-enqueued."
17
+ end
18
+
19
+ def discard
20
+ SolidQueue::FailedExecution.find(params[:id]).job.destroy
21
+ redirect_to failed_executions_path, notice: "Failed job discarded."
22
+ end
23
+
24
+ private
25
+
26
+ def ensure_retry_enabled
27
+ head :forbidden unless SolidWebUi::Queue.config.enable_retry
28
+ end
29
+
30
+ def ensure_discard_enabled
31
+ head :forbidden unless SolidWebUi::Queue.config.enable_discard
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidWebUi::Queue
4
+ class JobsController < ApplicationController
5
+ STATUS_SCOPES = {
6
+ "ready" => -> { SolidQueue::Job.where(id: SolidQueue::ReadyExecution.select(:job_id)) },
7
+ "scheduled" => -> { SolidQueue::Job.where(id: SolidQueue::ScheduledExecution.select(:job_id)) },
8
+ "in_progress" => -> { SolidQueue::Job.where(id: SolidQueue::ClaimedExecution.select(:job_id)) },
9
+ "blocked" => -> { SolidQueue::Job.where(id: SolidQueue::BlockedExecution.select(:job_id)) },
10
+ "failed" => -> { SolidQueue::Job.where(id: SolidQueue::FailedExecution.select(:job_id)) },
11
+ "finished" => -> { SolidQueue::Job.where.not(finished_at: nil) }
12
+ }.freeze
13
+
14
+ def index
15
+ @status = params[:status].presence_in(STATUS_SCOPES.keys) || "all"
16
+ scope = (STATUS_SCOPES[@status]&.call || SolidQueue::Job.all)
17
+ .includes(:ready_execution, :scheduled_execution, :claimed_execution, :blocked_execution, :failed_execution)
18
+ .order(id: :desc)
19
+
20
+ @paginator = SolidWebUi::Paginator.new(scope, page: params[:page], per_page: per_page)
21
+ @jobs = @paginator.records
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidWebUi::Queue
4
+ class ProcessesController < ApplicationController
5
+ def index
6
+ @processes = SolidQueue::Process.order(:kind, :name)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidWebUi::Queue
4
+ class QueuesController < ApplicationController
5
+ before_action :ensure_pause_enabled, only: %i[pause resume]
6
+
7
+ def index
8
+ @queues = SolidQueue::Queue.all
9
+ end
10
+
11
+ def pause
12
+ SolidQueue::Queue.find_by_name(params[:name]).pause
13
+ redirect_to queues_path, notice: "Queue #{params[:name]} paused."
14
+ end
15
+
16
+ def resume
17
+ SolidQueue::Queue.find_by_name(params[:name]).resume
18
+ redirect_to queues_path, notice: "Queue #{params[:name]} resumed."
19
+ end
20
+
21
+ private
22
+
23
+ def ensure_pause_enabled
24
+ head :forbidden unless SolidWebUi::Queue.config.enable_pause
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidWebUi::Queue
4
+ class RecurringTasksController < ApplicationController
5
+ def index
6
+ @tasks = SolidQueue::RecurringTask.order(:key)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidWebUi::Cable
4
+ module ApplicationHelper
5
+ def cable_nav(active)
6
+ [
7
+ { label: "Dashboard", href: root_path, active: active == :dashboard },
8
+ { label: "Channels", href: channels_path, active: active == :channels }
9
+ ]
10
+ end
11
+
12
+ def readable_channel(channel)
13
+ truncate(channel.to_s.dup.force_encoding("UTF-8").scrub("?"), length: 80)
14
+ end
15
+
16
+ def short_time(time)
17
+ return "—" if time.nil?
18
+
19
+ time.in_time_zone(SolidWebUi::Cable.config.time_zone).strftime("%Y-%m-%d %H:%M:%S")
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidWebUi::Cache
4
+ module ApplicationHelper
5
+ def cache_nav(active)
6
+ [
7
+ { label: "Dashboard", href: root_path, active: active == :dashboard },
8
+ { label: "Entries", href: entries_path, active: active == :entries }
9
+ ]
10
+ end
11
+
12
+ # Cache keys are stored as binary; present them as readable, truncated text.
13
+ def readable_key(key)
14
+ truncate(key.to_s.dup.force_encoding("UTF-8").scrub("?"), length: 80)
15
+ end
16
+
17
+ def short_time(time)
18
+ return "—" if time.nil?
19
+
20
+ time.in_time_zone(SolidWebUi::Cache.config.time_zone).strftime("%Y-%m-%d %H:%M:%S")
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidWebUi
4
+ # Thin view helpers wrapping the shared Ui::* ViewComponents, so engine views
5
+ # read `swui_page(...)` instead of `render SolidWebUi::Ui::PageComponent.new(...)`.
6
+ # Included into each engine's controller via `helper SolidWebUi::ComponentHelper`.
7
+ module ComponentHelper
8
+ def swui_page(title:, nav: [], &block)
9
+ render(Ui::PageComponent.new(title: title, nav: nav), &block)
10
+ end
11
+
12
+ def swui_stat_card(label:, value:, tone: :neutral, href: nil)
13
+ render(Ui::StatCardComponent.new(label: label, value: value, tone: tone, href: href))
14
+ end
15
+
16
+ def swui_status_badge(label:, status: nil)
17
+ render(Ui::StatusBadgeComponent.new(label: label, status: status))
18
+ end
19
+
20
+ def swui_table(headers:, empty_message: "Nothing to show.", &block)
21
+ render(Ui::TableComponent.new(headers: headers, empty_message: empty_message), &block)
22
+ end
23
+
24
+ def swui_paginator(paginator:, page_url:)
25
+ render(Ui::PaginatorComponent.new(paginator: paginator, page_url: page_url))
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidWebUi::Queue
4
+ module ApplicationHelper
5
+ def queue_nav(active)
6
+ [
7
+ { label: "Dashboard", href: root_path, active: active == :dashboard },
8
+ { label: "Queues", href: queues_path, active: active == :queues },
9
+ { label: "Jobs", href: jobs_path, active: active == :jobs },
10
+ { label: "Failed", href: failed_executions_path, active: active == :failed },
11
+ { label: "Processes", href: processes_path, active: active == :processes },
12
+ { label: "Recurring", href: recurring_tasks_path, active: active == :recurring }
13
+ ]
14
+ end
15
+
16
+ # Status of a job derived from which execution table holds it.
17
+ def job_status(job)
18
+ return :finished if job.finished_at?
19
+ return :failed if job.failed_execution
20
+ return :blocked if job.blocked_execution
21
+ return :in_progress if job.claimed_execution
22
+ return :scheduled if job.scheduled_execution
23
+ return :ready if job.ready_execution
24
+
25
+ :unknown
26
+ end
27
+
28
+ def queue_latency(queue)
29
+ queue.respond_to?(:human_latency) ? queue.human_latency : queue.latency
30
+ end
31
+
32
+ def short_time(time)
33
+ return "—" if time.nil?
34
+
35
+ time.in_time_zone(SolidWebUi::Queue.config.time_zone).strftime("%Y-%m-%d %H:%M:%S")
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,33 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title><%= content_for?(:title) ? yield(:title) : "Solid Web" %></title>
7
+ <%= csrf_meta_tags %>
8
+
9
+ <% if SolidWebUi.config.stylesheet %>
10
+ <%= stylesheet_link_tag "solid_web_ui", "data-turbo-track": "reload" %>
11
+ <% end %>
12
+ <% Array(SolidWebUi.config.extra_stylesheets).each do |sheet| %>
13
+ <%= stylesheet_link_tag sheet, "data-turbo-track": "reload" %>
14
+ <% end %>
15
+
16
+ <%# Theme tokens (host re-themes by overriding these values, never the stylesheet). %>
17
+ <style>
18
+ .solid-web-ui { <%= SolidWebUi::Theme.css_vars(SolidWebUi.config.theme).html_safe %> }
19
+ <% if SolidWebUi.config.color_scheme.to_s == "dark" %>
20
+ .solid-web-ui { <%= SolidWebUi::Theme.dark_css_vars(SolidWebUi.config.theme).html_safe %> }
21
+ <% elsif SolidWebUi.config.color_scheme.to_s == "auto" %>
22
+ @media (prefers-color-scheme: dark) {
23
+ .solid-web-ui { <%= SolidWebUi::Theme.dark_css_vars(SolidWebUi.config.theme).html_safe %> }
24
+ }
25
+ <% end %>
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <div class="solid-web-ui" data-color-scheme="<%= SolidWebUi.config.color_scheme %>">
30
+ <%= yield %>
31
+ </div>
32
+ </body>
33
+ </html>
@@ -0,0 +1,12 @@
1
+ <% content_for :title, "Channels" %>
2
+ <%= swui_page(title: "Channels", nav: cable_nav(:channels)) do %>
3
+ <%= swui_table(headers: [ "Channel", "Messages", "Last activity" ], empty_message: "No channels.") do %>
4
+ <% @channels.each do |row| %>
5
+ <tr>
6
+ <td><code><%= readable_channel(row[:name]) %></code></td>
7
+ <td><%= number_with_delimiter(row[:count]) %></td>
8
+ <td><%= short_time(row[:last]) %></td>
9
+ </tr>
10
+ <% end %>
11
+ <% end %>
12
+ <% end %>
@@ -0,0 +1,24 @@
1
+ <% content_for :title, "#{SolidWebUi::Cable.config.page_title} · Dashboard" %>
2
+ <%= swui_page(title: SolidWebUi::Cable.config.page_title, nav: cable_nav(:dashboard)) do %>
3
+ <div class="swui-grid">
4
+ <%= swui_stat_card(label: "Messages", value: number_with_delimiter(@total), tone: :primary) %>
5
+ <%= swui_stat_card(label: "Channels", value: @channel_count, href: channels_path) %>
6
+ <%= swui_stat_card(label: "Last hour", value: number_with_delimiter(@last_hour)) %>
7
+ <%= swui_stat_card(label: "Trimmable", value: number_with_delimiter(@trimmable), tone: @trimmable.positive? ? :warning : :neutral) %>
8
+ </div>
9
+
10
+ <% if SolidWebUi::Cable.config.enable_trim %>
11
+ <%= button_to "Trim old messages", trim_messages_path, method: :delete, class: "swui-btn swui-btn--danger",
12
+ form: { data: { turbo_confirm: "Delete messages older than the retention window?" } } %>
13
+ <% end %>
14
+
15
+ <h2 class="swui-section-title">Top channels</h2>
16
+ <%= swui_table(headers: [ "Channel", "Messages" ], empty_message: "No messages broadcast yet.") do %>
17
+ <% @top_channels.each do |channel, count| %>
18
+ <tr>
19
+ <td><code><%= readable_channel(channel) %></code></td>
20
+ <td><%= number_with_delimiter(count) %></td>
21
+ </tr>
22
+ <% end %>
23
+ <% end %>
24
+ <% end %>
@@ -0,0 +1,15 @@
1
+ <% content_for :title, "#{SolidWebUi::Cache.config.page_title} · Dashboard" %>
2
+ <%= swui_page(title: SolidWebUi::Cache.config.page_title, nav: cache_nav(:dashboard)) do %>
3
+ <div class="swui-grid">
4
+ <%= swui_stat_card(label: "Entries", value: number_with_delimiter(@count), tone: :primary, href: entries_path) %>
5
+ <%= swui_stat_card(label: "Total size", value: number_to_human_size(@total_bytes)) %>
6
+ <%= swui_stat_card(label: "Avg entry", value: number_to_human_size(@avg_bytes)) %>
7
+ <%= swui_stat_card(label: "Oldest", value: short_time(@oldest)) %>
8
+ <%= swui_stat_card(label: "Newest", value: short_time(@newest)) %>
9
+ </div>
10
+
11
+ <% if SolidWebUi::Cache.config.enable_clear %>
12
+ <%= button_to "Clear cache", clear_entries_path, method: :delete, class: "swui-btn swui-btn--danger",
13
+ form: { data: { turbo_confirm: "Delete ALL cache entries?" } } %>
14
+ <% end %>
15
+ <% end %>
@@ -0,0 +1,15 @@
1
+ <% content_for :title, "Cache entries" %>
2
+ <%= swui_page(title: "Cache entries", nav: cache_nav(:entries)) do %>
3
+ <%= swui_table(headers: [ "ID", "Key", "Size", "Created" ], empty_message: "The cache is empty.") do %>
4
+ <% @entries.each do |entry| %>
5
+ <tr>
6
+ <td><%= entry.id %></td>
7
+ <td><code><%= readable_key(entry.key) %></code></td>
8
+ <td><%= number_to_human_size(entry.byte_size) %></td>
9
+ <td><%= short_time(entry.created_at) %></td>
10
+ </tr>
11
+ <% end %>
12
+ <% end %>
13
+
14
+ <%= swui_paginator(paginator: @paginator, page_url: ->(page) { entries_path(page: page) }) %>
15
+ <% end %>
@@ -0,0 +1,13 @@
1
+ <% content_for :title, "#{SolidWebUi::Queue.config.page_title} · Dashboard" %>
2
+ <%= swui_page(title: SolidWebUi::Queue.config.page_title, nav: queue_nav(:dashboard)) do %>
3
+ <div class="swui-grid">
4
+ <%= swui_stat_card(label: "Ready", value: @counts[:ready], tone: :primary, href: jobs_path(status: "ready")) %>
5
+ <%= swui_stat_card(label: "Scheduled", value: @counts[:scheduled], href: jobs_path(status: "scheduled")) %>
6
+ <%= swui_stat_card(label: "In progress", value: @counts[:in_progress], href: jobs_path(status: "in_progress")) %>
7
+ <%= swui_stat_card(label: "Blocked", value: @counts[:blocked], tone: :warning, href: jobs_path(status: "blocked")) %>
8
+ <%= swui_stat_card(label: "Failed", value: @counts[:failed], tone: :danger, href: failed_executions_path) %>
9
+ <%= swui_stat_card(label: "Finished", value: @counts[:finished], tone: :success, href: jobs_path(status: "finished")) %>
10
+ <%= swui_stat_card(label: "Queues", value: @queue_count, href: queues_path) %>
11
+ <%= swui_stat_card(label: "Processes", value: @process_count, href: processes_path) %>
12
+ </div>
13
+ <% end %>
@@ -0,0 +1,26 @@
1
+ <% content_for :title, "Failed jobs" %>
2
+ <%= swui_page(title: "Failed jobs", nav: queue_nav(:failed)) do %>
3
+ <%= swui_table(headers: [ "ID", "Job", "Queue", "Exception", "Message", "Failed at", "" ], empty_message: "No failed jobs. 🎉") do %>
4
+ <% @failed.each do |failed| %>
5
+ <tr>
6
+ <td><%= failed.id %></td>
7
+ <td><%= failed.job.class_name %></td>
8
+ <td><%= failed.job.queue_name %></td>
9
+ <td><%= swui_status_badge(label: failed.exception_class.to_s, status: :failed) %></td>
10
+ <td><%= truncate(failed.message.to_s, length: 80) %></td>
11
+ <td><%= short_time(failed.created_at) %></td>
12
+ <td>
13
+ <% if SolidWebUi::Queue.config.enable_retry %>
14
+ <%= button_to "Retry", retry_failed_execution_path(failed), class: "swui-btn" %>
15
+ <% end %>
16
+ <% if SolidWebUi::Queue.config.enable_discard %>
17
+ <%= button_to "Discard", failed_execution_path(failed), method: :delete, class: "swui-btn swui-btn--danger",
18
+ form: { data: { turbo_confirm: "Discard this job permanently?" } } %>
19
+ <% end %>
20
+ </td>
21
+ </tr>
22
+ <% end %>
23
+ <% end %>
24
+
25
+ <%= swui_paginator(paginator: @paginator, page_url: ->(page) { failed_executions_path(page: page) }) %>
26
+ <% end %>
@@ -0,0 +1,23 @@
1
+ <% content_for :title, "Jobs" %>
2
+ <%= swui_page(title: "Jobs", nav: queue_nav(:jobs)) do %>
3
+ <nav class="swui-nav" style="margin-bottom: 1rem;">
4
+ <% %w[all ready scheduled in_progress blocked finished failed].each do |status| %>
5
+ <%= link_to status.humanize, jobs_path(status: status),
6
+ class: "swui-nav__link #{'swui-nav__link--active' if @status == status}" %>
7
+ <% end %>
8
+ </nav>
9
+
10
+ <%= swui_table(headers: [ "ID", "Job", "Queue", "Status", "Created" ], empty_message: "No jobs.") do %>
11
+ <% @jobs.each do |job| %>
12
+ <tr>
13
+ <td><%= job.id %></td>
14
+ <td><%= job.class_name %></td>
15
+ <td><%= job.queue_name %></td>
16
+ <td><%= swui_status_badge(label: job_status(job).to_s.humanize, status: job_status(job)) %></td>
17
+ <td><%= short_time(job.created_at) %></td>
18
+ </tr>
19
+ <% end %>
20
+ <% end %>
21
+
22
+ <%= swui_paginator(paginator: @paginator, page_url: ->(page) { jobs_path(status: @status, page: page) }) %>
23
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <% content_for :title, "Processes" %>
2
+ <%= swui_page(title: "Processes", nav: queue_nav(:processes)) do %>
3
+ <%= swui_table(headers: [ "Kind", "Name", "PID", "Host", "Last heartbeat" ], empty_message: "No running processes.") do %>
4
+ <% @processes.each do |process| %>
5
+ <tr>
6
+ <td><%= swui_status_badge(label: process.kind, status: :info) %></td>
7
+ <td><%= process.name %></td>
8
+ <td><%= process.pid %></td>
9
+ <td><%= process.hostname %></td>
10
+ <td><%= short_time(process.last_heartbeat_at) %></td>
11
+ </tr>
12
+ <% end %>
13
+ <% end %>
14
+ <% end %>
@@ -0,0 +1,25 @@
1
+ <% content_for :title, "Queues" %>
2
+ <%= swui_page(title: "Queues", nav: queue_nav(:queues)) do %>
3
+ <%= swui_table(headers: [ "Queue", "Size", "Latency", "Status", "" ], empty_message: "No queues with activity.") do %>
4
+ <% @queues.each do |queue| %>
5
+ <tr>
6
+ <td><%= queue.name %></td>
7
+ <td><%= queue.size %></td>
8
+ <td><%= queue_latency(queue) %></td>
9
+ <td>
10
+ <%= swui_status_badge(label: queue.paused? ? "paused" : "active",
11
+ status: queue.paused? ? :paused : :ready) %>
12
+ </td>
13
+ <td>
14
+ <% if SolidWebUi::Queue.config.enable_pause %>
15
+ <% if queue.paused? %>
16
+ <%= button_to "Resume", resume_queue_path(queue.name), class: "swui-btn" %>
17
+ <% else %>
18
+ <%= button_to "Pause", pause_queue_path(queue.name), class: "swui-btn" %>
19
+ <% end %>
20
+ <% end %>
21
+ </td>
22
+ </tr>
23
+ <% end %>
24
+ <% end %>
25
+ <% end %>
@@ -0,0 +1,17 @@
1
+ <% content_for :title, "Recurring tasks" %>
2
+ <%= swui_page(title: "Recurring tasks", nav: queue_nav(:recurring)) do %>
3
+ <%= swui_table(headers: [ "Key", "Schedule", "Class / Command", "Queue", "Type" ], empty_message: "No recurring tasks.") do %>
4
+ <% @tasks.each do |task| %>
5
+ <tr>
6
+ <td><%= task.key %></td>
7
+ <td><code><%= task.schedule %></code></td>
8
+ <td><%= task.class_name.presence || task.command %></td>
9
+ <td><%= task.queue_name %></td>
10
+ <td>
11
+ <%= swui_status_badge(label: task.static? ? "static" : "dynamic",
12
+ status: task.static? ? :info : :neutral) %>
13
+ </td>
14
+ </tr>
15
+ <% end %>
16
+ <% end %>
17
+ <% end %>
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+
5
+ module SolidWebUi
6
+ module Cable
7
+ class Engine < ::Rails::Engine
8
+ isolate_namespace SolidWebUi::Cable
9
+
10
+ config.paths["config/routes.rb"] = [ "lib/solid_web_ui/cable/routes.rb" ]
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ SolidWebUi::Cable::Engine.routes.draw do
4
+ root to: "dashboard#index"
5
+ get "channels", to: "channels#index", as: :channels
6
+ delete "messages/trim", to: "messages#trim", as: :trim_messages
7
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "solid_cable"
4
+
5
+ module SolidWebUi
6
+ # Mountable dashboard for Solid Cable. Part of the solid_web_ui gem; mount its
7
+ # engine independently: `mount SolidWebUi::Cable::Engine => "/admin/cable"`.
8
+ module Cable
9
+ extend SolidWebUi::Configurable
10
+
11
+ config.page_title = "Solid Cable"
12
+
13
+ setting :enable_trim, default: true
14
+ setting :retention, default: 1.day
15
+ end
16
+ end
17
+
18
+ require "solid_web_ui/cable/engine"
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+
5
+ module SolidWebUi
6
+ module Cache
7
+ class Engine < ::Rails::Engine
8
+ isolate_namespace SolidWebUi::Cache
9
+
10
+ config.paths["config/routes.rb"] = [ "lib/solid_web_ui/cache/routes.rb" ]
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ SolidWebUi::Cache::Engine.routes.draw do
4
+ root to: "dashboard#index"
5
+ get "entries", to: "entries#index", as: :entries
6
+ delete "entries", to: "entries#clear", as: :clear_entries
7
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "solid_cache"
4
+
5
+ module SolidWebUi
6
+ # Mountable dashboard for Solid Cache. Part of the solid_web_ui gem; mount its
7
+ # engine independently: `mount SolidWebUi::Cache::Engine => "/admin/cache"`.
8
+ module Cache
9
+ extend SolidWebUi::Configurable
10
+
11
+ config.page_title = "Solid Cache"
12
+
13
+ setting :enable_clear, default: true
14
+ end
15
+ end
16
+
17
+ require "solid_web_ui/cache/engine"
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/configurable"
4
+
5
+ module SolidWebUi
6
+ # Shared dry-configurable base for the three web engines. Each engine does
7
+ # `extend SolidWebUi::Configurable` and then adds its own feature-flag settings
8
+ # (enable_retry, enable_clear, enable_trim, …). Centralizes the settings every
9
+ # dashboard needs: which controller to inherit (host auth), page size, time zone
10
+ # and the page title.
11
+ module Configurable
12
+ def self.extended(base)
13
+ base.extend Dry::Configurable
14
+ base.setting :base_controller_class, default: "ActionController::Base"
15
+ base.setting :per_page, default: 25
16
+ base.setting :time_zone, default: "UTC"
17
+ base.setting :page_title, default: base.respond_to?(:name) ? base.name : "Solid Web"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "view_component"
5
+
6
+ module SolidWebUi
7
+ # Not isolated on purpose: this engine is a shared design library. Keeping its
8
+ # app/views, app/components and app/assets on the global lookup paths lets the
9
+ # three web engines (queue/cache/cable) resolve the shared layout, components
10
+ # and the single precompiled stylesheet without any manual view-path wiring.
11
+ class Engine < ::Rails::Engine
12
+ end
13
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidWebUi
4
+ # Tiny offset paginator so the gems don't take a hard dependency on
5
+ # kaminari/pagy. Works with any object responding to #count, #limit and
6
+ # #offset (an ActiveRecord::Relation), or with a precomputed Integer count.
7
+ class Paginator
8
+ attr_reader :page, :per_page, :total_count
9
+
10
+ def initialize(scope, page:, per_page: 25)
11
+ @scope = scope
12
+ @per_page = [ per_page.to_i, 1 ].max
13
+ @total_count = scope.is_a?(Integer) ? scope : scope.count
14
+ @page = [ page.to_i, 1 ].max
15
+ end
16
+
17
+ def total_pages
18
+ [ (total_count.to_f / per_page).ceil, 1 ].max
19
+ end
20
+
21
+ def current_page
22
+ [ [ page, total_pages ].min, 1 ].max
23
+ end
24
+
25
+ def offset
26
+ (current_page - 1) * per_page
27
+ end
28
+
29
+ def records
30
+ @scope.limit(per_page).offset(offset)
31
+ end
32
+
33
+ def first_page?
34
+ current_page <= 1
35
+ end
36
+
37
+ def last_page?
38
+ current_page >= total_pages
39
+ end
40
+
41
+ def prev_page
42
+ first_page? ? nil : current_page - 1
43
+ end
44
+
45
+ def next_page
46
+ last_page? ? nil : current_page + 1
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+
5
+ module SolidWebUi
6
+ module Queue
7
+ class Engine < ::Rails::Engine
8
+ isolate_namespace SolidWebUi::Queue
9
+
10
+ # All sub-engines share the gem root, so point each at its own routes file
11
+ # (the default config/routes.rb would collide across the parts).
12
+ config.paths["config/routes.rb"] = [ "lib/solid_web_ui/queue/routes.rb" ]
13
+ end
14
+ end
15
+ end