solid_queue_web 0.6.0 → 0.7.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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -2
  3. data/Rakefile +2 -2
  4. data/app/assets/stylesheets/solid_queue_web/_01_base.css +41 -0
  5. data/app/assets/stylesheets/solid_queue_web/_02_layout.css +105 -0
  6. data/app/assets/stylesheets/solid_queue_web/_03_stats.css +49 -0
  7. data/app/assets/stylesheets/solid_queue_web/_04_table.css +52 -0
  8. data/app/assets/stylesheets/solid_queue_web/_05_badges.css +27 -0
  9. data/app/assets/stylesheets/solid_queue_web/_06_buttons.css +38 -0
  10. data/app/assets/stylesheets/solid_queue_web/_07_forms.css +103 -0
  11. data/app/assets/stylesheets/solid_queue_web/_08_detail.css +84 -0
  12. data/app/assets/stylesheets/solid_queue_web/_09_pagination.css +27 -0
  13. data/app/assets/stylesheets/solid_queue_web/_10_responsive.css +73 -0
  14. data/app/assets/stylesheets/solid_queue_web/_11_throughput.css +68 -0
  15. data/app/assets/stylesheets/solid_queue_web/application.css +1 -617
  16. data/app/controllers/solid_queue_web/dashboard_controller.rb +12 -0
  17. data/app/controllers/solid_queue_web/failed_jobs_controller.rb +1 -1
  18. data/app/controllers/solid_queue_web/history_controller.rb +16 -0
  19. data/app/controllers/solid_queue_web/jobs_controller.rb +1 -1
  20. data/app/controllers/solid_queue_web/queues/jobs_controller.rb +1 -1
  21. data/app/controllers/solid_queue_web/queues_controller.rb +15 -0
  22. data/app/helpers/solid_queue_web/application_helper.rb +15 -1
  23. data/app/javascript/solid_queue_web/refresh_controller.js +3 -2
  24. data/app/views/layouts/solid_queue_web/application.html.erb +1 -0
  25. data/app/views/solid_queue_web/dashboard/index.html.erb +38 -0
  26. data/app/views/solid_queue_web/failed_jobs/index.html.erb +0 -1
  27. data/app/views/solid_queue_web/history/index.html.erb +67 -0
  28. data/app/views/solid_queue_web/jobs/index.html.erb +0 -1
  29. data/app/views/solid_queue_web/queues/index.html.erb +15 -1
  30. data/config/routes.rb +9 -8
  31. data/lib/solid_queue_web/version.rb +1 -1
  32. metadata +14 -1
@@ -1,6 +1,6 @@
1
1
  module SolidQueueWeb
2
2
  class JobsController < ApplicationController
3
- before_action :set_status, only: [ :destroy, :discard_all, :discard_selected ]
3
+ before_action :set_status, only: [:destroy, :discard_all, :discard_selected]
4
4
 
5
5
  def index
6
6
  @status = params[:status].presence_in(Job::STATUSES) || "ready"
@@ -2,7 +2,7 @@ module SolidQueueWeb
2
2
  module Queues
3
3
  class JobsController < ApplicationController
4
4
  before_action :set_queue
5
- before_action :set_status, only: [ :destroy, :discard_all ]
5
+ before_action :set_status, only: [:destroy, :discard_all]
6
6
 
7
7
  def index
8
8
  @status = params[:status].presence_in(Job::STATUSES) || "ready"
@@ -2,6 +2,21 @@ module SolidQueueWeb
2
2
  class QueuesController < ApplicationController
3
3
  def index
4
4
  @queues = SolidQueue::Queue.all.sort_by(&:name)
5
+
6
+ now = Time.current
7
+ @completed_24h = SolidQueue::Job
8
+ .where(finished_at: 24.hours.ago..now)
9
+ .group(:queue_name)
10
+ .count
11
+ @failed_24h = SolidQueue::FailedExecution
12
+ .joins(:job)
13
+ .where(created_at: 24.hours.ago..now)
14
+ .group("solid_queue_jobs.queue_name")
15
+ .count
16
+ @oldest_ready = SolidQueue::ReadyExecution
17
+ .joins(:job)
18
+ .group("solid_queue_jobs.queue_name")
19
+ .minimum("solid_queue_jobs.created_at")
5
20
  end
6
21
 
7
22
  def pause
@@ -1,8 +1,22 @@
1
1
  module SolidQueueWeb
2
2
  module ApplicationHelper
3
3
  def inline_styles
4
- css = SolidQueueWeb::Engine.root.join("app/assets/stylesheets/solid_queue_web/application.css").read
4
+ dir = SolidQueueWeb::Engine.root.join("app/assets/stylesheets/solid_queue_web")
5
+ css = dir.glob("_*.css").sort.map(&:read).join("\n")
5
6
  content_tag(:style, css.html_safe)
6
7
  end
8
+
9
+ def format_duration(seconds)
10
+ s = seconds.to_i
11
+ return "< 1s" if s < 1
12
+
13
+ if s < 60
14
+ "#{s}s"
15
+ elsif s < 3600
16
+ "#{s / 60}m #{s % 60}s"
17
+ else
18
+ "#{s / 3600}h #{(s % 3600) / 60}m"
19
+ end
20
+ end
7
21
  end
8
22
  end
@@ -23,7 +23,8 @@ export default class extends Controller {
23
23
 
24
24
  async _reload() {
25
25
  clearTimeout(this._timer)
26
- if (!document.hidden) {
26
+ const hasSelection = this.element.querySelector("input[type='checkbox']:checked")
27
+ if (!document.hidden && !hasSelection) {
27
28
  try {
28
29
  const response = await fetch(window.location.href, {
29
30
  headers: { "Turbo-Frame": this.element.id, Accept: "text/html" }
@@ -44,7 +45,7 @@ export default class extends Controller {
44
45
  _onVisibilityChange() {
45
46
  if (document.hidden) {
46
47
  clearTimeout(this._timer)
47
- } else {
48
+ } else if (!this.element.querySelector("input[type='checkbox']:checked")) {
48
49
  this._reload()
49
50
  }
50
51
  }
@@ -26,6 +26,7 @@
26
26
  <li><%= link_to "Dashboard", root_path, class: current_page?(root_path) ? "active" : "", aria: { current: current_page?(root_path) ? "page" : nil } %></li>
27
27
  <li><%= link_to "Queues", queues_path, class: current_page?(queues_path) ? "active" : "", aria: { current: current_page?(queues_path) ? "page" : nil } %></li>
28
28
  <li><%= link_to "Jobs", jobs_path, class: current_page?(jobs_path) ? "active" : "", aria: { current: current_page?(jobs_path) ? "page" : nil } %></li>
29
+ <li><%= link_to "History", history_path, class: current_page?(history_path) ? "active" : "", aria: { current: current_page?(history_path) ? "page" : nil } %></li>
29
30
  <li><%= link_to "Failed", failed_jobs_path, class: current_page?(failed_jobs_path) ? "active" : "", aria: { current: current_page?(failed_jobs_path) ? "page" : nil } %></li>
30
31
  <li><%= link_to "Recurring", recurring_tasks_path, class: current_page?(recurring_tasks_path) ? "active" : "", aria: { current: current_page?(recurring_tasks_path) ? "page" : nil } %></li>
31
32
  <li><%= link_to "Processes", processes_path, class: current_page?(processes_path) ? "active" : "", aria: { current: current_page?(processes_path) ? "page" : nil } %></li>
@@ -34,6 +34,44 @@
34
34
  <div class="sqd-stat__value"><%= @stats[:processes] %></div>
35
35
  <div class="sqd-stat__label">Processes</div>
36
36
  <% end %>
37
+ <%= link_to history_path(period: "1h"), class: "sqd-stat sqd-stat--done sqd-stat--link" do %>
38
+ <div class="sqd-stat__value"><%= @throughput[:completed_1h] %></div>
39
+ <div class="sqd-stat__label">Done (1h)</div>
40
+ <% end %>
41
+ <%= link_to history_path(period: "24h"), class: "sqd-stat sqd-stat--done sqd-stat--link" do %>
42
+ <div class="sqd-stat__value"><%= @throughput[:completed_24h] %></div>
43
+ <div class="sqd-stat__label">Done (24h)</div>
44
+ <% end %>
45
+ </div>
46
+
47
+ <% max_val = [@sparkline.max, 1].max %>
48
+ <div class="sqd-card" style="margin-bottom: 1rem;">
49
+ <div class="sqd-card__header">
50
+ <span class="sqd-card__title">Throughput &mdash; Last 12 Hours</span>
51
+ <div class="sqd-throughput__summary">
52
+ <span>1h: <strong><%= @throughput[:completed_1h] %></strong></span>
53
+ <span>24h: <strong><%= @throughput[:completed_24h] %></strong></span>
54
+ </div>
55
+ </div>
56
+ <% if @throughput[:completed_24h] == 0 %>
57
+ <div class="sqd-sparkline__empty">No completed jobs in the last 24 hours</div>
58
+ <% else %>
59
+ <div class="sqd-sparkline" aria-label="Jobs completed per hour over the last 12 hours">
60
+ <% @sparkline.each_with_index do |count, i| %>
61
+ <% pct = (count.to_f / max_val * 100).round %>
62
+ <% hour_start = (12 - i).hours.ago %>
63
+ <% show_tick = [0, 3, 6, 9, 11].include?(i) %>
64
+ <div class="sqd-sparkline__col">
65
+ <div class="sqd-sparkline__bar-wrap">
66
+ <div class="sqd-sparkline__bar"
67
+ style="height: <%= [pct, 3].max %>%"
68
+ title="<%= hour_start.strftime('%-I%p').downcase %>: <%= count %> <%= "job".pluralize(count) %>"></div>
69
+ </div>
70
+ <div class="sqd-sparkline__tick"><%= show_tick ? (i == 11 ? "now" : hour_start.strftime("%-I%p").downcase) : "" %></div>
71
+ </div>
72
+ <% end %>
73
+ </div>
74
+ <% end %>
37
75
  </div>
38
76
 
39
77
  <div style="display:grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
@@ -24,7 +24,6 @@
24
24
  <input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
25
25
  placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
26
26
  data-action="input->search#filter">
27
- <button type="submit" class="sqd-btn sqd-btn--muted">Search</button>
28
27
  <% if @search.present? %>
29
28
  <%= link_to "Clear", failed_jobs_path(queue: @queue, period: @period), class: "sqd-btn sqd-btn--muted" %>
30
29
  <% end %>
@@ -0,0 +1,67 @@
1
+ <%= turbo_frame_tag "history-table", data: { controller: "refresh", refresh_interval_value: 10000 } do %>
2
+ <div class="sqd-page-header">
3
+ <h1 class="sqd-page-title">Job History</h1>
4
+ </div>
5
+
6
+ <form class="sqd-search" action="<%= history_path %>" method="get" data-controller="search">
7
+ <% if @queue.present? %>
8
+ <input type="hidden" name="queue" value="<%= @queue %>">
9
+ <% end %>
10
+ <input type="hidden" name="period" value="<%= @period %>">
11
+ <input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
12
+ placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
13
+ data-action="input->search#filter">
14
+ <% if @search.present? %>
15
+ <%= link_to "Clear", history_path(queue: @queue, period: @period), class: "sqd-btn sqd-btn--muted" %>
16
+ <% end %>
17
+ <div class="sqd-period-filter" role="group" aria-label="Time period">
18
+ <%= link_to "All", history_path(queue: @queue, q: @search), class: @period.nil? ? "active" : "", aria: { current: @period.nil? ? "true" : nil } %>
19
+ <%= link_to "1h", history_path(queue: @queue, q: @search, period: "1h"), class: @period == "1h" ? "active" : "", aria: { current: @period == "1h" ? "true" : nil } %>
20
+ <%= link_to "24h", history_path(queue: @queue, q: @search, period: "24h"), class: @period == "24h" ? "active" : "", aria: { current: @period == "24h" ? "true" : nil } %>
21
+ <%= link_to "7d", history_path(queue: @queue, q: @search, period: "7d"), class: @period == "7d" ? "active" : "", aria: { current: @period == "7d" ? "true" : nil } %>
22
+ </div>
23
+ </form>
24
+
25
+ <% if @queue.present? %>
26
+ <p style="margin-top: 0.5rem; font-size: 13px; color: var(--muted);">
27
+ Filtering by queue: <strong><%= @queue %></strong> &mdash;
28
+ <%= link_to "Clear filter", history_path(q: @search, period: @period) %>
29
+ </p>
30
+ <% end %>
31
+
32
+ <% if @jobs.any? %>
33
+ <div class="sqd-card">
34
+ <table>
35
+ <thead>
36
+ <tr>
37
+ <th scope="col">Job Class</th>
38
+ <th scope="col">Queue</th>
39
+ <th scope="col">Duration</th>
40
+ <th scope="col">Finished At</th>
41
+ </tr>
42
+ </thead>
43
+ <tbody>
44
+ <% @jobs.each do |job| %>
45
+ <tr>
46
+ <td><%= link_to job.class_name, job_path(job) %></td>
47
+ <td>
48
+ <%= link_to job.queue_name, history_path(queue: job.queue_name, q: @search, period: @period),
49
+ class: "sqd-mono", style: "color: inherit;" %>
50
+ </td>
51
+ <td class="sqd-mono"><%= format_duration(job.finished_at - job.created_at) %></td>
52
+ <td class="sqd-mono"><%= job.finished_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
53
+ </tr>
54
+ <% end %>
55
+ </tbody>
56
+ </table>
57
+ </div>
58
+ <% else %>
59
+ <div class="sqd-card">
60
+ <div class="sqd-empty">No finished jobs found.</div>
61
+ </div>
62
+ <% end %>
63
+
64
+ <% if @pagy.last > 1 %>
65
+ <%= @pagy.series_nav.html_safe %>
66
+ <% end %>
67
+ <% end %>
@@ -28,7 +28,6 @@
28
28
  <input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
29
29
  placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
30
30
  data-action="input->search#filter">
31
- <button type="submit" class="sqd-btn sqd-btn--muted">Search</button>
32
31
  <% if @search.present? %>
33
32
  <%= link_to "Clear", jobs_path(status: @status, period: @period), class: "sqd-btn sqd-btn--muted" %>
34
33
  <% end %>
@@ -10,6 +10,8 @@
10
10
  <th scope="col">Name</th>
11
11
  <th scope="col">Size</th>
12
12
  <th scope="col">Latency</th>
13
+ <th scope="col">Done (24h)</th>
14
+ <th scope="col">Failed (24h)</th>
13
15
  <th scope="col">Status</th>
14
16
  <th scope="col"><span class="sqd-sr-only">Actions</span></th>
15
17
  </tr>
@@ -19,7 +21,19 @@
19
21
  <tr>
20
22
  <td class="sqd-mono"><%= queue.name %></td>
21
23
  <td><%= queue.size %></td>
22
- <td><%= queue.human_latency %></td>
24
+ <td>
25
+ <% if (oldest = @oldest_ready[queue.name]) %>
26
+ <% age = Time.current - oldest %>
27
+ <% latency_color = age > 86_400 ? "var(--danger)" : age > 3_600 ? "var(--warning)" : "inherit" %>
28
+ <abbr title="<%= oldest.strftime("%Y-%m-%d %H:%M:%S UTC") %>">
29
+ <span style="color: <%= latency_color %>"><%= format_duration(age) %></span>
30
+ </abbr>
31
+ <% else %>
32
+ <span style="color: var(--muted)">—</span>
33
+ <% end %>
34
+ </td>
35
+ <td style="color: var(--success);"><%= @completed_24h[queue.name] || 0 %></td>
36
+ <td style="color: <%= (@failed_24h[queue.name] || 0) > 0 ? "var(--danger)" : "inherit" %>;"><%= @failed_24h[queue.name] || 0 %></td>
23
37
  <td>
24
38
  <% if queue.paused? %>
25
39
  <span class="sqd-badge sqd-badge--paused">Paused</span>
data/config/routes.rb CHANGED
@@ -2,15 +2,16 @@ SolidQueueWeb::Engine.routes.draw do
2
2
  root to: "dashboard#index"
3
3
 
4
4
  get "search", to: "search#index", as: :search
5
+ get "history", to: "history#index", as: :history
5
6
 
6
- resources :recurring_tasks, only: [ :index ]
7
- resources :processes, only: [ :index ]
8
- resources :queues, only: [ :index ], param: :name do
7
+ resources :recurring_tasks, only: [:index]
8
+ resources :processes, only: [:index]
9
+ resources :queues, only: [:index], param: :name do
9
10
  member do
10
11
  post :pause
11
12
  post :resume
12
13
  end
13
- resources :jobs, path: "list", only: [ :index, :destroy ], controller: "queues/jobs" do
14
+ resources :jobs, path: "list", only: [:index, :destroy], controller: "queues/jobs" do
14
15
  collection do
15
16
  post :discard_all
16
17
  end
@@ -19,16 +20,16 @@ SolidQueueWeb::Engine.routes.draw do
19
20
 
20
21
  # Singular selection resources must be defined before the member routes of their
21
22
  # parent resources, otherwise DELETE /list/selection matches /list/:id first.
22
- resource :job_selection, path: "list/selection", only: [ :destroy ], controller: "jobs/selections"
23
- resources :jobs, path: "list", only: [ :index, :show, :destroy ] do
23
+ resource :job_selection, path: "list/selection", only: [:destroy], controller: "jobs/selections"
24
+ resources :jobs, path: "list", only: [:index, :show, :destroy] do
24
25
  collection do
25
26
  post :discard_all
26
27
  end
27
28
  end
28
29
 
29
- resource :failed_job_selection, path: "failed_jobs/selection", only: [ :create, :destroy ],
30
+ resource :failed_job_selection, path: "failed_jobs/selection", only: [:create, :destroy],
30
31
  controller: "failed_jobs/selections"
31
- resources :failed_jobs, only: [ :index, :destroy ] do
32
+ resources :failed_jobs, only: [:index, :destroy] do
32
33
  collection do
33
34
  post :retry_all
34
35
  post :discard_all
@@ -1,3 +1,3 @@
1
1
  module SolidQueueWeb
2
- VERSION = "0.6.0"
2
+ VERSION = "0.7.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_queue_web
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -91,11 +91,23 @@ files:
91
91
  - MIT-LICENSE
92
92
  - README.md
93
93
  - Rakefile
94
+ - app/assets/stylesheets/solid_queue_web/_01_base.css
95
+ - app/assets/stylesheets/solid_queue_web/_02_layout.css
96
+ - app/assets/stylesheets/solid_queue_web/_03_stats.css
97
+ - app/assets/stylesheets/solid_queue_web/_04_table.css
98
+ - app/assets/stylesheets/solid_queue_web/_05_badges.css
99
+ - app/assets/stylesheets/solid_queue_web/_06_buttons.css
100
+ - app/assets/stylesheets/solid_queue_web/_07_forms.css
101
+ - app/assets/stylesheets/solid_queue_web/_08_detail.css
102
+ - app/assets/stylesheets/solid_queue_web/_09_pagination.css
103
+ - app/assets/stylesheets/solid_queue_web/_10_responsive.css
104
+ - app/assets/stylesheets/solid_queue_web/_11_throughput.css
94
105
  - app/assets/stylesheets/solid_queue_web/application.css
95
106
  - app/controllers/solid_queue_web/application_controller.rb
96
107
  - app/controllers/solid_queue_web/dashboard_controller.rb
97
108
  - app/controllers/solid_queue_web/failed_jobs/selections_controller.rb
98
109
  - app/controllers/solid_queue_web/failed_jobs_controller.rb
110
+ - app/controllers/solid_queue_web/history_controller.rb
99
111
  - app/controllers/solid_queue_web/jobs/selections_controller.rb
100
112
  - app/controllers/solid_queue_web/jobs_controller.rb
101
113
  - app/controllers/solid_queue_web/processes_controller.rb
@@ -114,6 +126,7 @@ files:
114
126
  - app/views/layouts/solid_queue_web/application.html.erb
115
127
  - app/views/solid_queue_web/dashboard/index.html.erb
116
128
  - app/views/solid_queue_web/failed_jobs/index.html.erb
129
+ - app/views/solid_queue_web/history/index.html.erb
117
130
  - app/views/solid_queue_web/jobs/destroy.turbo_stream.erb
118
131
  - app/views/solid_queue_web/jobs/index.html.erb
119
132
  - app/views/solid_queue_web/jobs/show.html.erb