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.
- checksums.yaml +7 -0
- data/README.md +74 -0
- data/app/assets/stylesheets/solid_web_ui.css +232 -0
- data/app/components/solid_web_ui/ui/page_component.html.erb +15 -0
- data/app/components/solid_web_ui/ui/page_component.rb +22 -0
- data/app/components/solid_web_ui/ui/paginator_component.html.erb +21 -0
- data/app/components/solid_web_ui/ui/paginator_component.rb +23 -0
- data/app/components/solid_web_ui/ui/stat_card_component.html.erb +11 -0
- data/app/components/solid_web_ui/ui/stat_card_component.rb +24 -0
- data/app/components/solid_web_ui/ui/status_badge_component.rb +37 -0
- data/app/components/solid_web_ui/ui/table_component.html.erb +18 -0
- data/app/components/solid_web_ui/ui/table_component.rb +19 -0
- data/app/controllers/solid_web_ui/cable/application_controller.rb +14 -0
- data/app/controllers/solid_web_ui/cable/channels_controller.rb +13 -0
- data/app/controllers/solid_web_ui/cable/dashboard_controller.rb +13 -0
- data/app/controllers/solid_web_ui/cable/messages_controller.rb +18 -0
- data/app/controllers/solid_web_ui/cache/application_controller.rb +14 -0
- data/app/controllers/solid_web_ui/cache/dashboard_controller.rb +13 -0
- data/app/controllers/solid_web_ui/cache/entries_controller.rb +24 -0
- data/app/controllers/solid_web_ui/queue/application_controller.rb +17 -0
- data/app/controllers/solid_web_ui/queue/dashboard_controller.rb +18 -0
- data/app/controllers/solid_web_ui/queue/failed_executions_controller.rb +34 -0
- data/app/controllers/solid_web_ui/queue/jobs_controller.rb +24 -0
- data/app/controllers/solid_web_ui/queue/processes_controller.rb +9 -0
- data/app/controllers/solid_web_ui/queue/queues_controller.rb +27 -0
- data/app/controllers/solid_web_ui/queue/recurring_tasks_controller.rb +9 -0
- data/app/helpers/solid_web_ui/cable/application_helper.rb +22 -0
- data/app/helpers/solid_web_ui/cache/application_helper.rb +23 -0
- data/app/helpers/solid_web_ui/component_helper.rb +28 -0
- data/app/helpers/solid_web_ui/queue/application_helper.rb +38 -0
- data/app/views/layouts/solid_web_ui.html.erb +33 -0
- data/app/views/solid_web_ui/cable/channels/index.html.erb +12 -0
- data/app/views/solid_web_ui/cable/dashboard/index.html.erb +24 -0
- data/app/views/solid_web_ui/cache/dashboard/index.html.erb +15 -0
- data/app/views/solid_web_ui/cache/entries/index.html.erb +15 -0
- data/app/views/solid_web_ui/queue/dashboard/index.html.erb +13 -0
- data/app/views/solid_web_ui/queue/failed_executions/index.html.erb +26 -0
- data/app/views/solid_web_ui/queue/jobs/index.html.erb +23 -0
- data/app/views/solid_web_ui/queue/processes/index.html.erb +14 -0
- data/app/views/solid_web_ui/queue/queues/index.html.erb +25 -0
- data/app/views/solid_web_ui/queue/recurring_tasks/index.html.erb +17 -0
- data/lib/solid_web_ui/cable/engine.rb +13 -0
- data/lib/solid_web_ui/cable/routes.rb +7 -0
- data/lib/solid_web_ui/cable.rb +18 -0
- data/lib/solid_web_ui/cache/engine.rb +13 -0
- data/lib/solid_web_ui/cache/routes.rb +7 -0
- data/lib/solid_web_ui/cache.rb +17 -0
- data/lib/solid_web_ui/configurable.rb +20 -0
- data/lib/solid_web_ui/engine.rb +13 -0
- data/lib/solid_web_ui/paginator.rb +49 -0
- data/lib/solid_web_ui/queue/engine.rb +15 -0
- data/lib/solid_web_ui/queue/routes.rb +18 -0
- data/lib/solid_web_ui/queue.rb +19 -0
- data/lib/solid_web_ui/theme.rb +71 -0
- data/lib/solid_web_ui/version.rb +5 -0
- data/lib/solid_web_ui.rb +33 -0
- 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,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,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,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,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
|