diff --git a/CHANGELOG.md b/CHANGELOG.md index dec7b91..30902c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Sticky filter preferences — last-used status tab (Jobs) and time-period pill (Jobs, Failed Jobs, History) are persisted to `localStorage`; revisiting a page via the nav link restores the previous filter automatically; clicking "All" clears the saved period; implemented as a lightweight Stimulus controller (`FiltersController`) with no server-side changes + - Configurable display timezone — `SolidQueueWeb.configure { |c| c.time_zone = "America/New_York" }` renders all dashboard timestamps in the configured zone rather than UTC; uses `in_time_zone` from ActiveSupport; defaults to UTC when not set; a `format_timestamp` helper centralises all timestamp formatting across jobs, failed jobs, history, search, queues, recurring tasks, and the scheduled-job turbo stream update - Sortable table columns — Jobs, Failed Jobs, and History tables now support server-side sorting via `?sort=` and `?direction=` params; click any column header to sort ascending or descending; sort state is preserved across filter changes, status tab switches, and period buttons; a `sort_header_th` helper generates accessible `` elements with `aria-sort` and direction indicators (↑/↓) diff --git a/ROADMAP.md b/ROADMAP.md index 3466e38..f0af29e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -11,9 +11,7 @@ Pull requests for any of these are welcome. See [Contributing](README.md#contrib *Quality-of-life improvements for teams using the dashboard daily.* -| Feature | Notes | -|---|---| -| **Sticky filter preferences** | Persist last-used status/period to `localStorage` so filters survive page reloads. | +*All planned features shipped.* --- diff --git a/app/javascript/solid_queue_web/application.js b/app/javascript/solid_queue_web/application.js index 589dcb8..690cbae 100644 --- a/app/javascript/solid_queue_web/application.js +++ b/app/javascript/solid_queue_web/application.js @@ -4,9 +4,11 @@ import SearchController from "solid_queue_web/search_controller" import RefreshController from "solid_queue_web/refresh_controller" import SelectionController from "solid_queue_web/selection_controller" import ThemeController from "solid_queue_web/theme_controller" +import FiltersController from "solid_queue_web/filters_controller" const application = Application.start() application.register("search", SearchController) application.register("refresh", RefreshController) application.register("selection", SelectionController) application.register("theme", ThemeController) +application.register("filters", FiltersController) diff --git a/app/javascript/solid_queue_web/filters_controller.js b/app/javascript/solid_queue_web/filters_controller.js new file mode 100644 index 0000000..2839c89 --- /dev/null +++ b/app/javascript/solid_queue_web/filters_controller.js @@ -0,0 +1,34 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static values = { page: String } + + connect() { + const url = new URL(window.location.href) + const params = url.searchParams + const page = this.pageValue + let changed = false + + const keys = page === "jobs" ? ["status", "period"] : ["period"] + keys.forEach(key => { + if (!params.has(key)) { + const saved = localStorage.getItem(`sqd-${page}-${key}`) + if (saved) { params.set(key, saved); changed = true } + } + }) + + if (changed) window.location.replace(url.toString()) + } + + saveStatus({ params: { status } }) { + if (status) localStorage.setItem(`sqd-${this.pageValue}-status`, status) + } + + savePeriod({ params: { period } }) { + if (period) { + localStorage.setItem(`sqd-${this.pageValue}-period`, period) + } else { + localStorage.removeItem(`sqd-${this.pageValue}-period`) + } + } +} \ No newline at end of file diff --git a/app/views/solid_queue_web/failed_jobs/index.html.erb b/app/views/solid_queue_web/failed_jobs/index.html.erb index 3040e5d..d4e476c 100644 --- a/app/views/solid_queue_web/failed_jobs/index.html.erb +++ b/app/views/solid_queue_web/failed_jobs/index.html.erb @@ -42,11 +42,11 @@ <% if @search.present? %> <%= link_to "Clear", failed_jobs_path(queue: @queue, period: @period), class: "sqd-btn sqd-btn--muted" %> <% end %> -
- <%= link_to "All", failed_jobs_path(queue: @queue, q: @search), class: @period.nil? ? "active" : "", aria: { current: @period.nil? ? "true" : nil }, aria_label: "All time" %> - <%= link_to "1h", failed_jobs_path(queue: @queue, q: @search, period: "1h"), class: @period == "1h" ? "active" : "", aria: { current: @period == "1h" ? "true" : nil }, aria_label: "Last 1 hour" %> - <%= link_to "24h", failed_jobs_path(queue: @queue, q: @search, period: "24h"), class: @period == "24h" ? "active" : "", aria: { current: @period == "24h" ? "true" : nil }, aria_label: "Last 24 hours" %> - <%= link_to "7d", failed_jobs_path(queue: @queue, q: @search, period: "7d"), class: @period == "7d" ? "active" : "", aria: { current: @period == "7d" ? "true" : nil }, aria_label: "Last 7 days" %> +
+ <%= link_to "All", failed_jobs_path(queue: @queue, q: @search), class: @period.nil? ? "active" : "", aria: { current: @period.nil? ? "true" : nil }, aria_label: "All time", data: { action: "click->filters#savePeriod", filters_period_param: "" } %> + <%= link_to "1h", failed_jobs_path(queue: @queue, q: @search, period: "1h"), class: @period == "1h" ? "active" : "", aria: { current: @period == "1h" ? "true" : nil }, aria_label: "Last 1 hour", data: { action: "click->filters#savePeriod", filters_period_param: "1h" } %> + <%= link_to "24h", failed_jobs_path(queue: @queue, q: @search, period: "24h"), class: @period == "24h" ? "active" : "", aria: { current: @period == "24h" ? "true" : nil }, aria_label: "Last 24 hours", data: { action: "click->filters#savePeriod", filters_period_param: "24h" } %> + <%= link_to "7d", failed_jobs_path(queue: @queue, q: @search, period: "7d"), class: @period == "7d" ? "active" : "", aria: { current: @period == "7d" ? "true" : nil }, aria_label: "Last 7 days", data: { action: "click->filters#savePeriod", filters_period_param: "7d" } %>
diff --git a/app/views/solid_queue_web/history/index.html.erb b/app/views/solid_queue_web/history/index.html.erb index bb5085b..7a9cf93 100644 --- a/app/views/solid_queue_web/history/index.html.erb +++ b/app/views/solid_queue_web/history/index.html.erb @@ -22,11 +22,11 @@ <% if @search.present? %> <%= link_to "Clear", history_path(queue: @queue, period: @period), class: "sqd-btn sqd-btn--muted" %> <% end %> -
- <%= link_to "All", history_path(queue: @queue, q: @search), class: @period.nil? ? "active" : "", aria: { current: @period.nil? ? "true" : nil } %> - <%= link_to "1h", history_path(queue: @queue, q: @search, period: "1h"), class: @period == "1h" ? "active" : "", aria: { current: @period == "1h" ? "true" : nil } %> - <%= link_to "24h", history_path(queue: @queue, q: @search, period: "24h"), class: @period == "24h" ? "active" : "", aria: { current: @period == "24h" ? "true" : nil } %> - <%= link_to "7d", history_path(queue: @queue, q: @search, period: "7d"), class: @period == "7d" ? "active" : "", aria: { current: @period == "7d" ? "true" : nil } %> +
+ <%= link_to "All", history_path(queue: @queue, q: @search), class: @period.nil? ? "active" : "", aria: { current: @period.nil? ? "true" : nil }, data: { action: "click->filters#savePeriod", filters_period_param: "" } %> + <%= link_to "1h", history_path(queue: @queue, q: @search, period: "1h"), class: @period == "1h" ? "active" : "", aria: { current: @period == "1h" ? "true" : nil }, data: { action: "click->filters#savePeriod", filters_period_param: "1h" } %> + <%= link_to "24h", history_path(queue: @queue, q: @search, period: "24h"), class: @period == "24h" ? "active" : "", aria: { current: @period == "24h" ? "true" : nil }, data: { action: "click->filters#savePeriod", filters_period_param: "24h" } %> + <%= link_to "7d", history_path(queue: @queue, q: @search, period: "7d"), class: @period == "7d" ? "active" : "", aria: { current: @period == "7d" ? "true" : nil }, data: { action: "click->filters#savePeriod", filters_period_param: "7d" } %>
diff --git a/app/views/solid_queue_web/jobs/index.html.erb b/app/views/solid_queue_web/jobs/index.html.erb index a754eff..35b2597 100644 --- a/app/views/solid_queue_web/jobs/index.html.erb +++ b/app/views/solid_queue_web/jobs/index.html.erb @@ -3,13 +3,14 @@ <%= turbo_frame_tag "jobs-table", data: { turbo_action: "advance", controller: "refresh", refresh_interval_value: SolidQueueWeb.default_refresh_interval } do %> <% discardable = SolidQueueWeb::Job::DISCARDABLE.include?(@status) %> +
- <%= link_to "Ready", jobs_path(status: "ready", q: @search, period: @period, priority: @priority, sort: @sort, direction: @direction), class: @status == "ready" ? "active" : "" %> - <%= link_to "Scheduled", jobs_path(status: "scheduled", q: @search, period: @period, priority: @priority, sort: @sort, direction: @direction), class: @status == "scheduled" ? "active" : "" %> - <%= link_to "Running", jobs_path(status: "claimed", q: @search, period: @period, priority: @priority, sort: @sort, direction: @direction), class: @status == "claimed" ? "active" : "" %> - <%= link_to "Blocked", jobs_path(status: "blocked", q: @search, period: @period, priority: @priority, sort: @sort, direction: @direction), class: @status == "blocked" ? "active" : "" %> - <%= link_to "Failed", jobs_path(status: "failed", q: @search, period: @period, priority: @priority, sort: @sort, direction: @direction), class: @status == "failed" ? "active" : "" %> + <%= link_to "Ready", jobs_path(status: "ready", q: @search, period: @period, priority: @priority, sort: @sort, direction: @direction), class: @status == "ready" ? "active" : "", data: { action: "click->filters#saveStatus", filters_status_param: "ready" } %> + <%= link_to "Scheduled", jobs_path(status: "scheduled", q: @search, period: @period, priority: @priority, sort: @sort, direction: @direction), class: @status == "scheduled" ? "active" : "", data: { action: "click->filters#saveStatus", filters_status_param: "scheduled" } %> + <%= link_to "Running", jobs_path(status: "claimed", q: @search, period: @period, priority: @priority, sort: @sort, direction: @direction), class: @status == "claimed" ? "active" : "", data: { action: "click->filters#saveStatus", filters_status_param: "claimed" } %> + <%= link_to "Blocked", jobs_path(status: "blocked", q: @search, period: @period, priority: @priority, sort: @sort, direction: @direction), class: @status == "blocked" ? "active" : "", data: { action: "click->filters#saveStatus", filters_status_param: "blocked" } %> + <%= link_to "Failed", jobs_path(status: "failed", q: @search, period: @period, priority: @priority, sort: @sort, direction: @direction), class: @status == "failed" ? "active" : "", data: { action: "click->filters#saveStatus", filters_status_param: "failed" } %>
<% if @jobs.any? %>
@@ -54,12 +55,13 @@ <%= link_to "Clear", jobs_path(status: @status, period: @period, sort: @sort, direction: @direction), class: "sqd-btn sqd-btn--muted" %> <% end %>
- <%= link_to "All", jobs_path(status: @status, q: @search, priority: @priority, sort: @sort, direction: @direction), class: @period.nil? ? "active" : "", aria: { current: @period.nil? ? "true" : nil }, aria_label: "All time" %> - <%= link_to "1h", jobs_path(status: @status, q: @search, priority: @priority, period: "1h", sort: @sort, direction: @direction), class: @period == "1h" ? "active" : "", aria: { current: @period == "1h" ? "true" : nil }, aria_label: "Last 1 hour" %> - <%= link_to "24h", jobs_path(status: @status, q: @search, priority: @priority, period: "24h", sort: @sort, direction: @direction), class: @period == "24h" ? "active" : "", aria: { current: @period == "24h" ? "true" : nil }, aria_label: "Last 24 hours" %> - <%= link_to "7d", jobs_path(status: @status, q: @search, priority: @priority, period: "7d", sort: @sort, direction: @direction), class: @period == "7d" ? "active" : "", aria: { current: @period == "7d" ? "true" : nil }, aria_label: "Last 7 days" %> + <%= link_to "All", jobs_path(status: @status, q: @search, priority: @priority, sort: @sort, direction: @direction), class: @period.nil? ? "active" : "", aria: { current: @period.nil? ? "true" : nil }, aria_label: "All time", data: { action: "click->filters#savePeriod", filters_period_param: "" } %> + <%= link_to "1h", jobs_path(status: @status, q: @search, priority: @priority, period: "1h", sort: @sort, direction: @direction), class: @period == "1h" ? "active" : "", aria: { current: @period == "1h" ? "true" : nil }, aria_label: "Last 1 hour", data: { action: "click->filters#savePeriod", filters_period_param: "1h" } %> + <%= link_to "24h", jobs_path(status: @status, q: @search, priority: @priority, period: "24h", sort: @sort, direction: @direction), class: @period == "24h" ? "active" : "", aria: { current: @period == "24h" ? "true" : nil }, aria_label: "Last 24 hours", data: { action: "click->filters#savePeriod", filters_period_param: "24h" } %> + <%= link_to "7d", jobs_path(status: @status, q: @search, priority: @priority, period: "7d", sort: @sort, direction: @direction), class: @period == "7d" ? "active" : "", aria: { current: @period == "7d" ? "true" : nil }, aria_label: "Last 7 days", data: { action: "click->filters#savePeriod", filters_period_param: "7d" } %>
+
<% if discardable && @jobs.any? %>
diff --git a/config/importmap.rb b/config/importmap.rb index cf8be47..781558f 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -3,3 +3,4 @@ pin "solid_queue_web/refresh_controller", to: "solid_queue_web/refresh_controller.js" pin "solid_queue_web/selection_controller", to: "solid_queue_web/selection_controller.js" pin "solid_queue_web/theme_controller", to: "solid_queue_web/theme_controller.js" +pin "solid_queue_web/filters_controller", to: "solid_queue_web/filters_controller.js"