diff --git a/CHANGELOG.md b/CHANGELOG.md index 28e6012..a216c4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Sticky filter preferences — last-used status, period, and queue filter saved to `localStorage`; a fresh visit to `/jobs` or `/history` with no URL params restores the previous selection automatically; uses a new `filter-persist` Stimulus controller; gracefully no-ops when `localStorage` is unavailable - Sortable columns on jobs, failed jobs, and history — server-side `?sort=&direction=` params; jobs sortable by class, queue, priority, enqueued at; failed jobs by class, queue, failed at; history by class, queue, finished at; sort state is preserved across filter and period changes ## [1.1.0] - 2026-05-27 diff --git a/README.md b/README.md index 4528be0..f910359 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,7 @@ The dashboard is designed to be mounted behind your application's existing authe - **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 - **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` - **Turbo Stream** job discard — removes the row inline without a full page reload +- **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 - **Dark mode** — toggle button in the header switches between light and dark palettes; preference persisted in `localStorage`; respects `prefers-color-scheme` on first visit - **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 - **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 diff --git a/ROADMAP.md b/ROADMAP.md index 6c923c3..33c0dc4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7,14 +7,6 @@ The path to v1.0.0 is staged: first achieve feature parity with `solid_queue_das --- -## v1.2 — UX Polish - -> _Quality-of-life improvements for teams using the dashboard daily._ - -- **Sticky filter preferences** — persist last-used status, period, and queue filter to `localStorage` so filter state survives page reloads - ---- - ## v1.3 — Alerting Depth > _More signals, fewer blind spots._ diff --git a/app/javascript/solid_stack_web/application.js b/app/javascript/solid_stack_web/application.js index 728ef60..314987d 100644 --- a/app/javascript/solid_stack_web/application.js +++ b/app/javascript/solid_stack_web/application.js @@ -1,5 +1,6 @@ import "@hotwired/turbo" import { Application } from "@hotwired/stimulus" +import FilterPersistController from "solid_stack_web/filter_persist_controller" import RefreshController from "solid_stack_web/refresh_controller" import SearchController from "solid_stack_web/search_controller" import SelectionController from "solid_stack_web/selection_controller" @@ -8,6 +9,7 @@ import ThemeController from "solid_stack_web/theme_controller" import TimestampController from "solid_stack_web/timestamp_controller" const application = Application.start() +application.register("filter-persist", FilterPersistController) application.register("refresh", RefreshController) application.register("search", SearchController) application.register("selection", SelectionController) diff --git a/app/javascript/solid_stack_web/filter_persist_controller.js b/app/javascript/solid_stack_web/filter_persist_controller.js new file mode 100644 index 0000000..215978f --- /dev/null +++ b/app/javascript/solid_stack_web/filter_persist_controller.js @@ -0,0 +1,28 @@ +import { Controller } from "@hotwired/stimulus" + +const PERSIST_KEYS = ["status", "period", "queue"] + +export default class extends Controller { + static values = { key: String } + + connect() { + const params = new URLSearchParams(window.location.search) + + if (params.toString()) { + const toSave = new URLSearchParams() + PERSIST_KEYS.forEach(k => { if (params.get(k)) toSave.set(k, params.get(k)) }) + if (toSave.toString()) this.write(toSave.toString()) + } else { + const stored = this.read() + if (stored) window.location.replace(`${window.location.pathname}?${stored}`) + } + } + + read() { + try { return localStorage.getItem(this.keyValue) } catch (e) { return null } + } + + write(value) { + try { localStorage.setItem(this.keyValue, value) } catch (e) { /* ignore */ } + } +} \ No newline at end of file diff --git a/app/views/solid_stack_web/history/index.html.erb b/app/views/solid_stack_web/history/index.html.erb index c1e6738..88a0e93 100644 --- a/app/views/solid_stack_web/history/index.html.erb +++ b/app/views/solid_stack_web/history/index.html.erb @@ -10,7 +10,9 @@ -
+ <% if @queue.present? %> <% end %> diff --git a/app/views/solid_stack_web/jobs/index.html.erb b/app/views/solid_stack_web/jobs/index.html.erb index 546e41c..dcca7c0 100644 --- a/app/views/solid_stack_web/jobs/index.html.erb +++ b/app/views/solid_stack_web/jobs/index.html.erb @@ -32,7 +32,9 @@ <%= turbo_frame_tag "sqw-jobs-filter", data: { turbo_action: "advance", controller: "refresh", refresh_interval_value: SolidStackWeb.default_refresh_interval } do %> - + <%= hidden_field_tag :status, @status %> <%= hidden_field_tag :period, @period %> <%= hidden_field_tag :sort, @sort %> diff --git a/spec/requests/solid_stack_web/history_spec.rb b/spec/requests/solid_stack_web/history_spec.rb index 06108f9..571c660 100644 --- a/spec/requests/solid_stack_web/history_spec.rb +++ b/spec/requests/solid_stack_web/history_spec.rb @@ -50,6 +50,12 @@ def finished_job(class_name: "TestJob", queue_name: "default", duration: 30, fin get "#{engine_root}/history" expect(response.body).to include("sqw-subnav__link--active") end + + it "includes filter-persist controller with the history key" do + get "#{engine_root}/history" + expect(response.body).to include("filter-persist") + expect(response.body).to include("sqw-history-filters") + end end describe "GET /history?period=" do diff --git a/spec/requests/solid_stack_web/jobs_spec.rb b/spec/requests/solid_stack_web/jobs_spec.rb index 08854ef..ce620ec 100644 --- a/spec/requests/solid_stack_web/jobs_spec.rb +++ b/spec/requests/solid_stack_web/jobs_spec.rb @@ -29,6 +29,12 @@ def create_ready(class_name: "MyJob", queue_name: "default", priority: 0) expect(response.body).to include('name="q"') expect(response.body).to include("sqw-period-filter") end + + it "includes filter-persist controller with the jobs key" do + get "#{engine_root}/jobs" + expect(response.body).to include("filter-persist") + expect(response.body).to include("sqw-jobs-filters") + end end describe "GET /jobs?q=" do