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 +4 -4
- data/README.md +4 -3
- data/app/controllers/solid_stack_web/failed_jobs_controller.rb +17 -1
- data/app/controllers/solid_stack_web/history_controller.rb +15 -5
- data/app/controllers/solid_stack_web/jobs/selections_controller.rb +7 -5
- data/app/controllers/solid_stack_web/jobs_controller.rb +27 -11
- data/app/helpers/solid_stack_web/application_helper.rb +13 -0
- data/app/javascript/solid_stack_web/application.js +2 -0
- data/app/javascript/solid_stack_web/filter_persist_controller.js +28 -0
- data/app/views/solid_stack_web/failed_jobs/index.html.erb +4 -3
- data/app/views/solid_stack_web/history/index.html.erb +9 -4
- data/app/views/solid_stack_web/jobs/index.html.erb +25 -18
- data/lib/solid_stack_web/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f3dbb76a6507f2e2a0480bafe3b9e18ac8f34777563c853911dfaf7b7f549f23
|
|
4
|
+
data.tar.gz: 49f69c487ad4580d704ce122e46024ba96ff05c0c4b25166d64148128a28949d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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).
|
|
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
|
|
20
|
-
@search
|
|
21
|
-
@period
|
|
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(
|
|
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.
|
|
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:
|
|
14
|
-
q:
|
|
15
|
-
queue:
|
|
16
|
-
period:
|
|
17
|
-
priority:
|
|
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
|
|
58
|
-
@queue
|
|
59
|
-
@period
|
|
60
|
-
@priority
|
|
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).
|
|
79
|
-
scope = scope.
|
|
80
|
-
scope = scope.
|
|
81
|
-
scope = scope.
|
|
82
|
-
scope = scope.
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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"
|
|
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,
|
|
81
|
-
<%= f.hidden_field :q,
|
|
82
|
-
<%= f.hidden_field :queue,
|
|
83
|
-
<%= f.hidden_field :period,
|
|
84
|
-
<%= f.hidden_field :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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 %>
|
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.
|
|
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
|