solid_stack_web 1.1.0 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1717d75a5baabd8728f6f9877b5146849be9322a627cf295bd922690aa284592
4
- data.tar.gz: ae814ccee9aa89798575f914652ccfbb51137473961d7fd1a22b7a4461449102
3
+ metadata.gz: f3dbb76a6507f2e2a0480bafe3b9e18ac8f34777563c853911dfaf7b7f549f23
4
+ data.tar.gz: 49f69c487ad4580d704ce122e46024ba96ff05c0c4b25166d64148128a28949d
5
5
  SHA512:
6
- metadata.gz: e0d79705beafccb16561d20a28cac815607f2a202bc6d0839449c501f8d4f5bd93bd83ccb4a7c4be0d6b701ecb69b9aca8fa5ee2a3d4462712fc8399c935162a
7
- data.tar.gz: 2663a57cf04ca3aa26463732638020c63ce76d10046979dbd22baae8f5b529d3092e3597aa9c4a8b9b06b0a43b9a648e0a573349bca1f39d6f944fe4a8313884
6
+ metadata.gz: aa1598c4f1ee1a527ccc5b50240e5c866274e848ad2e7245dfe02ef6f05a6b1b889a7eb88ce80cad46f72f1b7f533a858b47cb1a3d130fd300c8bb32698d9842
7
+ data.tar.gz: 752a9f8176b029fa54f7c0dcef93081d4a6b25bc75dccebab67ae7561854ff3c5d1f9ae96804ac290841afbc38630b7329d0472b45fbaf7cbf95d3f342797b0b
data/README.md CHANGED
@@ -143,19 +143,20 @@ The dashboard is designed to be mounted behind your application's existing authe
143
143
  ### Features
144
144
 
145
145
  - **Overview dashboard** — live counts across all queue statuses; done (1h/24h), healthy/stale process counts, and optionally slow jobs (when `slow_job_threshold` is configured); 12-hour throughput sparkline and a 12-hour failures sparkline (red bars) with per-bar hover tooltips — failure spikes visible before clicking into the failed jobs list
146
- - **Job browser** — browse jobs by status (ready, scheduled, claimed, blocked) with filtering by job class, queue name, priority, and time period; **Discard All** bulk-discards every job matching the current filters in one request; **CSV export** downloads jobs respecting active filters
146
+ - **Job browser** — browse jobs by status (ready, scheduled, claimed, blocked) with filtering by job class, queue name, priority, and time period; sortable by class, queue, priority, and enqueued-at; sort state is preserved across filter and period changes; **Discard All** bulk-discards every job matching the current filters in one request; **CSV export** downloads jobs respecting active filters
147
147
  - **Bulk selection** — checkbox-select individual jobs for discard; select-all support
148
148
  - **Per-queue browser** — click any queue name or size to drill into its ready jobs with per-row and bulk discard; pause/resume controls on the queue page
149
149
  - **Queue depth sparklines** — Queues index shows a 12-hour depth chart per queue; each bar is the ready-job count at an hourly snapshot with an instant hover tooltip
150
150
  - **Job detail page** — full arguments (pretty-printed JSON), queue, priority, enqueued time, Active Job ID, concurrency key, scheduled/blocked-until metadata, and a Discard button
151
- - **Failed jobs** — list with retry / discard / bulk retry / bulk discard; **Failed job detail page** — full error, backtrace, and an inline JSON argument editor; submit to update arguments and retry in one action
151
+ - **Failed jobs** — list with retry / discard / bulk retry / bulk discard; sortable by class, queue, and failed-at; **Failed job detail page** — full error, backtrace, and an inline JSON argument editor; submit to update arguments and retry in one action
152
152
  - **Error frequency report** — `GET /failed_jobs/errors` groups all failed jobs by exception class and message prefix with a count and expandable sample backtrace; links through to a filtered list for each error group
153
153
  - **Scheduled job management** — "Run Now" and offset buttons (+1h / +24h / +7d) per row update the scheduled time inline via Turbo Stream; "Run All Now (N)" back-dates all matching executions at once
154
154
  - **Recurring task list** — enumerates all `SolidQueue::RecurringTask` records with cron schedule, job class or command, queue, next-run and last-run times, and a static/dynamic badge; each row has a "Run Now" button
155
155
  - **Performance statistics page** — `GET /stats` aggregates finished jobs by class name with execution count, avg, p50, p95, p99, std dev, min, and max duration; click any column header to sort; defaults to p95 descending; high std dev flags inconsistent jobs worth investigating
156
- - **Job history view** — paginated list of all finished jobs with class name, queue, duration, and finished-at time; filterable by queue (click a badge), class substring, and time period; CSV export respects active filters
156
+ - **Job history view** — paginated list of all finished jobs with class name, queue, duration, and finished-at time; filterable by queue (click a badge), class substring, and time period; sortable by class, queue, and finished-at; CSV export respects active filters
157
157
  - **Auto-refresh** — dashboard, jobs, processes, and history views poll automatically; pauses when the tab is hidden or a checkbox is checked; intervals configurable via `dashboard_refresh_interval` and `default_refresh_interval`
158
158
  - **Turbo Stream** job discard — removes the row inline without a full page reload
159
+ - **Sticky filter preferences** — last-used status, period, and queue filter saved to `localStorage`; a fresh visit to the jobs or history list with no URL params automatically restores the previous selection
159
160
  - **Dark mode** — toggle button in the header switches between light and dark palettes; preference persisted in `localStorage`; respects `prefers-color-scheme` on first visit
160
161
  - **Responsive layout** — stats cards, tables, and two-column grids adapt to narrow viewports; tables scroll horizontally rather than overflow; split page headers stack on small screens
161
162
  - **Empty-state improvements** — all list views show a contextual title and an actionable hint; search empty states include a "Clear search" link; filters-active history view offers "Clear filters"; processes and recurring tasks explain the next step
@@ -1,9 +1,12 @@
1
1
  module SolidStackWeb
2
2
  class FailedJobsController < ApplicationController
3
3
  def index
4
+ @sort = params[:sort].presence_in(sortable_columns) || "created_at"
5
+ @direction = params[:direction] == "asc" ? "asc" : "desc"
6
+
4
7
  respond_to do |format|
5
8
  format.html do
6
- scope = ::SolidQueue::FailedExecution.includes(:job).order(created_at: :desc)
9
+ scope = ::SolidQueue::FailedExecution.includes(:job).references(:job).order(sort_expression)
7
10
  @error_class = params[:error_class].presence
8
11
  scope = scope.where(id: ids_for_error_class(@error_class)) if @error_class
9
12
  @pagy, @executions = pagy(scope)
@@ -43,6 +46,19 @@ module SolidStackWeb
43
46
 
44
47
  private
45
48
 
49
+ def sortable_columns
50
+ %w[class_name queue_name created_at]
51
+ end
52
+
53
+ def sort_expression
54
+ sql_col = case @sort
55
+ when "class_name" then "solid_queue_jobs.class_name"
56
+ when "queue_name" then "solid_queue_jobs.queue_name"
57
+ else "solid_queue_failed_executions.created_at"
58
+ end
59
+ Arel.sql("#{sql_col} #{@direction == 'asc' ? 'ASC' : 'DESC'}")
60
+ end
61
+
46
62
  def ids_for_error_class(ec)
47
63
  ::SolidQueue::FailedExecution.pluck(:id, :error).filter_map do |id, raw|
48
64
  error = raw.is_a?(Hash) ? raw : JSON.parse(raw)
@@ -16,13 +16,23 @@ module SolidStackWeb
16
16
  private
17
17
 
18
18
  def set_filters
19
- @queue = params[:queue].presence
20
- @search = params[:q].presence
21
- @period = params[:period].presence_in(PERIOD_DURATIONS.keys)
19
+ @queue = params[:queue].presence
20
+ @search = params[:q].presence
21
+ @period = params[:period].presence_in(PERIOD_DURATIONS.keys)
22
+ @sort = params[:sort].presence_in(sortable_columns) || "finished_at"
23
+ @direction = params[:direction] == "asc" ? "asc" : "desc"
24
+ end
25
+
26
+ def sortable_columns
27
+ %w[class_name queue_name finished_at]
28
+ end
29
+
30
+ def sort_expression
31
+ Arel.sql("#{@sort} #{@direction == 'asc' ? 'ASC' : 'DESC'}")
22
32
  end
23
33
 
24
34
  def filtered_scope
25
- scope = SolidQueue::Job.where.not(finished_at: nil).order(finished_at: :desc)
35
+ scope = SolidQueue::Job.where.not(finished_at: nil).order(sort_expression)
26
36
  scope = scope.where(queue_name: @queue) if @queue.present?
27
37
  scope = scope.where("class_name LIKE ?", "%#{@search}%") if @search.present?
28
38
  scope = scope.where("finished_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
@@ -32,7 +42,7 @@ module SolidStackWeb
32
42
  def history_csv(scope)
33
43
  CSV.generate(headers: true) do |csv|
34
44
  csv << %w[id class_name queue_name duration_seconds finished_at]
35
- scope.order(finished_at: :desc).each do |job|
45
+ scope.each do |job|
36
46
  duration = job.finished_at && job.created_at ? (job.finished_at - job.created_at).round : nil
37
47
  csv << [job.id, job.class_name, job.queue_name, duration, job.finished_at.iso8601]
38
48
  end
@@ -10,11 +10,13 @@ module SolidStackWeb
10
10
  count = SolidQueue::Job.where(id: job_ids).destroy_all.size
11
11
 
12
12
  redirect_to jobs_path(
13
- status: status,
14
- q: params[:q].presence,
15
- queue: params[:queue].presence,
16
- period: params[:period].presence_in(PERIOD_DURATIONS.keys),
17
- priority: params[:priority].presence
13
+ status: status,
14
+ q: params[:q].presence,
15
+ queue: params[:queue].presence,
16
+ period: params[:period].presence_in(PERIOD_DURATIONS.keys),
17
+ priority: params[:priority].presence,
18
+ sort: params[:sort].presence,
19
+ direction: params[:direction].presence
18
20
  ), notice: "#{count} #{count == 1 ? "job" : "jobs"} discarded."
19
21
  rescue ArgumentError => e
20
22
  redirect_to jobs_path(status: params[:status]), alert: e.message
@@ -36,13 +36,13 @@ module SolidStackWeb
36
36
  @notice = "Job discarded."
37
37
 
38
38
  respond_to do |format|
39
- format.html { redirect_to jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority) }
39
+ format.html { redirect_to jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority, sort: @sort, direction: @direction) }
40
40
  format.turbo_stream
41
41
  end
42
42
  else
43
43
  job_ids = filtered_scope.pluck(:job_id)
44
44
  count = SolidQueue::Job.where(id: job_ids).destroy_all.size
45
- redirect_to jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
45
+ redirect_to jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority, sort: @sort, direction: @direction),
46
46
  notice: "#{count} #{count == 1 ? "job" : "jobs"} discarded."
47
47
  end
48
48
  end
@@ -54,10 +54,26 @@ module SolidStackWeb
54
54
  end
55
55
 
56
56
  def set_filters
57
- @search = params[:q].presence
58
- @queue = params[:queue].presence
59
- @period = params[:period].presence_in(PERIOD_DURATIONS.keys)
60
- @priority = params[:priority].presence
57
+ @search = params[:q].presence
58
+ @queue = params[:queue].presence
59
+ @period = params[:period].presence_in(PERIOD_DURATIONS.keys)
60
+ @priority = params[:priority].presence
61
+ @sort = params[:sort].presence_in(sortable_columns) || "created_at"
62
+ @direction = params[:direction] == "asc" ? "asc" : "desc"
63
+ end
64
+
65
+ def sortable_columns
66
+ %w[class_name queue_name priority created_at]
67
+ end
68
+
69
+ def sort_expression
70
+ sql_col = case @sort
71
+ when "class_name" then "solid_queue_jobs.class_name"
72
+ when "queue_name" then "solid_queue_jobs.queue_name"
73
+ when "priority" then "solid_queue_jobs.priority"
74
+ else "#{Job::EXECUTION_MODELS[@status].quoted_table_name}.created_at"
75
+ end
76
+ Arel.sql("#{sql_col} #{@direction == 'asc' ? 'ASC' : 'DESC'}")
61
77
  end
62
78
 
63
79
  def require_discardable
@@ -75,11 +91,11 @@ module SolidStackWeb
75
91
  end
76
92
 
77
93
  def filtered_scope
78
- scope = Job::EXECUTION_MODELS[@status].includes(:job).order(created_at: :desc)
79
- scope = scope.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
80
- scope = scope.references(:job).where("solid_queue_jobs.queue_name = ?", @queue) if @queue.present?
81
- scope = scope.references(:job).where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
82
- scope = scope.references(:job).where("solid_queue_jobs.priority = ?", @priority.to_i) if @priority.present?
94
+ scope = Job::EXECUTION_MODELS[@status].includes(:job).references(:job).order(sort_expression)
95
+ scope = scope.where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
96
+ scope = scope.where("solid_queue_jobs.queue_name = ?", @queue) if @queue.present?
97
+ scope = scope.where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
98
+ scope = scope.where("solid_queue_jobs.priority = ?", @priority.to_i) if @priority.present?
83
99
  scope
84
100
  end
85
101
  end
@@ -87,6 +87,19 @@ module SolidStackWeb
87
87
  end
88
88
  end
89
89
 
90
+ def sort_header_th(label, col, url_proc, current_sort:, current_dir:)
91
+ is_active = current_sort == col
92
+ next_dir = (is_active && current_dir == "desc") ? "asc" : "desc"
93
+ indicator = is_active ? content_tag(:span, current_dir == "desc" ? "↓" : "↑", class: "sqw-sort-indicator") : nil
94
+ tag_opts = { scope: "col" }
95
+ tag_opts[:"aria-sort"] = is_active ? (current_dir == "asc" ? "ascending" : "descending") : nil if is_active
96
+ content_tag(:th, **tag_opts) do
97
+ link_to(url_proc.call(sort: col, direction: next_dir)) do
98
+ safe_join([label, indicator].compact)
99
+ end
100
+ end
101
+ end
102
+
90
103
  def failed_job_sparkline_svg(sparkline)
91
104
  build_sparkline_svg(sparkline, aria_label: "Failed jobs over the last 12 hours") do |count, i|
92
105
  hours_ago = SolidStackWeb::FailedJobSparkline::HOURS - i
@@ -1,5 +1,6 @@
1
1
  import "@hotwired/turbo"
2
2
  import { Application } from "@hotwired/stimulus"
3
+ import FilterPersistController from "solid_stack_web/filter_persist_controller"
3
4
  import RefreshController from "solid_stack_web/refresh_controller"
4
5
  import SearchController from "solid_stack_web/search_controller"
5
6
  import SelectionController from "solid_stack_web/selection_controller"
@@ -8,6 +9,7 @@ import ThemeController from "solid_stack_web/theme_controller"
8
9
  import TimestampController from "solid_stack_web/timestamp_controller"
9
10
 
10
11
  const application = Application.start()
12
+ application.register("filter-persist", FilterPersistController)
11
13
  application.register("refresh", RefreshController)
12
14
  application.register("search", SearchController)
13
15
  application.register("selection", SelectionController)
@@ -0,0 +1,28 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ const PERSIST_KEYS = ["status", "period", "queue"]
4
+
5
+ export default class extends Controller {
6
+ static values = { key: String }
7
+
8
+ connect() {
9
+ const params = new URLSearchParams(window.location.search)
10
+
11
+ if (params.toString()) {
12
+ const toSave = new URLSearchParams()
13
+ PERSIST_KEYS.forEach(k => { if (params.get(k)) toSave.set(k, params.get(k)) })
14
+ if (toSave.toString()) this.write(toSave.toString())
15
+ } else {
16
+ const stored = this.read()
17
+ if (stored) window.location.replace(`${window.location.pathname}?${stored}`)
18
+ }
19
+ }
20
+
21
+ read() {
22
+ try { return localStorage.getItem(this.keyValue) } catch (e) { return null }
23
+ }
24
+
25
+ write(value) {
26
+ try { localStorage.setItem(this.keyValue, value) } catch (e) { /* ignore */ }
27
+ }
28
+ }
@@ -41,10 +41,11 @@
41
41
  <th scope="col"><input type="checkbox" class="sqw-checkbox" aria-label="Select all"
42
42
  data-selection-target="selectAll"
43
43
  data-action="change->selection#selectAll"></th>
44
- <th scope="col">Job Class</th>
45
- <th scope="col">Queue</th>
44
+ <% sort_url = ->(p) { failed_jobs_path(error_class: @error_class, **p) } %>
45
+ <%= sort_header_th("Job Class", "class_name", sort_url, current_sort: @sort, current_dir: @direction) %>
46
+ <%= sort_header_th("Queue", "queue_name", sort_url, current_sort: @sort, current_dir: @direction) %>
46
47
  <th scope="col">Error</th>
47
- <th scope="col">Failed At</th>
48
+ <%= sort_header_th("Failed At", "created_at", sort_url, current_sort: @sort, current_dir: @direction) %>
48
49
  <th scope="col"><span class="sqw-sr-only">Actions</span></th>
49
50
  </tr>
50
51
  </thead>
@@ -10,11 +10,15 @@
10
10
  </div>
11
11
  </div>
12
12
 
13
- <form class="sqw-filters" action="<%= history_path %>" method="get" data-controller="search">
13
+ <form class="sqw-filters" action="<%= history_path %>" method="get"
14
+ data-controller="search filter-persist"
15
+ data-filter-persist-key-value="sqw-history-filters">
14
16
  <% if @queue.present? %>
15
17
  <input type="hidden" name="queue" value="<%= @queue %>">
16
18
  <% end %>
17
19
  <input type="hidden" name="period" value="<%= @period %>">
20
+ <input type="hidden" name="sort" value="<%= @sort %>">
21
+ <input type="hidden" name="direction" value="<%= @direction %>">
18
22
  <input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
19
23
  placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
20
24
  data-action="input->search#filter">
@@ -44,11 +48,12 @@
44
48
  <div class="sqw-detail-card">
45
49
  <table class="sqw-table">
46
50
  <thead>
51
+ <% sort_url = ->(p) { history_path(queue: @queue, q: @search, period: @period, **p) } %>
47
52
  <tr>
48
- <th scope="col">Job Class</th>
49
- <th scope="col">Queue</th>
53
+ <%= sort_header_th("Job Class", "class_name", sort_url, current_sort: @sort, current_dir: @direction) %>
54
+ <%= sort_header_th("Queue", "queue_name", sort_url, current_sort: @sort, current_dir: @direction) %>
50
55
  <th scope="col">Duration</th>
51
- <th scope="col">Finished At</th>
56
+ <%= sort_header_th("Finished At", "finished_at", sort_url, current_sort: @sort, current_dir: @direction) %>
52
57
  </tr>
53
58
  </thead>
54
59
  <tbody>
@@ -1,7 +1,7 @@
1
1
  <div class="sqw-page-header sqw-page-header--split">
2
2
  <h1 class="sqw-page-title">Jobs</h1>
3
3
  <div class="sqw-header-actions">
4
- <%= link_to "Export CSV", jobs_path(format: :csv, status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
4
+ <%= link_to "Export CSV", jobs_path(format: :csv, status: @status, q: @search, queue: @queue, period: @period, priority: @priority, sort: @sort, direction: @direction),
5
5
  class: "sqw-btn sqw-btn--muted sqw-btn--sm", data: { turbo: false } %>
6
6
  <% if @status == "scheduled" && @executions&.any? %>
7
7
  <%= button_to "Run All Now (#{@pagy.count})",
@@ -24,7 +24,7 @@
24
24
 
25
25
  <div class="sqw-tabs">
26
26
  <% SolidStackWeb::Job::TAB_LABELS.each do |status, label| %>
27
- <%= link_to label, jobs_path(status: status, q: @search, queue: @queue, period: @period, priority: @priority),
27
+ <%= link_to label, jobs_path(status: status, q: @search, queue: @queue, period: @period, priority: @priority, sort: @sort, direction: @direction),
28
28
  class: "sqw-tab #{"sqw-tab--active" if @status == status}" %>
29
29
  <% end %>
30
30
  </div>
@@ -32,9 +32,13 @@
32
32
  <%= turbo_frame_tag "sqw-jobs-filter",
33
33
  data: { turbo_action: "advance", controller: "refresh",
34
34
  refresh_interval_value: SolidStackWeb.default_refresh_interval } do %>
35
- <form class="sqw-filters" action="<%= jobs_path %>" method="get" data-controller="search">
35
+ <form class="sqw-filters" action="<%= jobs_path %>" method="get"
36
+ data-controller="search filter-persist"
37
+ data-filter-persist-key-value="sqw-jobs-filters">
36
38
  <%= hidden_field_tag :status, @status %>
37
39
  <%= hidden_field_tag :period, @period %>
40
+ <%= hidden_field_tag :sort, @sort %>
41
+ <%= hidden_field_tag :direction, @direction %>
38
42
  <input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
39
43
  placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
40
44
  data-action="input->search#filter">
@@ -57,16 +61,16 @@
57
61
  </select>
58
62
  <% end %>
59
63
  <% if @search.present? || @queue.present? || @priority.present? %>
60
- <%= link_to "Clear", jobs_path(status: @status, period: @period), class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
64
+ <%= link_to "Clear", jobs_path(status: @status, period: @period, sort: @sort, direction: @direction), class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
61
65
  <% end %>
62
66
  <div class="sqw-period-filter" role="group" aria-label="Time period">
63
- <%= link_to "All", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority),
67
+ <%= link_to "All", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, sort: @sort, direction: @direction),
64
68
  class: "sqw-period-btn #{"sqw-period-btn--active" if @period.nil?}" %>
65
- <%= link_to "1h", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "1h"),
69
+ <%= link_to "1h", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "1h", sort: @sort, direction: @direction),
66
70
  class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "1h"}" %>
67
- <%= link_to "24h", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "24h"),
71
+ <%= link_to "24h", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "24h", sort: @sort, direction: @direction),
68
72
  class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "24h"}" %>
69
- <%= link_to "7d", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "7d"),
73
+ <%= link_to "7d", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "7d", sort: @sort, direction: @direction),
70
74
  class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "7d"}" %>
71
75
  </div>
72
76
  </form>
@@ -77,11 +81,13 @@
77
81
  <% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>
78
82
  <%= form_with url: job_selection_path, method: :delete, id: "job-selection-form",
79
83
  data: { turbo_confirm: "Discard selected jobs? This cannot be undone." } do |f| %>
80
- <%= f.hidden_field :status, value: @status %>
81
- <%= f.hidden_field :q, value: @search %>
82
- <%= f.hidden_field :queue, value: @queue %>
83
- <%= f.hidden_field :period, value: @period %>
84
- <%= f.hidden_field :priority, value: @priority %>
84
+ <%= f.hidden_field :status, value: @status %>
85
+ <%= f.hidden_field :q, value: @search %>
86
+ <%= f.hidden_field :queue, value: @queue %>
87
+ <%= f.hidden_field :period, value: @period %>
88
+ <%= f.hidden_field :priority, value: @priority %>
89
+ <%= f.hidden_field :sort, value: @sort %>
90
+ <%= f.hidden_field :direction, value: @direction %>
85
91
  <% end %>
86
92
 
87
93
  <div class="sqw-selection-bar" data-selection-target="bar" style="display: none;">
@@ -100,10 +106,11 @@
100
106
  data-selection-target="selectAll"
101
107
  data-action="change->selection#selectAll"></th>
102
108
  <% end %>
103
- <th scope="col">Job Class</th>
104
- <th scope="col">Queue</th>
105
- <th scope="col">Priority</th>
106
- <th scope="col">Enqueued At</th>
109
+ <% sort_url = ->(p) { jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority, **p) } %>
110
+ <%= sort_header_th("Job Class", "class_name", sort_url, current_sort: @sort, current_dir: @direction) %>
111
+ <%= sort_header_th("Queue", "queue_name", sort_url, current_sort: @sort, current_dir: @direction) %>
112
+ <%= sort_header_th("Priority", "priority", sort_url, current_sort: @sort, current_dir: @direction) %>
113
+ <%= sort_header_th("Enqueued At", "created_at", sort_url, current_sort: @sort, current_dir: @direction) %>
107
114
  <% if @status == "scheduled" %><th scope="col">Scheduled At</th><% end %>
108
115
  <th scope="col"><span class="sqw-sr-only">Actions</span></th>
109
116
  </tr>
@@ -140,7 +147,7 @@
140
147
  <% end %>
141
148
  <% end %>
142
149
  <% if %w[ready scheduled blocked].include?(@status) %>
143
- <%= button_to "Discard", job_path(execution, status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
150
+ <%= button_to "Discard", job_path(execution, status: @status, q: @search, queue: @queue, period: @period, priority: @priority, sort: @sort, direction: @direction),
144
151
  method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
145
152
  data: { turbo_confirm: "Discard this job?" } %>
146
153
  <% end %>
@@ -1,3 +1,3 @@
1
1
  module SolidStackWeb
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_stack_web
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -176,6 +176,7 @@ files:
176
176
  - app/controllers/solid_stack_web/stats_controller.rb
177
177
  - app/helpers/solid_stack_web/application_helper.rb
178
178
  - app/javascript/solid_stack_web/application.js
179
+ - app/javascript/solid_stack_web/filter_persist_controller.js
179
180
  - app/javascript/solid_stack_web/refresh_controller.js
180
181
  - app/javascript/solid_stack_web/search_controller.js
181
182
  - app/javascript/solid_stack_web/selection_controller.js